├── README.md ├── lotto ├── Readme.md ├── step1 │ ├── hope.md │ ├── img │ │ ├── schema82.png │ │ ├── schema84.png │ │ └── schema92.png │ ├── lokba.md │ ├── lotto-step1-integrated-review.md │ ├── marco.md │ └── movie.md └── step2 │ ├── hope.md │ ├── kkojae.md │ ├── lokba.md │ ├── lotto-step2-intergrated-review.md │ ├── marco.md │ └── movie.md ├── racing-car └── marco.md ├── vending-machine └── README.md └── youtube ├── README.md ├── step1 ├── hope.md ├── img │ └── class_diagram.png ├── intergrated-review.md ├── kkojae.md ├── lokba.md ├── marco.md └── movie.md └── step2 ├── hope.md ├── intergrated-review.md ├── kkojae.md ├── lokba.md ├── marco.md └── movie.md /README.md: -------------------------------------------------------------------------------- 1 | # 코드리뷰스터디(코리스) 2 | 3 | ## 0. 코드리뷰스터디 결과물 4 | 5 | |통합정리본|참여자| 6 | |:---:|:---:| 7 | |[자동차게임 미션](https://github.com/woowacourse-study/2022-code-review-study/blob/main/racing-car/marco.md)|마르코| 8 | |[로또 미션 step1](https://github.com/woowacourse-study/2022-code-review-study/blob/main/lotto/step1/lotto-step1-integrated-review.md)|마르코, 호프, 록바, 무비| 9 | |[로또 미션 step2](https://github.com/woowacourse-study/2022-code-review-study/blob/main/lotto/step2/lotto-step2-intergrated-review.md)|마르코, 호프, 록바, 무비, 꼬재| 10 | |[유튜브 미션 step1](https://github.com/woowacourse-study/2022-code-review-study/blob/main/youtube/step1/intergrated-review.md)|마르코, 호프, 록바, 무비, 꼬재| 11 | |[유튜브 미션 step2](https://github.com/woowacourse-study/2022-code-review-study/blob/main/youtube/step2/intergrated-review.md)|마르코, 호프, 록바, 무비, 꼬재| 12 | 13 | ## 1. 스터디 소개 14 | 15 | - 우아한테크코스 4기 미션 코드리뷰를 '리뷰'하는 스터디입니다. 16 | - 주요 활동은 다른 크루들이 받은 좋은 코드리뷰들을 각자 분량을 나누어 정리한 후 함께 공유하는 것입니다. 17 | - 목표는 코드리뷰를 통하여 `메타인지` 및 `클린 코드` 역량 향상입니다. 18 | - 스터디원 19 | 20 | |Nickname|Github| 21 | |:---:|:---:| 22 | |마르코|[@wonsss](https://github.com/wonsss)| 23 | |록바|[@lokba](https://github.com/lokba)| 24 | |무비|[@byhhh2](https://github.com/byhhh2)| 25 | |호프|[@moonheekim0118](https://github.com/moonheekim0118)| 26 | |꼬재|[@kkojae91](https://github.com/kkojae91)| 27 | 28 | ## 2. 일정 29 | 30 | - 스터디 운영 기한 31 | - 2.28. ~ level1 로또 미션까지 한시적으로 진행합니다. 32 | - 이후 참여의사 확인 및 피드백을 거쳐서 모임을 재시작합니다. 33 | - 스터디 모임(화상회의) 주기 34 | - step1 리뷰 스터디는 step1 종료한 날(일요일)의 다음 주 금요일 저녁 9시 35 | - step2 리뷰 스터디는 step2 종료한 날(월요일) 주 금요일 저녁 9시 36 | - 정리한 내용 PR 기한 37 | - 모임 전날인 목요일 밤 12시까지 38 | - 토론 주제 상정 기한 39 | - 모임 당일 오후 5시까지 40 | 41 | ## 3. 주요활동 42 | 43 | - 1인당 담당하는 코드리뷰PR은 8개이며, 자신과 페어를 포함하여 페어프로그래밍한 총 네 팀의 코드를 묶어서 맡습니다. 44 | 45 | ### 3-1. 피드백 정리 후 PR 46 | 47 | - 스터디원들은 각자 맡은 코드리뷰PR 피드백들을 정리하고 작성합니다. 48 | - 크게 `아키텍처 분석`과 `피드백 정리`를 수행합니다. `아키텍처 분석`은 담당한 4개(=페어 4팀)의 소프트웨어의 아키텍처 중 느낌이 오는 코드를 선택하여 간단히 분석하고 설명합니다(부담 갖지 말고 그냥 느끼는 대로 편하게 설명해주시면 될 것 같아요). `피드백 정리`는 리뷰어들이 8개의 PR에 달은 코멘트 중 `Aha` 모멘트가 있던 피드백을 메모하는 작업입니다. 49 | - 양식은 아래와 같습니다. 50 | 51 | ```plain 52 | # Level번호 미션명 Step번호(담당 PR 번호들) - 작성자닉네임 53 | - 분석 담당 코드 54 | - PR 번호(PR 링크) 55 | - PR 번호(PR 링크) 56 | ... 57 | 58 | ## 아키텍처 분석(desc: 담당한 4개의 소프트웨어의 아키텍처를 간단히 분석하고 설명합니다) 59 | - [해당 PR 번호] 내용 60 | - 상세 내용 61 | 62 | ## 피드백 정리 63 | ### 대분류(ex: 아키텍처, 함수/클래스, 컨벤션, DOM, 테스트 등) 64 | - [해당 PR 번호] 내용 65 | - 상세 내용 66 | ``` 67 | 68 | - 정리한 내용을 본 저장소에 `Pull Requests`합니다(기한: 모임 전날 목요일 밤12시). 69 | - 다음과 같이 폴더링하여 올려주세요. 예시: `lotto/step1/{닉네임}}.md` 70 | - PR 제목 : `[lotto-step1-lokba] 리뷰 분석 제출합니다.` 71 | - 아키텍처 분석 및 피드백 정리 도중 의문이 든 주제가 있을 경우, `Discussion`에 글을 올립니다(기한: 모임 당일 오후 5시). 72 | - `Discussion`에 올라온 토론 주제나 질문을 보고 자신의 의견을 미리 생각해보고 토론을 준비합니다. 73 | 74 | ### 3-2. 발표 및 토론 75 | 76 | - `매주 금요일 오후 9시`에 `모임(줌)`을 가지어, 발표 및 토론을 2시간 이내로 진행합니다. 77 | - 발표 78 | - 각자 정리한 내용을 10분씩 발표합니다. 79 | - 질의응답 80 | - 1명씩 발표를 마친 후, 질의응답 시간을 5분씩 갖습니다. 81 | - 토론 82 | - 발표 및 질의응답을 모두 마친 후, `Discussion`에 게시된 주제에 대해 토론을 진행합니다. 83 | - 토론 후, 각 `Discussion`의 주제에 대해 새로 알게 된 내용이나 정리된 본인의 의견을 `answer`로 남깁니다. 84 | 85 | ## 4. 운영 관련 86 | 87 | - 매주 스터디원들이 번갈아 가며 모임 운영 역할을 맡습니다. 88 | - 운영자 역할 89 | - 스터디원들에게 담당 코드리뷰PR 분배 90 | - 스터디원들이 올린 PR 검토 후 merge 91 | - PR 마감 기한 지연 시, 스터디원에게 일정 준수 요청 92 | - 발표 및 토론 시 모임 진행 93 | - 각자 올린 정리 파일을 하나로 편집하여 통합(주말까지) 94 | - 스터디원은 총 4명으로 구성합니다(추후 검토). 95 | - 매주 각자 분석할 코드리뷰PR의 분량은 8개입니다. 96 | - 나와 나의 페어(2개), 페어 3팀(6개) 97 | - 4*8=32... 37-32=5... 기본 배정되지 않은 5개의 PR은 스터디원들이 자율적으로 담당합니다. 98 | -------------------------------------------------------------------------------- /lotto/Readme.md: -------------------------------------------------------------------------------- 1 | # 로또 미션 2 | 3 | ## 페어 확인 4 | - [x] 동키콩, 온스타 5 | - [x] 위니, 자스민 6 | - [x] 앨버, 결 7 | - [x] 안, 코이 8 | - [x] 나인, 비녀 9 | - [x] 하리, 록바, 코카콜라 10 | - [x] 후이, 콤피 11 | - [x] 블링, 태태 12 | - [x] 아놀드, 빅터 13 | - [x] 돔하디, 민초 14 | - [x] 시지프, 호프 15 | - [x] 우연, 소피아 16 | - [x] 병민, 티거 17 | - [x] 유세지, 준찌 18 | - [x] 꼬재, 도리 19 | - [x] 샐리, 해리 20 | - [x] 마르코, 무비 21 | - [x] 밧드, 우디 22 | 23 | ## 담당 페어 코드 24 | ### 마르코 25 | - 유세지, 준찌 26 | - 동키콩, 온스타 27 | - 위니, 자스민 28 | - 아놀드, 빅터 29 | 30 | ### 호프 31 | - 시지프, 호프 32 | - 병민, 티거 33 | - 우연, 소피아 34 | - 해리 35 | 36 | ### 록바 37 | - 하리, 록바, 코카콜라 38 | - 블링, 태태 39 | - 돔하디, 민초 40 | - 안 41 | 42 | ### 무비 43 | - 마르코, 무비 44 | - 밧드, 우디 45 | - 앨버, 결 46 | - 샐리 47 | 48 | ### 꼬재 49 | - 꼬재, 도리 50 | - 후이, 콤피 51 | - 나인, 비녀 52 | - 코이 53 | -------------------------------------------------------------------------------- /lotto/step1/hope.md: -------------------------------------------------------------------------------- 1 | # Level1 로또 Step1(담당 PR 번호들) - 호프 2 | - 분석 담당 코드 3 | - [#83-시지프 ](https://github.com/woowacourse/javascript-lotto/pull/83) 4 | - [#89-호프](https://github.com/woowacourse/javascript-lotto/pull/89) 5 | - [#116-티거](https://github.com/woowacourse/javascript-lotto/pull/116) 6 | - [#119-병민](https://github.com/woowacourse/javascript-lotto/pull/119) 7 | - [#112-도리](https://github.com/woowacourse/javascript-lotto/pull/112) 8 | - [#113-꼬재](https://github.com/woowacourse/javascript-lotto/pull/113) 9 | - [#107-소피아](https://github.com/woowacourse/javascript-lotto/pull/107) 10 | - [#110-우연](https://github.com/woowacourse/javascript-lotto/pull/110) 11 | … 12 | 13 | ## 아키텍처 분석 14 | 15 | ### #83 시지프 16 | ![](https://i.ibb.co/f4YqcNz/IMG-1597.jpg) 17 | - Model, Controller, View 로 분리 18 | #### Model 19 | - 발급받은 로또를 관리한다. 20 | - 발급될 로또 수량을 매개변수로 받으면 로또 넘버를 생성해 준 후, 저장한다. 21 | 22 | #### Controller 23 | - View로 넘겨줄 이벤트 핸들러를 정의한다. 24 | - 이 때, 이벤트 핸들러는 여러가지의 View와 Model을 조작하는 역할을 한다. 25 | - View에 이벤트 핸들러를 넘겨줄 때는 아래와 같이 해당하는 View의 `bindSubmitCash` 메서드의 인자로서 핸들러를 넘겨준다. 26 | 27 | ```javascript 28 | #bindEventHandlers() { 29 | this.#purchaseFormView.bindSubmitCash(cash => this.#handleSubmitCash(cash)); 30 | } 31 | ``` 32 | 33 | #### View 34 | - 해당 영역(section)의 이벤트 핸들러를 붙여준다. 35 | - 이벤트가 View에서만 관리될 수도 있고, 필요시 위의 Controller로부터 추가 핸들러를 받아온다. 36 | 37 | - 전형적인 MVC패턴으로 Model 과 View를 Controller가 연결해준다. 38 | - Model과 View 간의 영역 침범(?)이 없다. 둘은 서로의 존재를 아예 모른다. 39 | 40 | 41 | 42 | ### #89 호프 43 | - 위의 83번 페어코드와 다른 점만 설명합니다. 44 | ```javascript 45 | export default class View { 46 | constructor(props = {}) { 47 | this.props = props; 48 | this._init(); 49 | } 50 | _init() { 51 | this._configureDOM(); 52 | this._bindEvents(); 53 | } 54 | _bindEvents() {} 55 | _configureDOM() {} 56 | } 57 | ``` 58 | - View 추상클래스를 정의해서, 반복되는 부분을 제거한다. 59 | - configureDOM : 해당 View에서 필요한 Dom Element를 선택하는 영역 60 | - bindEvents : 해당 View 에서 필요한 이벤트 리스너를 붙이는 영역 61 | - 객체가 생성되면 필요한 Dom Elemet를 선택하고, 이벤트 리스너를 붙인다. 62 | ```javascript 63 | #purchaseFormView = new PurchaseFormView({ 64 | submitCashHandler: cash => this.#submitCashHanlder(cash), 65 | }); 66 | ``` 67 | - #83 의 코드에서는 Controller 의 이벤트 핸들러를 View 로 전달할 때, 해당 View 인스턴스의 bindEvents 메서드를 직접 실행한 반면, 해당 코드에서는 View의 생성자 매개변수(props)로 필요한 이벤트 핸들러를 전달 68 | - 로또 번호를 생성하는 함수는 모델 -> util로 분리 69 | - 왜 ? 로또 번호 생성은 엄밀히 말하면 Model의 역할이 아니라고 판단했기 때문입니다. Model은 `데이터를 생성하고 저장`하는 영역이라고 생각했기 때문에 로또 번호를 만드는 로직을 Model 클래스가 알 필요가 없다고 생각합니다. 추후에 로또 번호 생성 로직이 달라질 수도 있구요. 70 | 71 | 72 | 73 | 74 | ### #116 티거 / #119 병민 75 | ![](https://i.ibb.co/J2mQPGS/IMG-1601.jpg) 76 | - flux 패턴을 사용하여 Component를 Store의 구독자로 등록. Store 내부의 상태가 변경될 시, Store의 구독자 Component를 모두 리렌더링하여 새로운 상태를 반영하도록 구현 77 | 78 | #### Store 79 | ```javascript 80 | class Store { 81 | #subscribers = []; 82 | #state = {}; 83 | 84 | constructor(initialState) { 85 | this.#state = initialState; 86 | } 87 | 88 | subscribe(component) { 89 | this.#subscribers.push(component); 90 | } 91 | 92 | dispatch(action) { 93 | const newState = reducer(this.getState(), action); 94 | this.#setState(newState); 95 | this.#subscribers.forEach((subscriber) => { 96 | subscriber.notify(); 97 | }); 98 | } 99 | getState() { 100 | return this.#state; 101 | } 102 | 103 | #setState(newState) { 104 | this.#state = newState; 105 | } 106 | } 107 | ``` 108 | 109 | - 생성자에서 초기 상태값을 받아온다. 110 | - subscribe : 해당 메서드를 통해 원하는 컴포넌트를 구독자로 등록한다. 111 | - dispatch : 컴포넌트에서 상태를 변경 할 때 실행하는 메서드. action을 매개변수로 받고, 등록된 action에 따라 새로운 상태값이 reducer로부터 반환되고 상태가 업데이트 된다. 업데이트 된 후, 구독자 Component들에게 notify로 상태값이 변경되었음을 알린다. 112 | 113 | 114 | #### Reducer 115 | ```javascript 116 | export default function reducer(state, { type, payload }) { 117 | const newState = { ...state }; 118 | switch (type) { 119 | case ACTION.PURCHASE_LOTTO: { 120 | newState.money = payload; 121 | newState.lottoList = generateLottoList(payload); 122 | return newState; 123 | } 124 | case ACTION.TOGGLE_LOTTO_LIST: 125 | newState.lottoListVisibility = payload; 126 | return newState; 127 | case ACTION.SET_WINNING_NUMBERS: 128 | newState.winningNumbers = payload; 129 | return newState; 130 | default: 131 | return state; 132 | } 133 | } 134 | ``` 135 | 136 | - 위의 Store에서 사용된 Reducer 함수 137 | - 액션 타입에 따라서 다른 상태값을 받아온다. 138 | - payload 는 액션과 함께 받아온 새로운 데이터를 의미한다. 139 | - 알려지지 않은 액션타입이 들어온 경우 기존의 상태를 반환한다. 140 | 141 | #### Component 142 | ```javascript 143 | class Component extends HTMLElement { 144 | connectedCallback() { 145 | this.render(); 146 | this.subscribe(); 147 | this.setEvent(); 148 | } 149 | render() { 150 | this.innerHTML = this.template(); 151 | } 152 | template() { 153 | return ''; 154 | } 155 | 156 | subscribe() { 157 | window.store.subscribe(this); 158 | } 159 | setEvent() {} 160 | 161 | addEvent(eventType, selector, callback) { 162 | const children = [...this.querySelectorAll(selector)]; 163 | const isTarget = (target) => children.includes(target) || target.closest(selector); 164 | this.addEventListener(eventType, (event) => { 165 | if (!isTarget(event.target)) { 166 | return false; 167 | } 168 | return callback(event); 169 | }); 170 | } 171 | notify() { 172 | this.render(); 173 | } 174 | hide() { 175 | this.setAttribute('hidden', true); 176 | } 177 | show() { 178 | this.removeAttribute('hidden'); 179 | } 180 | } 181 | ``` 182 | - Custom Element 를 사용하였다. 183 | - Component 추상 클래스가 애초에 HTMLElement 를 상속받는다.즉, Component 자체가 하나의 Element를 의미한다. 184 | - connectedCallback : Custom Element의 라이프사이클 중 하나로, Custom Element가 등록될 때 제일 처음 실행되는 메서드 185 | - render : 해당 Element의 innerHTML을 수정한다. 186 | - subscribe: 해당 Component 생성과 함께 실행되어, Component를 위에서 생성된 Store의 구독자로 등록한다. 187 | - setEvent: 아래의 addEvent 메서드를 호출한다. 188 | - addEvent: 이벤트 타입, 셀렉터, 이벤트 핸들러 콜백 함수를 받아온다. 이벤트 리스너를 맨 위의 root (this) 에 등록하고, eventTarget 을 검사하여 콜백을 실행한다. 189 | - notify : Store에서 상태 변경시 실행되어 리렌더링 한다. 190 | 191 | - 개인적인 의견으로 아쉬운 점은 모든 컴포넌트가 하나의 스토어를 바라보고 있다는 점입니다. 물론 로또앱 자체가 작아서 리렌더링이 심하게 일어나지는 않지만, 로또 상태와 관련이 없는 불필요한 부분에도 리렌더링이 지속적으로 일어날 가능성이 높아서 아쉽습니다. 상태에 상관 없는 부분, 예를들면 Form 같은 부분은 Container 로 또 다른 추상 클래스를 정의해서 상속 받아서 불필요한 notify를 줄이는건 어떨까요 ? 192 | - 혹은 해당 Store의 상태를 직접적으로 참조하는 Component만 리렌더링되게 하는 방법이 있지 않을까요? 193 | 194 | 195 | ### #112- 도리 / 113- 꼬재 196 | ![](https://i.ibb.co/jyd95B1/IMG-1602.jpg) 197 | - LottoApp 영역에서 이벤트 바인딩, 핸들링, UI 조작 등을 모두 담당한다. 198 | - LottoApp 에서 LottoList 라는 데이터를 관리하는데, LottoList 는 Lotto 객체의 배열로 이루어져 있다. 199 | - index.html에 다른 Element가 등록되어 있지 않기 때문에 이벤트를 app 엘리먼트 (최상위) 에 바인딩 시켜준 점이 인상깊다. 200 | 201 | #### bindEventListener 함수 202 | ```javascript 203 | export const bindEventListener = ({ appElement, type, selector, callback }) => { 204 | const children = [...getElements(selector)]; 205 | const isTarget = (target) => 206 | children.includes(target) || target.closest(selector); 207 | appElement.addEventListener(type, (e) => { 208 | if (!isTarget(e.target)) return; 209 | e.preventDefault(); 210 | callback(e); 211 | }); 212 | }; 213 | ``` 214 | 215 | - 위의 함수가 클로져를 잘 사용했다고 생각해서 발췌 216 | - app에 이벤트를 바인딩 시킨다. 217 | - 받은 selector를 사용해서 app 내부에 있는 selector에 해당하는 모든 element를 가져온다. 218 | - isTarget 함수를 클로져로 저장하여, 아래 이벤트 리스너 콜백에서 현재 event.target이 등록된 이벤트 타겟인지 검사한다. 219 | - 아쉬운 점이 있다면 ! e.preventDefault(); 호출을 type 이 submit에만 해줘도 되지 않을까? 하는 점 220 | 221 | 222 | ### #107- 소피아 / 110 - 우연 223 | ![](https://i.ibb.co/TWST6Nz/IMG-1603.jpg) 224 | - LottoMachine, LottoManager, LottoMachineView, Lotto 클래스로 나뉘어진다. 225 | - MVC 패턴으로 보이지만, 클래스 이름을 도메인과 관련하여 지으니까 훨씬 유의미 하다는 것을 깨달았다. 226 | 227 | #### LottoMachine 228 | - lottoManager (Model) 과 lottoMachineView (View) 인스턴스를 생성한다. 229 | - 이벤트를 바인딩하고 처리한다. 230 | 231 | #### LottoMachineView 232 | - UI 조작을 담당한다. 233 | 234 | #### LottoManager 235 | - 모델 역할을 하여 로또를 생성한다. 236 | - lottos [] 배열은 Lotto 객체 배열이다. 237 | 238 | #### Lotto 239 | - 로또 번호를 생성한다. 240 | 241 | 242 | ## 피드백 정리 243 | ### 컨벤션 244 | - [#116] switch 문 245 | - break를 하기보다 return을 바로 하는게 더 안전하지 않을까요? 또한 break를 하고 마지막에 return하기보다 액션이 없는경우 default로 return 하는것이 더 명료할것 같아요. 246 | 247 | ### 클래스 248 | - [#83] 은닉화 249 | - 외부에서 사용하지 않는 변수나 메서드는 무조건 private 으로 처리해주면 좋은가? 250 | - 외부에서 알 필요가 없는 메서드라면 private로 처리해주는게 맞다고 생각합니다. 예를들어 전기포트가 있다고 할 때 저희는 전원을 키고 끄는법에 대해서만 알면되고 내부에서 어떻게 전기포트에서 열이 올라서 물을 끓이는지 세부적인 내용에 대해 사용자가 알 필요가 없는 부분과 같은 맥락이라 생각합니다.중요한 데이터를 외부에서 건드릴 수 없도록 + 외부에서 사용하지 않는다의 맥락이 모두 위의 예시에 빗대어 생각해보시면 어느정도 윤곽이 잡히시지 않을 까 생각해봅니다. 251 | - [#112] 생성자의 파라미터로 넘기기 vs setter 사용하기 252 | - 생성자의 파라미터로 넘기는 건, 인스턴스를 생성할 때부터 값을 셋팅해주는거고, 기본적으로 OOP 에서는 외부에서 직접적으로 객체의 데이터에 접근하는 것을 막기 위해 setter 를 사용합니다. (같이 봐야하는 개념으로 private 이 있습니다) 만약 외부에서 변경이 가능하다면 우리가 정의한 객체의 무결성이 깨질 수 있기 때문입니다. 예를 들어, lottoList 가 private 하지 않는다면 아래와 같이 객체를 깨뜨릴 수 있겠죠. 그렇기 때문에 setter 를 사용합니다..! 253 | 254 | ### 함수 255 | - [#83] 함수 분리 기준 256 | - 함수를 분리하지 않더라도 충분히 이해할 수 있는 범주의 역할인데다가 과도한 분리는 오히려 유지보수를 어렵게 만든다는 생각을 하고있기에 코멘트를 드렸습니다. 혹 재사용을 하더라도 분리할 필요성이 떨어지는 함수라고 생각이 들어요. 우선은 제가 함수를 분리하는 관점은 기능 확장 될 여지가 있거나 내부의 구현이 복잡하다면 추상화를 통해 미리 해서 분리한다. 기능이 확장 될 여지가 없거나 코드가 짧거나(코드를 처음 봤을때 맥락이 애해되는 범주) 재사용성이 떨어진다면 분리하지 않는 방향성들을 고려해보고 판단하는게 맞는 방향이라고 생각됩니다 257 | 258 | 259 | ### 도메인 관련 함수 260 | - [#83] 로또 번호 추첨 함수 261 | - 더 나아가서 Set을 통해서 중복되었을 때 한번 더 루프를 도는 방식이 아니라 무조건 NUMBERS_COUNT번만에 끝낼 수 있는 방식으로 구현할 수 있을까요? 실제로 로또 번호를 추첨하는 방식처럼요! 262 | - 1~45까지의 배열에서 최대값이 배열의 크기인 랜덤한 수를 얻어 해당 index를 기반으로 spliced를 통해 로또 번호를 뽑는 방식 263 | 264 | - [#112] validation 함수 네이밍 및 역할 관련 265 | ```javascript 266 | const isDivisibleBy = (payment, price) => { 267 | if (payment % price !== 0) { 268 | throw new Error(ERROR_MESSAGE.MONEY_OUT_OF_STANDARD) 269 | 270 | } 271 | return parseInt(payment / price); 272 | }; 273 | ``` 274 | - 저는 파라미터가 두번째 파라미터로 나누어 떨어져? 라고 물어봤는데 응/아니야 라고 대답하는 게 아니라 Error! / 계산된 값 줄게 으로 대답하는 게 조금은 이상한데 어떻게 생각하시는 지 궁금합니다…! isDivisibleBy 보단 차라리, divideBy 가 어떨까 싶어요..! (이걸로 나눠줘! -> 불가능 그러니까 에러 발생시킬게! / 그래 여기) 275 | 276 | ### 네이밍 277 | - [#83] - set 278 | - setOnSubmitCash 네이밍도 나쁘진 않은 것 같은데 아무래도 네이밍 앞에 set이 붙으면 이벤트 바인딩보다는 내부에 있는 상태를 설정하는 느낌이 드네요 279 | 280 | ### DOM 281 | - [#113] SVG vs PNG 282 | - 이미지를 svg로 사용하게 되면 좋은 이유: 283 | 1. 모든 크기의 브라우저를 원할하게 지원한다. (무한한 확장이 가능) 284 | 2. svg일부를 스타일링 할 수 있다. 285 | 3. dom을 사용하여 svg를 실시간으로 수정할 수 있다. 286 | 4. 용량이 적어 출력이 빠르다. 287 | 이미지를 png로 사용해야하는 경우: 288 | * 백그라운드 이미지와 같이 이미지를 벡터화하기 어려울 경우 289 | 디자이너가 넘겨주는 파일을 사용하는게 맞겠지만, 290 | 백그라운드 이미지와 같이 이미지를 벡터화하기 어렵다면 png를 사용하고 그렇지 않은 경우에는 svg를 사용하는 편이 좋은 거군요?! 291 | 292 | ### CSS 293 | - [#116] CSS 선택자 294 | - 순서에 의존하는 선택자는 지양 295 | - lotto-list > . Lotto-list-container . 와 같은 선택자는 지양 296 | - [#97] 0px 297 | - margin,padding 같은 속성에서 0으로 속성을 줘야할 때 px을 주지 않는 것이 성능상 유리하다. 298 | 299 | ### 웹 접근성 300 | - [#110] hidden 301 | - 스크린리더기에서 읽혀야 하지만, 화면에 보이지 않아야 할 때는 어떻게 해야할까? 302 | - CSS의 visibility hidden 속성을 주도록 한다. 303 | - 따라서 , section 내부 타이틀을 설정하고 , hidden 속성을 주도록 한다. 304 | 305 | 306 | -------------------------------------------------------------------------------- /lotto/step1/img/schema82.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woowacourse-study/2022-code-review-study/35261e14ace715a90b33c211f885ac269a6ee996/lotto/step1/img/schema82.png -------------------------------------------------------------------------------- /lotto/step1/img/schema84.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woowacourse-study/2022-code-review-study/35261e14ace715a90b33c211f885ac269a6ee996/lotto/step1/img/schema84.png -------------------------------------------------------------------------------- /lotto/step1/img/schema92.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woowacourse-study/2022-code-review-study/35261e14ace715a90b33c211f885ac269a6ee996/lotto/step1/img/schema92.png -------------------------------------------------------------------------------- /lotto/step1/lokba.md: -------------------------------------------------------------------------------- 1 | # Level1 Lotto Step1(담당 PR 번호들) - 록바 2 | 3 | - 분석 담당 코드 4 | - 후이 [#93](https://github.com/woowacourse/javascript-lotto/pull/93) 5 | - 콤피 [#98](https://github.com/woowacourse/javascript-lotto/pull/98) 6 | - 돔하디 [#108](https://github.com/woowacourse/javascript-lotto/pull/108) 7 | - 민초 [#104](https://github.com/woowacourse/javascript-lotto/pull/104) 8 | - 블링 [#88](https://github.com/woowacourse/javascript-lotto/pull/88) 9 | - 태태 [#91](https://github.com/woowacourse/javascript-lotto/pull/91) 10 | - 하리 [#115](https://github.com/woowacourse/javascript-lotto/pull/115) 11 | - 록바 [#103](https://github.com/woowacourse/javascript-lotto/pull/103) 12 | - 코카콜라 [#111](https://github.com/woowacourse/javascript-lotto/pull/111) 13 | - 안 [#114](https://github.com/woowacourse/javascript-lotto/pull/114) 14 | 15 |
16 | 17 | ## 아키텍처 분석(desc: 담당한 4개의 소프트웨어의 아키텍처를 간단히 분석하고 설명합니다) 18 | 19 | ### [#93, #98] 후이, 콤피 20 | 21 | 파일 구조도는 MVC 패턴을 따르고 있다. 22 | 23 | - Model - Lotto, LottosModel 24 | - View - MoneyInputView, LottoListView 25 | - Controller - LottoController 26 | 27 | #### Controller 28 | 29 | - Controller는 Model, View에게 지시하는 역할을 수행하고 있다. 30 | 31 | - Model, View 인스턴스를 생성 후, 멤버 변수로 가지고 있다. 32 | 33 | - 인스턴스 생성을 constructor()에서 하지 않고 private 필드에서 진행하였다. 34 | 35 | ```javascript 36 | class LottoController{ 37 | #MoneyInputView = new MoneyInputView($(`.${SELECTOR.CLASS.LOTTO_MONEY_SECTION}`)); 38 | #LottoListView = new LottoListView($(`.${SELECTOR.CLASS.LOTTO_LIST_SECTION}`)); 39 | #LottosModel = new LottosModel(); 40 | 41 | constructor() {...} 42 | } 43 | ``` 44 | 45 | - constructor()에서 이벤트를 바인딩하는 함수를 호출하고 있다. 46 | - dom을 View에서 관리하다 보니, Controller에서 수행햐는 함수를 콜백함수로 전달하고 있다. 47 | - Controller는 메인 로직인, 입력받는 금액을 바탕으로 Model에 접근하여 상태 데이터를 간접적으로 변경하고 View에 화면의 변화를 주도록 지시하고 있다. 48 | 49 |
50 | 51 | #### View 52 | 53 | - View는 MoneyInputView와 LottoListView로 구성되어 있다. 54 | 55 | - Controller에서 인스턴스 생성시 전달한 dom 객체를 멤버변수로 할당하고 있다. 56 | 57 | ```javascript 58 | //View 59 | #container; 60 | 61 | constructor($element){ 62 | this.#container = $element; 63 | } 64 | ``` 65 | 66 | - 특정 영역을 show/hide 및 toggle은 classList를 활용해 변화를 담당했다. 67 | - View는 Model, Controller에 직접 접근하는 경우가 없다. 68 | 69 |
70 | 71 | #### Model 72 | 73 | - Model은 Lotto, LottosModel로 구성되어 있다. 74 | - Lotto 모델은 pickNumbers(선택된 로또번호들)을 멤버 변수로 가지고 있다. Set() 자료형을 사용했다. 75 | ```javascript 76 | #pickNumbers = new Set(); 77 | ``` 78 | - LottosModel은 lottos(로또 객체들)을 멤버 변수로 가지고 있다. 배열 [] 자료형을 사용했다. 79 | - Controller에서 Model의 상태 데이터에 접근하기 위해서 getter를 사용하고 있다. 80 | 81 |
82 | 83 | #### 인상 깊은 점 84 | 85 | - Array.from 활용 부분 86 | 87 | ```javascript 88 | const lottoCount = inputMoney / LOTTO_SETTING.PRICE; 89 | 90 | Array.from({ length: lottoCount }, () => { 91 | const lottoInstance = new Lotto(); 92 | lottoInstance.generateNumberList(); 93 | 94 | this.#lottos.push(lottoInstance); 95 | }); 96 | ``` 97 | 98 | Array.from의 length 속성을 이용하고, Array.from의 스펙을 잘 이용한 것 같다. 99 | 100 |
101 | 102 | - scss 사용 103 | 104 | ```css 105 | $border-color: #b4b4b4; 106 | $border-radius: 5px; 107 | $primary-color: #00bcd4; 108 | ``` 109 | 110 | 자주 쓰이는 속성값을 변수에 담아서 활용하였다. 111 | 112 |
113 | 114 | ### [#103, #111, #115] 하리, 록바, 코카콜라 115 | 116 | 파일 구조도는 MVC 패턴을 따르고 있다. 117 | 118 | - Model - LottoNumbers, PurchasedLottos 119 | - View - View, PurchasedMoneyView, PurchasedLottoView, WinningNumberView 120 | - Controller - LottoMachineController 121 | 122 | #### Controller 123 | 124 | - Controller는 Model, View에게 지시하는 역할을 수행하고 있다. 125 | - constructor에서 Model, View 인스턴스를 생성 후, 멤버 변수로 가지고 있다. 126 | - **dom을 View에서 관리하고 있기 때문에, Controller에서 실행되어야 하는 함수를 콜백함수로 전달하는 것이 아닌, 부모 클래스인 View의 handlers(멤버 변수)에 저장한다.** 127 | 128 |
129 | 130 | #### View 131 | 132 | - View는 PurchasedMoneyView, PurchasedLottoView, WinningNumberView, View로 구성되어 있다. 133 | 134 | - View(부모 클래스)는 View에서 실행해야하는 Controller의 handler 함수들을 관리하고 있다. 135 | 136 | ```javascript 137 | export default class View { 138 | handlers = new Map(); 139 | 140 | addHandler({ name, handler }) { 141 | const data = this.handlers.get(name) || []; 142 | data.push(handler); 143 | this.handlers.set(name, data); 144 | } 145 | } 146 | ``` 147 | 148 | - View(부모 클래스)를 상속하고 있는 PurchasedMoneyView 클래스는 View(부모 클래스)에 handler 멤버 변수에 접근하여, Controller에 있는 함수를 호출한다. 149 | 150 | ```javascript 151 | //PurchasedMoneyView 152 | submitHandler(e) { 153 | e.preventDefault(); 154 | 155 | const purchaseMoney = convertToNumber(this.input.value); 156 | 157 | try { 158 | validatePurchaseMoney(purchaseMoney); 159 | this.handlers 160 | .get('purchasedMoneySubmit') 161 | .forEach(func => func(purchaseMoney)); 162 | } catch (error) { 163 | this.resetInputValue(); 164 | alert(error); 165 | } 166 | } 167 | ``` 168 | 169 | - View는 Model, Controller에 직접 접근하는 경우가 없다. 170 | 171 |
172 | 173 | #### Model 174 | 175 | - Model은 LottoNumbers, PurchasedLottos로 구성되어 있다. 176 | - 각 Model의 상태 데이터는 #을 이용해 private field에 선언하였다. 177 | - 외부에서 Model 상태 데이터를 접근하는 방식을 getter/setter 방식을 사용하였다. 178 | 179 |
180 | 181 | #### 인상 깊은 점 182 | 183 | - 로또 번호를 6개를 생성하는 방식 184 | 185 | - 1 ~ 45 배열을 생성한다. 186 | - 해당 배열을 shuffle하여 뒤섞는다. 187 | - 배열에서 6개의 번호를 추첨한다. 188 | 189 | ```javascript 190 | export const pickLottoNumber = (count) => { 191 | //로또번호 1 ~ 45를 소유하고 있는 배열 192 | const lottoNumbers = [...Array(RULES.MAX_LOTTO_NUMBER)].map( 193 | (_, index) => index + 1 194 | ); 195 | shuffleArray(lottoNumbers); 196 | 197 | const numbers = []; 198 | 199 | for (let i = 0; i < count; i++) { 200 | numbers.push(lottoNumbers.pop()); 201 | } 202 | 203 | return numbers; 204 | }; 205 | 206 | //immutable 적용하기ㅠㅠ 207 | function shuffleArray(inputArray) { 208 | inputArray.sort(() => Math.random() - 0.5); 209 | } 210 | ``` 211 | 212 | - [리뷰어 답변] 효율/성능을 따져야할 만큼 심각한 성능저하가 따르는 경우가 아닌 한은 읽기좋은 코드 / 간단한 코드를 작성하는 다양한 방법을 생각해 보시면 좋겠다. 213 | 214 |
215 | 216 | ### [#104, #108] 민초, 돔하디 217 | 218 | 파일 구조도는 MVC 패턴을 따르고 있다. 219 | 220 | - Model - Lotto 221 | - View - lottoView 222 | - Controller - LottoController 223 | 224 | #### Controller 225 | 226 | - Controller는 LottoController로 구성되어 있다. 227 | - constructor에서 lottos(로또번호를 포함하고 있는 멤버들로 구성된)멤버변수를 가지며, 이벤트리스너를 등록하고 있다. 228 | 229 |
230 | 231 | #### View 232 | 233 | - View는 lottoView로 구성되어 있다. 234 | - 클래스가 아닌, 함수표현식으로 구성되어 있다. 235 | ```javascript 236 | export const showResult = (lottos) => { 237 | deactivateForm(); 238 | showResultElements(); 239 | showNumberOfLottos(lottos.length); 240 | showLottoImage(lottos); 241 | }; 242 | ``` 243 | - showResult함수에서, view에 관련된 함수를 호출하는 형태로 구성되어있다. 244 | 245 | #### Model 246 | 247 | - Model은 Lotto로 구성되어 있다. 248 | - constructor에는 lottoNumbers(로또 번호들) 멤버변수와 로또 번호를 생성하는 함수를 호출하고 있다. 249 | 250 |
251 | 252 | ### [#114] 안 253 | 254 | 파일 구조도는 MVC 패턴을 따르고 있다. 255 | 256 | - Model - Lotto 257 | - View - LottoGameView 258 | - Controller - LottoGame 259 | 260 | #### Controller 261 | 262 | - Controller는 LottoGame로 구성되어 있다. 263 | - constructor에는 Model과 View 인스턴스를 생성한 후, 멤버변수로 할당하고 있다. 264 | - constructor에서 이벤트리스너를 등록하고 있다. 265 | 266 |
267 | 268 | #### View 269 | 270 | - View는 LottoGameView로 구성되어 있다. 271 | - View에서 토글 이벤트리스너를 등록하고 있다. 272 | 273 |
274 | 275 | #### Model 276 | 277 | - Model은 Lotto로 구성되어 있다. 278 | - constructor에는 lottos(로또번호를 포함하고 있는 멤버들로 구성된)멤버변수를 가지고 있다. 279 | 280 |
281 | 282 | #### 인상깊은 부분 283 | 284 | ```javascript 285 | resetLottoList() { 286 | this.lottoNumberList.replaceChildren(""); 287 | } 288 | ``` 289 | 290 | - MDN에 따르면, Element.replaceChildren()메서드는 기존 자식을 지정된 새 자식 집합으로 바꿉니다. 이것들은 DOMString 또는 Node객체가 될 수 있다. 291 | 292 |
293 | 294 | ### [#88, #91] 블링, 태태 295 | 296 | 파일 구조도는 MVC 패턴을 따르고 있다. 297 | 298 | - Model - lotto, lottoManager 299 | - View - lottoView 300 | - Controller - lottoController 301 | 302 | #### Controller 303 | 304 | - Controller는 lottoController로 구성되어 있다. 305 | - constructor에는 Model과 View 인스턴스를 생성한 후, 멤버변수로 할당하고 있다. 306 | - View에서 dom을 관리하고 있기 때문에, 이벤트 동작과정에서 Controller에 필요한 부분을 callback으로 View에게 전달하고 있다. 307 | - 외부에 드러낼 필요없는 부분은 #을 이용해 은닉화를 진행하였다. 308 | 309 |
310 | 311 | #### View 312 | 313 | - View는 lottoView로 구성되어 있다. 314 | - constructor에는 화면에 연관되어 있는 element를 멤버변수로 할당하고 있다. 315 | - 로또 element는 템플릿을 사용하지 않고, tag와 className을 이용해 element를 생성하고 text를 넣는 식으로 구성하였다. 316 | - 외부로 드러낼 필요없는 부분은 #을 이용해 은닉화를 진행하였다. 317 | 318 |
319 | 320 | #### Model 321 | 322 | - Model은 lotto, lottoManager로 구성되어 있다. 323 | - Lotto클래스는 lottoNumberSet(로또 번호들)을 멤버변수로 가지고 있으며, Set 자료형을 사용하고 있다. Set 객체를 생성할 때, 로또 번호를 랜덤하게 가져오는 함수를 호출하여 리턴 값을 매개변수로 전달하고 있다. 324 | ```javascript 325 | class Lotto { 326 | constructor() { 327 | this.lottoNumberSet = new Set( 328 | generateRandomNumberInRange({ 329 | min: LOTTO_NUMBER_RANGE.MIN, 330 | max: LOTTO_NUMBER_RANGE.MAX, 331 | count: LOTTO_NUMBER_COUNT, 332 | }) 333 | ); 334 | } 335 | } 336 | ``` 337 | - LottoManager클래스는 lottoPrice(로또 가격), purchaseAmount(구매한 로또 갯수), lottos(로또 번호들로 이루어진 배열)을 멤버변수로 초기화하고 있다. 338 | 339 |
340 | 341 | #### 인상깊은 부분 342 | 343 | ```javascript 344 | #toggleLottoNumbersShow = ({ target: { checked: isVisible } }) => { 345 | const { classList: lottoGridClassList } = this.lottoGrid; 346 | if (isVisible) { 347 | lottoGridClassList.add(CLASSNAMES.ONE_COLUMN_GRID_CLASSNAME); 348 | lottoGridClassList.remove(CLASSNAMES.HIDE_NUMBERS_CLASSNAME); 349 | return; 350 | } 351 | lottoGridClassList.remove(CLASSNAMES.ONE_COLUMN_GRID_CLASSNAME); 352 | lottoGridClassList.add(CLASSNAMES.HIDE_NUMBERS_CLASSNAME); 353 | }; 354 | ``` 355 | 356 | - 구조 분해(destructuring)가 인상깊다. 357 | 358 |
359 | 360 | ```javascript 361 | #generateLottoElement(lotto) { 362 | const lottoElement = createElementWithClassName('div', CLASSNAMES.LOTTO_CLASSNAME); 363 | 364 | const lottoImage = createElementWithClassName('p', CLASSNAMES.LOTTO_IMAGE_CLASSNAME); 365 | lottoImage.textContent = LOTTO_IMAGE; 366 | 367 | const lottoNumbers = createElementWithClassName('p', CLASSNAMES.LOTTO_NUMBERS_CLASSNAME); 368 | lottoNumbers.textContent = Array.from(lotto.lottoNumberSet).join(', '); 369 | 370 | lottoElement.append(lottoImage, lottoNumbers); 371 | return lottoElement; 372 | } 373 | ``` 374 | 375 | - innerHTML, insertAdjacentHTML을 피하고, append을 쓴게 인상깊다. 376 | 377 |
378 | 379 | --- 380 | 381 | ## 피드백 정리 382 | 383 | ### 대분류(ex: 아키텍처, 함수/클래스, 컨벤션, DOM, 테스트 등) 384 | 385 |
386 | 387 | #### 아키텍처 388 | 389 | - [#103] Controller와 View의 의존성을 끊는 방법 390 | - Controller와 View의 의존성을 끊기 위해서는 다양한 방법이 있는데, 웹에서는 흔히 customEvent를 활용합니다. View에서 이벤트핸들러를 등록하고, 그 핸들러 내부에서 위의 customEvent를 dispatch합니다. Controller에서는 여러가지 customEvent를 수신해서 동작을 수행합니다. 391 | - window객체에 메서드(dispatch)를 만들어두는 방법으로 우회하는 방법도 있습니다. View에서 등록한 핸들러 내부에서 적정한 타입/값을 담은 dispatch를 호출합니다. dispatch함수는 인자를 받아 controller들에게 알립니다(publish). 해당 타입을 subscribe하고 있는 controller가 이를 처리합니다. 392 | 393 |
394 | 395 | - [#114] 이벤트등록은 View에서 해야할까? Controller에서 해야할까? 396 | - 셀렉터라는게 view에서 element를 선택하자는 거니까 view에 의존적인게 당연하죠. 당연히 view에 의존적일 수밖에 없는 내용이라면 view에 몰아넣는게 합리적이지 않나요? 모든 크루들이 view와 controller의 의존관계를 끊어내려 하기보다 오히려 controller가 적극적으로 view에 개입하고 있는데, 그 이유는 제가 추측하기로는 크루분들 사이에 eventListener를 controller에서 등록해야 한다는 강박 같은게 자리잡은게 아닌가 싶거든요.. 왜 그런 컨센서스가 생긴건지 모르겠는데, **이벤트등록은 뷰에서 하는게 맞습니다.** 이벤트 발생시 컨트롤러에서의 동작은 별도의 함수를 마련, 호출함으로써 해결하면 되구요. 397 | 398 |
399 | 400 | #### 함수/클래스 401 | 402 | - [#93] 토글 조작으로 인한 화면 show/hide 관련 함수 403 | 404 | ```javascript 405 | showLottoList() { 406 | this.#container.classList.add('show'); 407 | } 408 | 409 | hideLottoList() { 410 | this.#container.classList.remove('show'); 411 | } 412 | ``` 413 | 414 | - toggle 이라는거는 show -> hide, hide -> show 를 번갈아가면서 하겠다는 것을 내포하고 있는데 show 가 왜 붙어야하는지 모르겠습니다! 415 | - showLottoList, hideLottoList -> toggleLottoList로 정정 416 | 417 |
418 | 419 | - [#103] 불필요한 초기화 메서드를 만들지 마라. 420 | - initDom(), initEventListener() 등 불필요한 메서드를 만들지 말고, constructor에서 초기화를 진행해라. 421 | 422 |
423 | 424 | - [#103] getter/setter의 장점 425 | - IDE 환경에 따라 다르겠지만, `.`만 찍어도 접근자들을 확인할 수 있다. 426 | - 내부 로직을 은닉화하고, 외부에는 필요한 접근자들만 제공한다. 427 | - Object.assign, destructuring assignment등에 의해서도 노출되지 않는다. 428 | 429 |
430 | 431 | - [#103] 이벤트 리스너 내부 익명함수 -> 핸들러 분리 432 | - 이벤트 리스너는 원래 `등록`과 `해제`를 함께 고려해야 한다. 433 | - 해제해주지 않으면, 메모리가 낭비될 뿐만 아니라 경우에 따라서 중복 등록되어 이벤트에 대해 함수가 여러번 호출될 수 있다. 434 | - 익명함수를 사용할 경우 해제를 할 수 없기때문에 핸들러로 분리해야 한다. 435 | 436 |
437 | 438 | - [#115] #을 이용한 은닉화의 범위 439 | - 객체지향 관점에서 외부로 노출될 메소드들만 public으로 지정해놓는게 좋고, 440 | 노출되지 않을 메소드는 private으로 만드는게 좋습니다! 441 | 442 |
443 | 444 | - [#91] static 메서드의 기준은 무엇일까? 445 | - class는 기본적으로 인스턴스를 생성하는 틀입니다. 인스턴스 내부 '메소드'의 정의는 인스턴스 '자신'의 내부 동작을 수행하는 함수입니다. '자신'에 관여하는 내용이 없다면 이는 '메서드'가 아닌 '함수'로 불러야 하겠죠. 그런 맥락에서 보면, **this를 사용하지 않는 메서드는 인스턴스에서 접근해야할 필요가 없습니다. 인스턴스에 제공될 필요가 없으니 굳이 상속시키지 않게끔 static으로 분리하라는 것이죠.** 그런데 전부 static 메서드만 있는 클래스의 경우, 이를 바탕으로 인스턴스로 만들어봐야 그 인스턴스는 빈껍데기만 있는 객체에 불과합니다. 실제로 인스턴스로 만드는 시도조차 안하실거구요. 그렇다면 해당 클래스는 클래스로서가 아니라 일반 객체로서의 의미밖에 없는 것이니, static으로만 구성된 클래스는 차라리 처음부터 객체로 만드는 편이 좋다고 생각합니다. 446 | 447 |
448 | 449 | ### 컨벤션 450 | 451 | - [#93] 혹시 class 네이밍 컨벤션 관련 검색 키워드 추천 해주실 만한게 있을까요? 452 | 453 | - (정답은 아니고, 제안) label과 input을 감싸는 애들은 **wrapper**로, 여러 컴포넌트를 감싸는 애들은 **container**로 한다. 454 | 455 |
456 | 457 | - [#93] 함수 매개변수명 관련 코멘트 458 | ```javascript 459 | handleMoneyInputSubmit({ moneyInput }); 460 | ``` 461 | - moneyInput -> inputElment가 넘어오는 걸로 착각을 일으킨다. 462 | - moneyInput -> money 수정 463 | 464 |
465 | 466 | - [#93] 배열을 할당받고 있는 변수명 네이밍 관련 코멘트 467 | - 배열은 보통 ~~s 또는 ~~List로 네이밍을 짓는다. 468 | 469 |
470 | 471 | - [#93] 변수명 관련 코멘트 472 | - 현재, 두 가지 View의 멤버변수는 `this.#container = $element`로 되어있다. 473 | - [리뷰어님 답변] : container 같이 element 에 대한 변수명을 지을 때 number, string 과 같은 값을 의미하는 변수와 구분을 두면 가독성이 좋아질 것 같습니다. 예를 들어 변수명 뒤에 ~~~Element 를 붙인다던지, element 변수명 앞에는 $ 를 붙인다던지 등등 규칙을 정하자! 474 | 475 |
476 | 477 | - [#98] [리뷰어 질문] : selector 도 모두 상수화하신 게 인상깊네요. 개인적으로는 대문자 상수를 많이 쓰게 되면 가독성이 오히려 떨어지는 느낌인데, 쓰면서 어떠셨나요~? 478 | - 콤피의 답변 479 | - 가독성은 좋지는 않다. 480 | - 추후 선택자를 수정사항이 생기면, 한 번에 변경이 가능하다. 481 | - vscode의 자동 표기법이 편하다. 482 | 483 |
484 | 485 | - [#98] 아래 코드에서는 Object.freeze를 할 필요없을까? 486 | ```javascript 487 | export const LOTTO_SETTING = { 488 | MIN_RANDOM_NUMBER: 1, 489 | MAX_RANDOM_NUMBER: 45, 490 | PRICE: 1000, 491 | LOTTO_NUMBER_LENGTH: 6, 492 | }; 493 | ``` 494 | - 객체는 변경가능해지기도 해서 상수는 보통 문자열로 선언해 사용한다. 495 | - 변수명 자체를 대문자로 쓰는 편이라, Object.freeze를 하지 않는다. 496 | 497 |
498 | 499 | - [#103] Object.freeze가 인상적인데, "굳이"라는 생각이 든다 500 | 501 | - 이미 '상수'임을 모두가 아는 상태에서, 객체에 접근하는 경우가 발생한다면 건든 사람이 책임을 져야하는 일이다. 오버스펙이라 생각한다. 502 | 503 |
504 | 505 | - [#111] css 파일명 원칙 506 | - 보통 css 파일명으로 카멜케이스를 사용하지 않는다. 507 | 508 |
509 | 510 | - [#104] && 연산자 관련 코멘트 511 | ```javascript 512 | const isValidMoneyInput = (money) => 513 | isThousandMultiple(money) && 514 | isOverThouand(money) && 515 | isUnderMillion(money) && 516 | isValidMoneyRange(money); 517 | ``` 518 | - && 연산자를 사용할 때, 값에 대한 평가로만 사용하고 함수 호출을 지양하는 편이다. 519 | 520 |
521 | 522 | - [#104] 네이밍에 전치사? 523 | - 클린코드 원칙에는 전치사 사용을 자제하는것을 추천한다. 524 | 525 |
526 | 527 | - [#91] 상수는 굳이 멤버로 할당할 필요가 없다. 528 | ```javascript 529 | this.lottoPrice = LOTTO_PRICE; 530 | ``` 531 | 532 |
533 | 534 | -[#88] model에 직접 접근은 지양해라. 535 | 536 | ```javascript 537 | //직접 접근 538 | this.view.renderLottos(this.lottoManager.lottos); 539 | 540 | //간접 접근 541 | const lottos = this.lottoManager.getLottos(); 542 | this.view.renderLottos(lottos); 543 | ``` 544 | 545 |
546 | 547 | ### 패키지 548 | 549 | - [#115] ^(캐럿)을 붙인 이유는 무엇인가요? 550 | ```javascript 551 | "devDependencies": { 552 | "@babel/core": "^7.16.12", 553 | "@babel/preset-env": "^7.16.11", 554 | "babel-jest": "^27.4.6", 555 | "babel-loader": "^8.2.3", 556 | "clean-webpack-plugin": "^4.0.0", 557 | "html-webpack-plugin": "^5.5.0", 558 | "css-loader": "^6.6.0", 559 | "eslint": "^8.9.0", 560 | ``` 561 | - npm 측에서의 버전 간의 호환성을 위하여 도입. 562 | 563 |
564 | 565 | ### CSS 566 | 567 | - [#115] 공통적으로 쓰이는 색상은 global css variable로 만들자. 568 | - 추후 개발을 진행하거나 했을때 일관적으로 색상 팔레트가 관리되고 있지 않다면, 찾아다니는게 대규모 프로젝트에선 생각외로 힘들다. 569 | 570 |
571 | 572 | ### 새로운 접근 573 | 574 | - [#114] 꼭, constructor에서 element에 접근해서 멤버변수로 해야할까? 575 | - HTML상에 위 element의 생명주기가 JS의 생명주기보다 먼저 시작해서 끝까지 살아있다는 전제하에서는, 위 값을 상수 또는 변수로 만들어두면 안될까요? 576 | ```javascript 577 | const ELEMENTS = { 578 | ... 579 | SWITCH_INPUT: $(".switch-input") 580 | } 581 | ``` 582 | -------------------------------------------------------------------------------- /lotto/step1/marco.md: -------------------------------------------------------------------------------- 1 | # Level1 Lotto Step1(담당 PR 번호들) - 마르코 2 | 3 | - 분석 담당 코드 4 | - 준찌 [#81](https://github.com/woowacourse/javascript-lotto/pull/81) 5 | - 유세지 [#82](https://github.com/woowacourse/javascript-lotto/pull/82) 6 | - 동키콩 [#92](https://github.com/woowacourse/javascript-lotto/pull/92) 7 | - 온스타 [#85](https://github.com/woowacourse/javascript-lotto/pull/85) 8 | - 위니 [#96](https://github.com/woowacourse/javascript-lotto/pull/96) 9 | - 자스민 [#86](https://github.com/woowacourse/javascript-lotto/pull/86) 10 | - 아놀드 [#84](https://github.com/woowacourse/javascript-lotto/pull/84) 11 | - 빅터 [#106](https://github.com/woowacourse/javascript-lotto/pull/106) 12 | - 코이 [#105](https://github.com/woowacourse/javascript-lotto/pull/105) 13 | 14 | ## 1. 코드 분석 15 | 16 | ### [#81] 준찌, [#82] 유세지 17 | 18 | ![준찌유세지](img/schema82.png) 19 | 20 | #### Controller 21 | 22 | - 진입점인 index.js가 바로 부르는 RacingGameManager() 클래스에서 .start() 메서드로 시작한다. 해당 클래스는 컨트롤러 역할인 듯하다. 23 | 24 | - start 메서드는 `다른 클래스 인스턴스 생성`, `dom요소 가져오기`, `event 바인딩`을 init한다. 25 | 26 | - (1) model과 view 클래스 인스턴스 생성하여 멤버변수로 할당 27 | - (2) DOM 요소 가져와서 멤버변수 할당 28 | 29 | - constructor가 없다. 메서드 내부에서 멤버 변수를 만들고 있다. 30 | 31 | - (3) 선택된 DOM 요소에 이벤트 바인딩 32 | - [네이밍] 핸들러 네이밍을 할 때, 접두사로 on을 붙인다. `onSubmitChargeInputForm` 33 | 34 | - controller는 이벤트를 통해 발생한 데이터를 가공하고, view의 렌더링 메서드를 호출하며 그 데이터를 매개변수로 전달한다. 35 | 36 | #### View 37 | 38 | - constructor 내부에 dom 요소를 멤버변수 할당하지 않고, 프라이빗 메서드 내부에서 dom 요소를 멤버변수로 할당하여 사용하고 있다. 39 | - render하는 메서드들은 매개변수로 렌더링할 데이터를 (컨트롤러로부터) 받고 있다. 40 | - view가 model에 의존하고 있지 않다. 41 | 42 | #### model 43 | 44 | - model에는 `LottoGameModel`과 `LottoModel` 2개가 있다. 45 | - 그런데 두 모델의 네이밍이 와닿지 않는다는 리뷰어의 의견이 있었다. ["Lotto 클래스는 어떤 정보를 담는 녀석인지, LottoGame은 또 어떤 녀석인지, 이름만 봤을 때 대략적으로라도 유추할 수 있는 네이밍을 고민해보시면 좋겠어요."] 46 | - controller에서 의존하고 있는 model인 LottoGameModel은 lottoList(2차원 배열)만 멤버변수로 관리하고 있다. 47 | - LottoGameModel이 의존하고 있는 LottoModel은 lottoNumbers(1차원 배열, 길이 6)만 멤버변수로 관리하고 있다. 48 | 49 | #### 클로저 활용 50 | 51 | - 6개 랜덤 숫자가 담긴 lotto 배열 만들 때, closure 사용 52 | 53 | ```jsx 54 | function shuffle(array) { 55 | const copy = [...array]; // 값을 받을 때 깊은복사 56 | 57 | copy.sort(() => Math.random() - 0.5); // random은 0과 1사이 랜덤값이고 sort메서드의 compareFunction은 음수냐 양수냐를 기준으로 두 값씩 비교하며 정렬하므로, 값들이 한 쪽으로 편중되지 않도록 random 값에 0.5를 빼서 음수 또는 양수가 랜덤하게 나오도록 compareFunction을 만들어서 사용한다. 58 | 59 | return [...copy]; // 값을 반환할 때도 깊은복사하여 반환 60 | } 61 | 62 | function lottoNumberClosure() { 63 | const pickNumbers = shuffle([...new Array(45)].map((_, idx) => idx + 1)); // [1,2,3,4,...,45] -> 섞어서 64 | 65 | return () => pickNumbers.pop(); // 배열에서 마지막 값을 하나씩 반환하고 배열에서 제거 66 | } 67 | 68 | class Lotto { 69 | constructor() { 70 | this.lottoNumbers = this.createLottoNumbers(); 71 | } 72 | createLottoNumbers() { 73 | const getLottoNumber = lottoNumberClosure(); 74 | // getLottoNumber()는 1부터 45 사이의 값 중 하나를 반환하고, 한 번 반환된 값은 다시 반환되지 않는다. 75 | return [...new Array(6)].map(() => getLottoNumber()); 76 | } 77 | } 78 | const lotto = new Lotto(); 79 | console.log(lotto); // Lotto { lottoNumbers: [ 7, 14, 45, 33, 18, 36 ] } 80 | ``` 81 | 82 | ### [#92] 동키콩, [#85] 온스타 83 | 84 | ![schema](img/schema92.png) 85 | 86 | - MVC에서 벗어나, 뷰로직과 비즈니스 로직을 분리하는데에만 집중하였다고 한다. `뷰에서 입력을 받고` `비즈니스 로직을 통해 데이터를 가공하여` `다시 뷰에서 렌더링`하는 방식으로 구현을 하였다. 87 | 88 | - 리뷰어는 이에 대해 `View 영역이 너무 방대해진 것` 같다는 의견을 주기도 했다. 이를 해결하기 위해, `도메인과 UI를 연결할 수 있는 Controller와 같은 영역의 추가`를 제안했다. 89 | - "꼭 특정 패턴을 맹목적으로 따르고 일부러 구조를 맞출 필요는 없지만 현재 고민하시는 내용에 MVC 패턴이 아이디어를 제공할 수 있을 것 같습니다" 90 | 91 | - DOM이 로드되면, 처음 발생하는 인스턴스는 LottoView 클래스의 인스턴스이다. LottoView의 constructor에서 인스턴스 발생시 , LottoMachine(모델, 멤버변수 #inputMoney / #lottos / #strategy) 클래스의 인스턴스를 만들고, 이벤트를 바인딩하였다. 92 | - LottoMachine 클래스에 있는 generateLottos() 메서드에서 Lotto(모델) 클래스의 인스턴스를 찍어내여 구입한 개수의 Lotto들의 2차원 배열을 만든다. 즉, LottoMachine 클래스는 받은 돈으로 로또들을 Lotto 클래스(로또 한 장을 책임)를 통해 만드는 책임을 지녔다. 93 | ```jsx 94 | generateLottos() { 95 | return Array(this.lottoQuantity) 96 | .fill() 97 | .map(() => new Lotto(this.#strategy).generate()); 98 | } 99 | ``` 100 | 101 | ### [#96] 위니 , [#86] 자스민 102 | 103 | - MVC 패턴을 사용했다. 104 | - Controller는 model과 view(resultView, inputView)에 의존한다. 105 | - 또한, Controller가 DOM을 조작하고 있고 이벤트 바인딩까지 해주고 있다. 그리고 이벤트의 콜백함수로서 비즈니스 로직 메서드들을 두고 있다. 106 | - > DOM 조작은 View의 역할로 보고 나눠주는 것이 어떨까? DOM 조작과 이벤트 바인딩은 View에서 하도록 분리할 수도 있을 것 같다. 107 | - view는 담당하는 화면 섹션에 따라 resultView, inputView로 분리해준 것 같다. 관리하는 DOM 멤버 변수는 렌더링을 받을 요소들로만 지정하고 있고, 메서드들 또한 어디에 어떤 HTMLString을 주입할 지 시키는 대로 하는 역할을 수행하고 있다. View의 역할 분리가 잘 된 것 같다. 108 | 109 | ### [#84] 아놀드, [#106] 빅터 110 | 111 | ![schema](img/schema84.png) 112 | 113 | - 클래스를 사용하지 않고 함수와 객체만을 이용하여 프로그래밍을 하였다. 또한, MVC처럼 설계하지 않고 도메인 중심으로 짠 것 같다. 114 | - 코드 진입하면, app.js에서 DOM 요소(금액 입력 폼, 체크박스) 를 선택하여 각각의 이벤트를 바인딩한다. 115 | - 이벤트의 콜백함수는 eventListener.js에 모아두었고, 이를 import해서 사용한다. eventListener.js에 있는 메서드는 비즈니스 로직을 담당하는 것 같다. 메서드 내부에서 validator 모듈에게 유효성 검사 요청 메시지 전달, lottoGame 모듈에게 데이터 생성 요청 메시지 전달, view 모듈에게 렌더링 요청 메시지 전달 등을 하고 있다. 116 | - lottoGame 모듈은 model 역할에 가까운 것 같고, view 모듈은 이름대로 view역할을 하고 있다. 기존 MVC에서 controller와 비슷한 역할(model과 view를 잇는)은 eventListener.js 모듈이 일부 담당하는 것 같다. 왜냐하면 eventListener.js는 lottoGame.js와 view.js에 의존하고 있으며, view에게 렌더링 요청 시 lottoGame에서 가져온 데이터를 인자로 넘겨주기 때문이다. 117 | - lottoGame 모듈은 모델의 역할을 하지만, 클래스 없이 전역변수와 함수로만 이루어져 있다. 그러면 싱글톤 객체와 같은 역할을 하게 되는 것 같다. 118 | - 이 때 만약 "여러 라운드의 로또 생성 내역을 히스토리로 보여줘야 할 니즈"가 생길 경우, 어떻게 해결할 수 있을까? 119 | - 반면, 클래스를 사용했다면, 라운드마다 모델에 새 인스턴스를 생성하고, 히스토리를 보여줄 때 여러 인스턴스의 데이터를 보여주는 방식으로 가능할 것 같다. 120 | 121 | ### [#105] 코이 122 | 123 | - MVC를 사용하였다. 124 | - controller에서 dom 요소를 멤버변수로 두고 이벤트를 바인딩하고 있다. 그리고 콜백함수도 함께 있다. 125 | - element를 disabled해주는 유틸 함수를 따로 빼서 사용하고 있는데, 좋은 것 같다. 126 | 127 | ```jsx 128 | export const setDisabled = element => element.setAttribute("disabled", true); 129 | ``` 130 | 131 | - view에서 렌더링을 지울 때, replaceChildren()메서드를 활용하고 있다. 132 | 133 | ```jsx 134 | 135 | resetLottoList() { 136 | this.lottoNumberList.replaceChildren(""); 137 | } 138 | ``` 139 | 140 | ## 2. 피드백 정리 141 | 142 | ### 아키텍처 143 | 144 | - [#84] 누가 어떤 역할을 하고 있는지는 명확히 보이도록 설계하는 것이 좋다. 145 | 146 | - [#86] 외부에서 알 필요가 없는 메서드라면 private로 처리해주는게 맞다. 147 | 148 | - 예를들어 전기포트가 있다고 할 때 사용자는 전원을 키고 끄는법에 대해서만 알면 되고 내부에서 어떻게 전기포트에서 열이 올라서 물을 끓이는지 세부적인 내용에 대해 알 필요가 없는 부분과 같은 맥락이다. "중요한 데이터를 외부에서 건드릴 수 없도록 하고, 외부에서 사용하지 않는다"의 맥락을 모두 이 전기포트의 예시에 빗대어 생각해볼 수 있다. 149 | 150 | - [#86] MVC에서 event 바인딩하는 함수를 controller보다 view에 두는 것이 일반적이긴 하나, 상황에 따라 자신이 좋다고 판단하고 작성한 코드가 오히려 정답일 수 있다. 151 | 152 | - 일단, 위임을 할지 말지에 따라서도 다르게 가져갈 것 같고 앱의 규모에 따라 다르게 가져갈 여지도 있다. 참고로 view에서 event 바인딩하는 함수를 구현한다면 유지보수 측면에서 원하는 요소를 찾기 쉬울 것 같다는 장점이 있다. 153 | 154 | - [#81] Model의 getter 함수에서 깊은 복사를 하여 반환해주는 것은 상황에 따라 전략을 취하면 된다. 155 | - 깊은 복사를 하여 불변성을 통해 얻는 이점이 해당 코드에서 있다면 할 수도 있다. 156 | - 그리고 중첩된 객체의 경우 전체 depth를 모두 깊은복사해야 하는 경우도 있겠지만, 1depth까지만 깊은 복사를 해도 충분한 경우, 또는 얕은 복사로도 아무 문제 없는 경우도 많다. 157 | - 그리고 성능최적화가 고도로된 V8 엔진에서는 깊은 복사를 해도 발생하는 성능 손실은 생각보다 미미하므로, 필요시 깊은복사를 해도 되겠다. 158 | 159 | ```jsx 160 | // 예시 161 | // 유틸 162 | export const deepCopy = object => JSON.parse(JSON.stringify(object)); 163 | // 이것은 아마 1depth까지만 깊은 복사가 되는 코드? 164 | 165 | // 모델 166 | class LottoGameModel { 167 | constructor() { 168 | this.lottoList = null; 169 | } 170 | 171 | getLottoList() { 172 | /** getter로 가져간 lottoList를 변경하여도 lottoList의 멤버에겐 영향이 없다. */ 173 | return deepCopy(this.lottoList); 174 | } 175 | } 176 | ``` 177 | 178 | - [#92] 캡슐화가 잘됐다면 객체가 외부에 반드시 제공해야 되는 기능만 드러내고 나머지는 은닉화해야 한다. 179 | - 어떤 객체의 getter, setter가 특별한 이유없이 public 이라면 캡슐화를 온전히 적용했다고 보기 어렵다. 캡슐화가 잘됐다면 객체가 외부에 반드시 제공해야 되는 기능만 드러내고 나머지는 은닉화 해야한다. 180 | - [#92] 이벤트 바인딩 시점을 조절해야 하는 것이 아니라면, 이벤트 바인딩을 묶은 메서드를 생성자 안으로 옮기는 것이 낫다. 181 | - [#92] 여러 곳에서 범용적으로 쓰일 법한 함수는 밖으로 옮기는 것이 낫다. 182 | - [#92] 뷰로직과 비즈니스 로직을 분리한다. 비즈니스 로직(=도메인 로직)이란, 실제 서비스의 핵심 기능을 구현하기 위해 `데이터를 어떻게 가공하고 관리할 것인가` 전반에 걸친 로직을 포함한다. https://en.wikipedia.org/wiki/Business_logic 183 | - 비즈니스 로직은 데이터가 생성, 저장 및 변경될 수 있는 방법을 결정하는 것이다. 반면, 그 외의 로직은 데이터베이스를 관리하거나 사용자 인터페이스를 표시하거나 일반적으로 프로그램의 다양한 부분을 연결하는 하위 수준 세부 정보와 관련되어 있다. 184 | 185 | ### 함수/클래스 186 | 187 | - [#105] 특정 길이만큼 배열을 만들어서 값을 넣어주려면, `for문과 push 메서드` 대신에 `특정 길이의 빈 배열에 map 메서드`를 쓸 수도 있다. 188 | - 특정 length의 빈 배열을 만들고 싶을 때, `[...Array(6)]` 또는 `Array.from({ length: 6 })` 를 활용할 수 있다. 189 | 190 | ```jsx 191 | for (let i = 0; i < count; i += 1) { 192 | const lotto = new Lotto(); 193 | lotto.generateRandomNumber(); 194 | this.lottos.push(lotto); 195 | } 196 | ``` 197 | 198 | ```jsx 199 | this.lottos = Array.from({ length: count }).map(() => { 200 | const lotto = new Lotto(); 201 | lotto.generateRandomNumber(); 202 | return lotto; 203 | }); 204 | ``` 205 | 206 | - [#86] 중복된 값을 저장하지 않는 Set 자료구조 및 해당 메서드(add)등도 활용할 수 있다. 207 | 208 | ```jsx 209 | const lottoNumberSet = new Set(); 210 | while (lottoNumberSet.size < LOTTO_NUMBERS.LOTTO_LENGTH) { 211 | lottoNumberSet.add( 212 | getRandomNumber( 213 | LOTTO_NUMBERS.MIN_LOTTO_NUMBER, 214 | LOTTO_NUMBERS.MAX_LOTTO_NUMBER 215 | ) 216 | ); 217 | } 218 | return [...lottoNumberSet]; 219 | ``` 220 | 221 | - [#81] 고차함수를 추천하는 이유 중 하나는 가독성이 좋아지는 점이 가장 크다. 222 | - map 메서드로 리팩토링 사례 223 | 224 | ```jsx 225 | // before 226 | const lottoList = []; 227 | for ( 228 | let lottoCount = 0; 229 | lottoCount < availableLottoAmount; 230 | lottoCount = lottoCount + 1 231 | ) { 232 | const lottoNumbers = this.createLottoNumbers(); 233 | const lotto = Lotto.create(lottoNumbers); 234 | lottoList.push(lotto); 235 | } 236 | ``` 237 | 238 | ```jsx 239 | // after 240 | const lottoList = [...new Array(availableLottoAmount)].map(() => new Lotto()); 241 | ``` 242 | 243 | - [#81] Prototype에 커스텀 메소드를 추가해서 사용하는 것 주의 244 | - Prototype에 커스텀 메소드를 추가해서 사용하는 것은, 내장 객체가 가지는 전역 범위때문에 생길 수 있는 변경 가능성 때문에 별로 추천하는 방법이 아니다. 또한, 유틸성 함수의 역할로 쓰려는 목적이라면 명시적으로 유틸 함수를 정의하고 모듈화하는 것이 더 좋다. 245 | - built-in 객체의 prototype에 이미 있던 함수를 새로운 내용으로 변경하는 것은 당연히 위험한 일이니 지양하는게 맞는데, 246 | 개인적으로는 built-in 객체의 prototype에 새로운 내용을 추가하는 것 역시 권장하고 싶지 않습니다. 다른 모듈(파일)들을 작업하는 협업자 입장에서는 deepCopy라는 메서드가 있는지 여부를 알기 어려워서 안쓰게 될 확률이 높아요. 247 | 명시적으로 import해서 쓸 수 있도록 static한 함수로 구현하는 것이 더 좋다고 생각해요. 그렇게 하면 prototype 체이닝상의 존재여부와 무관하게 다른 객체들도 쓸 수 있을테니까요. (deepCopy는 배열 뿐 아니라 객체에서도 쓰기 좋은 함수입니다). 248 | 굳이 this 및 prototype 체이닝을 이용한 '메서드'로 써보고 싶다! 라고 하면, Array를 extends하는 새로운 subClass를 만들어서 그 안에 method를 정의하는 편이 좋습니다. 이러면 앞서 설명드린 문제점들은 모두 해소됩니다. 다만 이 경우엔 리터럴 선언 방식은 유효하지 않다는 점이 아쉽긴 한데, prototype 메서드를 활용하고자 하는 니즈와 이 아쉬움 사이에서 잘 저울질해서 선택하면 되겠죠. 249 | 250 | ```jsx 251 | Array.prototype.deepCopy = function () { 252 | return JSON.parse(JSON.stringify(this)); 253 | }; 254 | ``` 255 | 256 | - [#82] `new.target` 사용 257 | 258 | - [new.target](https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Operators/new.target) 259 | - new.target 속성(property)은 함수 또는 생성자가 new 연산자를 사용하여 호출됐는지를 감지할 수 있습니다. new 연산자로 인스턴스화된 생성자 및 함수에서, new.target은 생성자 또는 함수 참조를 반환합니다. 일반 함수 호출에서는, new.target은 undefined입니다. 260 | 261 | - [#82] 드모르간 법칙을 활용하여 every 메서드에서 some 메서드로 변경하면 더 효율적인 로직일 수 있다. 262 | - 조건을 '모두 충족시켜야만' true가 반환되는 every보다는, 이를 뒤집어서 '일부 조건이라도 충족시' true가 반환되는 some을 쓰면 좀더 효율적인 검색이 이뤄질 수 있다. (드모르간 법칙 이용) 263 | 264 | ```jsx 265 | function isValidNumber(lottoNumbers) { 266 | return lottoNumbers.every( 267 | number => 268 | Number.isInteger(number) && 269 | number >= NUMBER.LOTTO_MIN_NUMBER && 270 | number <= NUMBER.LOTTO_MAX_NUMBER 271 | ); 272 | } 273 | ``` 274 | 275 | ### 컨벤션/네이밍 276 | 277 | - [#96] `this.model` 같은 추상적이고 모호한 이름보다 사용하는 곳에서 바로 알 수 있도록 구체적인 이름으로 바꾸는 것이 좋다. 278 | 279 | - [#86] 안 쓰는 인자는 언더바(\_)를 사용하면 남들이 이해하기 쉽다. 280 | 281 | ```jsx 282 | (e, idx) => idx + 1, // before 283 | (_, idx) => idx + 1, // after 284 | ``` 285 | 286 | - [#81] utils, constants에서 파일을 많이 생성하여, 코드들을 분리하는 것도 명확하다는 장점이 있다. 287 | - [#82] 하나의 로또 티켓에 들어가는 번호가 6개라는 의미로, 6을 값으로 갖는 `LOTTO_NUMBER_LENGTH`라는 변수명에서 `length`는 '자릿수'를 의미하는 것으로 오해할 여지가 있으므로, `length`를 'amount'로 변경해보는 것도 고민해보자. 288 | 289 | ### 테스트 290 | 291 | - [#105] 단위테스트는 어떤 기능 하나에 대한 테스트라고 볼 수 있으므로, 각 파일별로 테스트 파일을 작성하여, 그 파일별로 테스트 코드를 응집하는 게 나을 수 있다. 292 | 293 | - [#92] private 메서드는 세부 기능이기 때문에 특별한 경우가 아니라면 테스트 작성하지 않는 편이다. 링크 참고 http://shoulditestprivatemethods.com 294 | 295 | - [#92] 랜덤테스트의 가장 큰 문제는 검증을 실패하거나 성공했을 때 왜 성공했는지? 왜 실패했는지? 동일한 상황 재현이 어렵다는 것이다. 296 | 297 | - 랜덤테스트를 진행한다면 여러 테스트 케이스의 히스토리를 캡쳐하고 그 기록을 관리하는 체계가 필요하다. 298 | 299 | - [#92] 랜덤테스트를 안하려면 숫자를 테스트할 때 수동으로 입력해줘야 한다. 300 | 301 | - mock, spyOn 등의 키워드로 검색하여 방법 강구 302 | - > Mock 객체란 모듈의 겉모양이 실제 모듈과 비슷하게 보이도록 만든 가짜 객체이다. 모듈이 필요로 하는 의존성이 테스트 케이스 작성을 어렵게 만드는데, 이 의존성을 단절하기 위해 Mock 객체가 사용된다. https://eminentstar.github.io/2017/07/24/about-mock-test.html 303 | - > mocking에는 스파이(spy)라는 개념이 있습니다. 현실이나 영화 속에서 스파이라는 직업은 “몰래” 정보를 캐내야 합니다. 테스트를 작성할 때도 이처럼, 어떤 객체에 속한 함수의 구현을 가짜로 대체하지 않고, 해당 함수의 호출 여부와 어떻게 호출되었는지만을 알아내야 할 때가 있습니다. 이럴 때, Jest에서 제공하는 jest.spyOn(object, methodName) 함수를 이용하면 됩니다. https://www.daleseo.com/jest-fn-spy-on/ 304 | 305 | ```jsx 306 | const calculator = { 307 | add: (a, b) => a + b, 308 | }; 309 | 310 | const spyFn = jest.spyOn(calculator, "add"); 311 | 312 | const result = calculator.add(2, 3); 313 | 314 | expect(spyFn).toBeCalledTimes(1); 315 | expect(spyFn).toBeCalledWith(2, 3); 316 | expect(result).toBe(5); 317 | ``` 318 | 319 | - [#81] 유틸 함수에 대한 테스트에 가까울 경우, 서비스 로직 테스트와 분리하는 것이 더 분명한 의미를 전달할 수 있다. 320 | 321 | ### 함수/메서드 분리 322 | 323 | - [#84] 코드를 한 줄로 짜는 것(반환값을 다시 함수의 인자로 넣고 그 반환값을 다시 함수의 인자로 넣는 상황)도 좋지만, 뒤에서부터 읽고 반환값을 추측해야해서 코드를 이해하는 데에는 힘든 점이 있다. 함수에서 반환된 값은 따로 변수에 할당하고, 그 변수를 다른 함수에게 인수로 넘겨주도록 분리하자. 324 | 325 | - [#86] 과도한 분리는 오히려 복잡성을 증가시킬 수 있다. 326 | 327 | - [#82] 함수/메서드를 분리해야 하는 목적은 `외부에서의 접근수단 제공` 및 `여러 곳에서 호출`을 기준으로 판단해야 한다. 단순히 기능이 다르다는 이유만으로 잘게 분리하면 오히려 혼란을 줄 수도 있다. 328 | - 아래 코드의 경우 지나치게 잘게 분리되었다고 볼 수 있다. renderLottoList, renderPurchasedMessage는 모두 renderLottoSection 한군데에서만 호출하고 있는, 분리의 목적이 불분명한 메서드들이다. 이런 메서드들은 협업시 다른 사람들로 하여금 혹시 다른데서도 호출하는 곳이 있나? 하는 혼란만 주게 될 수 있지 않는지 고민할 필요가 있다. 329 | 330 | ```jsx 331 | renderLottoSection(lottoList) { 332 | this.renderPurchasedMessage(lottoList.length); 333 | this.renderLottoList(lottoList); 334 | } 335 | 336 | renderLottoList(lottoList) { 337 | this.$lottoContainer.innerHTML = lottoList 338 | .map((lotto) => this.generateLottoTemplate(lotto)) 339 | .join(''); 340 | } 341 | 342 | renderPurchasedMessage(lottoAmount) { 343 | this.$purchasedMessage.innerText = `총 ${lottoAmount}개를 구매하였습니다.`; 344 | } 345 | ``` 346 | 347 | ```jsx 348 | // 이 또한 과도한 메서드 분리라고 볼 수 있다. 기능별로 분리를 해두었지만, 실제 호출은 start메서드 한군데에서만 이뤄지고 있기 때문이다. 349 | class LottoGameManager { 350 | #initializeGame() { 351 | this.lottoGameModel = new LottoGameModel(); 352 | this.lottoGameView = new LottoGameView(); 353 | } 354 | 355 | #initializeDOM() { 356 | this.$chargeForm = findElement(SELECTOR.CHARGE_INPUT_FORM); 357 | this.$chargeInput = findElement(SELECTOR.CHARGE_INPUT); 358 | this.$alignConverter = findElement(SELECTOR.ALIGN_CONVERTER); 359 | } 360 | 361 | #initializeHandler() { 362 | this.$chargeForm.addEventListener("submit", this.onSubmitChargeInputForm); 363 | this.$alignConverter.addEventListener("change", this.onChangeAlignState); 364 | } 365 | start() { 366 | this.#initializeGame(); 367 | this.#initializeDOM(); 368 | this.#initializeHandler(); 369 | } 370 | } 371 | ``` 372 | 373 | ### 예외처리 374 | 375 | - [#85] 무한대의 값을 입력할 수 있도록 두면, 일정 금액 이상이 넘어가게 되면 앱이 정지하게 되므로, 예외처리를 해야 한다. 376 | 377 | ### DOM 378 | 379 | - [#86] Element가 받는 value 중 Number Input의 경우 value 대신 `valueAsNumber` 프로퍼티를 사용할 수도 있다. 380 | 381 | ### CSS 382 | 383 | - [#86] 중복되는 CSS 컬러는 CSS 변수로 관리해보자. 384 | - https://developer.mozilla.org/ko/docs/Web/CSS/Using_CSS_custom_properties 385 | -------------------------------------------------------------------------------- /lotto/step1/movie.md: -------------------------------------------------------------------------------- 1 | # LEVEL 1 행운의 로또 STEP 1 - 무비😎 2 | 3 | ## 분석 담당 코드 4 | 5 | - 마르코 [PR 101](https://github.com/woowacourse/javascript-lotto/pull/101) X 무비 [PR 99](https://github.com/woowacourse/javascript-lotto/pull/99) 6 | - 밧드 [PR 102](https://github.com/woowacourse/javascript-lotto/pull/102) X 우디 [PR 109](https://github.com/woowacourse/javascript-lotto/pull/109) 7 | - 앨버 [PR 90](https://github.com/woowacourse/javascript-lotto/pull/90) X 결 [PR 87](https://github.com/woowacourse/javascript-lotto/pull/87) 8 | - 나인 [PR 95](https://github.com/woowacourse/javascript-lotto/pull/95) X 비녀 [PR 94](https://github.com/woowacourse/javascript-lotto/pull/94) 9 | - 샐리 [PR 100](https://github.com/woowacourse/javascript-lotto/pull/100) 10 | 11 |
12 |
13 | 14 | ## 아키텍처 분석 15 | 16 | ### 마르코 [PR 101](https://github.com/woowacourse/javascript-lotto/pull/101) X 무비 [PR 99](https://github.com/woowacourse/javascript-lotto/pull/99) 17 | 18 | - MVC 패턴 19 | - custom 이벤트를 subscribe하는 형식 20 | - `emit`으로 이벤트를 송신하고 `on`으로 이벤트를 수신한다. 21 | - controller가 직접적으로 dom 이벤트를 처리하는 방식이 아닌, view가 dom 이벤트를 controller에게 알리고, controller가 처리하는 방식 22 | 23 | ### 밧드 [PR 102](https://github.com/woowacourse/javascript-lotto/pull/102) X 우디 [PR 109](https://github.com/woowacourse/javascript-lotto/pull/109) 24 | 25 | - MVC 패턴 26 | - 큰 controller가 존재하고 그 아래에 sub controller가 존재하는 방식, sub controller는 생성자의 매개변수로 큰 controller의 인스턴스를 받는다. 27 | - PR 109에서는 [core](https://github.com/woowacourse/javascript-lotto/tree/greenblues1190/src/js/core)에 부모 controller, model등이 존재하고 자식들이 상속받는 방식으로 변경됨 28 | - view에 `addEventListener`를 등록하는 함수가 존재하고 그 함수를 controller에서 호출한다. controller에서는 로직을 처리하는 함수를 `callback`으로 전달한다. 29 | - `index.html`에서 template를 만들어 놓고 `show`, `hide`하는 방식이 아닌, 이벤트가 발생할 때마다 `innerHTML`을 통해 template를 교체하는 방식 30 | 31 | ### 앨버 [PR 90](https://github.com/woowacourse/javascript-lotto/pull/90) X 결 [PR 87](https://github.com/woowacourse/javascript-lotto/pull/87) 32 | 33 | - MVC 패턴 34 | - `index.js`에서 model, view, controller를 선언해주는 방식이다. 35 | - custom 이벤트를 subscribe하는 형식 (마르코, 무비와 같음) 36 | - controller와 model은 하나씩 존재하고, view는 여러개 37 | 38 | ### 나인 [PR 95](https://github.com/woowacourse/javascript-lotto/pull/95) X 비녀 [PR 94](https://github.com/woowacourse/javascript-lotto/pull/94) 39 | 40 | - MVC 패턴 41 | - controller에서 이벤트를 등록해주고, model과 view에 존재하는 함수를 불러와 실행하는 방식 42 | - controller와 view는 하나씩 존재하며, model은 두개 존재 (로또와 로또 더미들) 43 | - controller를 `app.js`에서 생성해주고 controller의 생성자 안에서 model과 view를 생성하는 구조 44 | 45 | ### 샐리 [PR 100](https://github.com/woowacourse/javascript-lotto/pull/100) 46 | 47 | - 드디어 MVC 패턴이 아니다! 48 | - `app.js`에서 이벤트를 등록해주는 방식 49 | - `App`에서 validator라던지, view를 render해주는 모듈을 import해서 이벤트를 처리 50 | - `App`에 lotto에 대한 데이터, 사용자가 입력한 금액 등을 저장 51 | - 로직들이 있는 모듈은 `core`폴더에 존재, 이 곳에 `Lotto` 클래스도 존재 52 | 53 |
54 |
55 | 56 | ## 피드백 정리 57 | 58 | 😛 링크를 누르면 해당 discussion으로 이동! 😛 59 | 60 |
61 | 62 | ### MVC 패턴 63 | 64 | - 컨트롤러에서 모델에게 전달받은 상태를 직접 뷰에게 전달하는 것이 적절하다. - [PR 102](https://github.com/woowacourse/javascript-lotto/pull/102#discussion_r815267289) 65 | - 컨트롤러가 여러개의 view를 가질 수 있으니 확장성을 고려하며 설계할 것 - [PR 109](https://github.com/woowacourse/javascript-lotto/pull/109#discussion_r813851182) 66 | - MVC 패턴 관련 공유 내용 - [PR 90](https://github.com/woowacourse/javascript-lotto/pull/90#issuecomment-1049971008), [PR 90 - 추가 코멘트](https://github.com/woowacourse/javascript-lotto/pull/90#issuecomment-1050580916) 67 | 68 | ``` 69 | Model은 Controller와 View에 의존하지 않아야 한다. 70 | → Model 내부에 Controller와 View에 관련된 코드가 있으면 안 된다. 71 | → 즉 모델 내부에서 컨트롤러와 뷰에 있는 것을 임포트하면 안 됨 72 | View는 Model에만 의존해야하고, Controller에 의존하면 안 됨. 73 | → View에는 Model의 코드만 있을 수 있고, Controller의 코드가 있으면 안 됨. * 74 | View가 Model로부터 데이터를 받을 때, 75 | 사용자마다 다르게 보여주어야 하는 데이터에 대해서만 Model에서 받야아 한다. 76 | → 공통으로 보여지는 부분은 View가 자체적으로 가지고 있어야하는 것. 77 | Controller는 Model과 View에 의존해도 된다. 78 | View가 Model로부터 데이터를 받을 때, 반드시 Controller에서 받아야 한다. ** 79 | → 데이터를 받을 때 Controller 코드 안에서만 받야 한다. 80 | 81 | + 리뷰어님의 추가 코멘트 82 | View에는 Model의 코드만 있을 수 있고, Controller의 코드가 있으면 안 됨. * 83 | -> Controller를 통해 Model의 데이터가 업데이트되기때문에 Controller의 코드가 있을 필요가 없다. 84 | View가 Model로부터 데이터를 받을 때, 반드시 Controller에서 받아야 한다. ** 85 | -> Controller가 Model을 업데이트하고 그 업데이트된 Model의 데이터를 받아야한다. 위 글에서 "반드시 Controller에서 받아야 한다."는 Controller의 액션의 통해서 받아야한다 라고 말씀드리는게 명확했던 것 같네요. 86 | ``` 87 | 88 | - (_구조에 대한 생각?_) 리뷰어님의 개인적인 취향은 controller에서 model과 view 인스턴스를 생성하고, 생성하고 `index.js`에서 controller를 생성하는 편 - [PR 90](https://github.com/woowacourse/javascript-lotto/pull/90#issuecomment-1049971008) 89 | - (_view와 controller중 어디에서 `addEventListener`를 해주는 게 맞을까요?_) 리뷰어님의 개인적인 취향은 DOM에 대한 이벤트 바인딩은 view에서 담당하고 그 이벤트에 대한 액션은 controller에서 하는게 좀 더 역할 분담이 명확해 보임 - [PR 90](https://github.com/woowacourse/javascript-lotto/pull/90#issuecomment-1049971008) 90 | 91 | ### OOP 92 | 93 | - (_js에서 클래스를 통해서 OOP를 했을 때 가지는 장점이 있을까요?_) class를 사용하면 접근성이 좋아지고, 쉽고 일반적이다. - [PR 94](https://github.com/woowacourse/javascript-lotto/pull/94#pullrequestreview-894384954) 94 | 95 | ### 상속 96 | 97 | - 부모 클래스가 자식 클래스의 도메인 명을 알고 있지 않도록 주의 - [PR 109](https://github.com/woowacourse/javascript-lotto/pull/109#discussion_r813851496) 98 | 99 | ### 상수화 100 | 101 | - 리뷰어님이 기준이 명확하다고 한 상수화 - [PR 99](https://github.com/woowacourse/javascript-lotto/pull/99#discussion_r814070274) 102 | 1. 재사용과 변경이 될 수 있는 값을 상수화한다. 103 | 2. 그 자체로 의미를 알아볼 수 있는 값이면 상수화하지 않아도 된다. 104 | - 테스트 코드에도 상수를 사용하여 로또 가격이 바뀔 것을 고려할 것 - [PR 99](https://github.com/woowacourse/javascript-lotto/pull/99#discussion_r814079068) 105 | - 커스텀 이벤트를 사용한 경우 커스텀 이벤트가 많아질 경우를 대비해 상수화 할 것 - [PR 99](https://github.com/woowacourse/javascript-lotto/pull/99#discussion_r814082393) 106 | 107 |
108 | 109 | ### 변수 110 | 111 | - (_메서드 안에 임시변수를 쓰는 것은 지양되어야 할까요?_) 리뷰어님의 개인적인 의견은 남이 유츄가 가능한지 아닌지로 임시변수를 선언할지 말지를 결정한다. - [PR 90](https://github.com/woowacourse/javascript-lotto/pull/90#issuecomment-1049971008) 112 | - 예외의 값은 저장하지 않아도 된다. - [PR 90](https://github.com/woowacourse/javascript-lotto/pull/90#discussion_r813976935) 113 | 114 |
115 | 116 | ### 네이밍 117 | 118 | - `Controller`와 같은 일반적인 이름을 사용한다면 라이브러리인지, 구현체인지 헷갈리기 쉽다. `LottoControlle`와 같이 구체적으로 작명해줄 것 - [PR 94](https://github.com/woowacourse/javascript-lotto/pull/94#discussion_r815283358) 119 | - 함수의 네이밍은 동사/명령형 - [PR 100](https://github.com/woowacourse/javascript-lotto/pull/100#discussion_r814729214) 120 | 121 |
122 | 123 | ### 고차함수 124 | 125 | - 반복이면 바로 `forEach`를 쓸 수 있지만, 만약 반환을 해줘야 하는 경우라면 `map`을 고려해볼 것 - [PR 99](https://github.com/woowacourse/javascript-lotto/pull/99#discussion_r814069099) 126 | - 리뷰어님이 괜찮다고 해주신 커스텀 반복함수 - [PR 99](https://github.com/woowacourse/javascript-lotto/pull/99#discussion_r814030886) 127 | 128 | ```js 129 | const repeatCallback = (count, callback) => { 130 | for (let i = 0; i < count; i += 1) { 131 | callback(); 132 | } 133 | }; 134 | ``` 135 | 136 | - 템플릿을 만들 때 중복이 된다면 `map`을 고려해보자. - [PR 109](https://github.com/woowacourse/javascript-lotto/pull/109#discussion_r813861348) 137 | - 순차적인 데이터로 배열을 채울 때 활용할 수 있는 내장함수 - [PR 109](https://github.com/woowacourse/javascript-lotto/pull/109#discussion_r816423887) 138 | 139 | ```js 140 | //before 141 | const array = []; 142 | 143 | for (let i = start; i <= end; i += 1) { 144 | array.push(i); 145 | } 146 | 147 | return array; 148 | }; 149 | 150 | //after : for문을 사용하지 않는 방식 151 | Array.from({ length: end - start + 1 }).map((_, index) => start + index); 152 | ``` 153 | 154 | - 배열을 생성할 때 `map`을 생각해보자 - [PR 95](https://github.com/woowacourse/javascript-lotto/pull/95#discussion_r814757511) 155 | 156 |
157 | 158 | ### 함수 159 | 160 | - 데이터가 변경될 것을 대비해서 그 데이터를 대표하는 단어를 함수에 쓰지 말 것 - [PR 99](https://github.com/woowacourse/javascript-lotto/pull/99#discussion_r814071346) 161 | - 특정 값에 종속적인 함수는 분리할 필요가 없다. - [PR 101](https://github.com/woowacourse/javascript-lotto/pull/101#discussion_r815254485) 162 | - 역할을 분리하는 것은 좋지만 역할을 분리함으로써 불필요한 코드가 더 생기진 않는지 생각해보자. - [PR 95](https://github.com/woowacourse/javascript-lotto/pull/95#discussion_r814765497) 163 | 164 |
165 | 166 | ### 콜백함수 167 | 168 | - 콜백함수를 넘긴다는건 그 함수에게 역할을 위임 하겠다는 것 - [PR 102](https://github.com/woowacourse/javascript-lotto/pull/102#discussion_r815267289) 169 | 170 |
171 | 172 | ### 유용한 내장 함수 173 | 174 | - 가격에 단위표시를 하고 싶다면 `toLocaleString()`을 사용해보자. - [PR 99](https://github.com/woowacourse/javascript-lotto/pull/99#discussion_r815812894) 175 | - 현재 문자열의 시작을 다른 문자열로 특정 길이만큼 채우고 싶다면 [`padStart()`](https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/String/padStart)를 사용해보자. - [PR 95](https://github.com/woowacourse/javascript-lotto/pull/95#discussion_r814791814) 176 | 177 |
178 | 179 | ### 중복 판단 180 | 181 | - 중복을 판단하는 로직을 작성하기보다는 Set을 활용해보자 - [PR 99](https://github.com/woowacourse/javascript-lotto/pull/99#discussion_r814076018) 182 | 183 |
184 | 185 | ### 예외 처리 186 | 187 | - `Throw`와 `try-catch`문을 함께 사용할 때 `alert`를 `catch`에 작성해주면 코드량도 줄고 유연한 구조가 된다. - [PR 94](https://github.com/woowacourse/javascript-lotto/pull/94#discussion_r815283789) 188 | 189 |
190 | 191 | ### 테스트 192 | 193 | - TDD의 잔재주의! 테스트는 실제 모듈로 진행하자 - [PR 99](https://github.com/woowacourse/javascript-lotto/pull/99#discussion_r815802271) 194 | - 성공을 보장해야하는 테스트도 존재한다. [PR 99](https://github.com/woowacourse/javascript-lotto/pull/99#discussion_r816019589) 195 | - 테스트코드를 Given, When, Then 구조로 구체적인 설명을 적는다면 역할과 흐름이 잘 보여진다. - [PR 102](https://github.com/woowacourse/javascript-lotto/pull/102#pullrequestreview-894376083) 196 | - 경계값에 대한 테스트도 다양하게 작성해보자. - [PR 94](https://github.com/woowacourse/javascript-lotto/pull/94#discussion_r816456100) 197 | 198 |
199 | 200 | ### UI/UX 201 | 202 | - 디자인을 이유로 input의 focus를 알려주는 outline을 삭제하지 말 것. 어떤 요소에 접근했는지 알려주는 역할이기 때문이다. 이는 웹 접근성 관련 - [PR 101](https://github.com/woowacourse/javascript-lotto/pull/101#discussion_r814892644) 203 | - 버튼이 disable일때는 표시해주자. - [PR 100](https://github.com/woowacourse/javascript-lotto/pull/100#discussion_r814748074) 204 | 205 |
206 | 207 | ### HTML 208 | 209 | - [Heading](https://developer.mozilla.org/ko/docs/Web/HTML/Element/Heading_Elements#%EC%82%AC%EC%9A%A9_%EC%9D%BC%EB%9E%8C) 단계를 뛰어넘지 말 것. 순차적으로 기입하세요. Heading은 글자 크기를 위해 존재하는 게 아닙니다. - [PR 102](https://github.com/woowacourse/javascript-lotto/pull/102#discussion_r815262983) 210 | - `label`과 `input`을 `for`로 연결해줄 것 - [PR 102](https://github.com/woowacourse/javascript-lotto/pull/102#discussion_r815269072) 211 | - `innerHTML`은 XSS 공격에 취약하고, 무겁고 비싸다. 대신 `createElement`나 `insertAdjacentHTML`를 사용할 수 있다. - [PR 100](https://github.com/woowacourse/javascript-lotto/pull/100#discussion_r815269744) 212 | 213 | ### CSS 214 | 215 | - 기본 태그 (`input`, `p` 등)으로 스타일을 조정하면 서비스가 커질 때 사이드 이펙트를 발생시킬 확률이 높다. - [PR 109](https://github.com/woowacourse/javascript-lotto/pull/109#discussion_r813836290) 216 | - user agent stylesheet를 고려한 [reset.css](https://abcdqbbq.tistory.com/9) - [PR 90](https://github.com/woowacourse/javascript-lotto/pull/90#discussion_r813975297) 217 | - css를 HTML에 link를 통해 import할 경우 css의 rule이 전역에서 사용된다. 이는 사이드 이팩트를 일으킴. 이런 문제를 해결하기 위해 [css 모듈화](https://blog.toycrane.xyz/css%EC%9D%98-%EC%A7%84%ED%99%94-%EA%B3%BC%EC%A0%95-f7c9b4310ff7)가 진행 - [PR 94](https://github.com/woowacourse/javascript-lotto/pull/94#pullrequestreview-894384954) 218 | - js에서 직접 스타일을 조정하지 않고 class를 달아주고 제거하는 방식을 택하자. - [PR 95](https://github.com/woowacourse/javascript-lotto/pull/95#discussion_r814778236) 219 | 220 |
221 | 222 | ### util 223 | 224 | - 도메인에 속해있는 로직은 util로 분리하지 마세요. 범용적인 성격을 가진 아이들만 분리해주세요. - [PR 102](https://github.com/woowacourse/javascript-lotto/pull/102#discussion_r815270046) 225 | 226 |
227 | 228 | ### YAGNI! 229 | 230 | - `You Aren't Gonna Need It` - 처음부터 확장성이나 미래를 심히 고려해 오버엔지니어링하는 걸 경계하자. - [PR 100](https://github.com/woowacourse/javascript-lotto/pull/100#pullrequestreview-894510935) 231 | -------------------------------------------------------------------------------- /lotto/step2/hope.md: -------------------------------------------------------------------------------- 1 | 2 | ## 분석 담당 코드 3 | - [시지프](https://github.com/woowacourse/javascript-lotto/pull/151) 4 | - [호프](https://github.com/woowacourse/javascript-lotto/pull/154) 5 | - [병민](https://github.com/woowacourse/javascript-lotto/pull/133) 6 | - [티거](https://github.com/woowacourse/javascript-lotto/pull/150) 7 | - [우연](https://github.com/woowacourse/javascript-lotto/pull/149) 8 | - [소피아](https://github.com/woowacourse/javascript-lotto/pull/139) 9 | - [해리](https://github.com/woowacourse/javascript-lotto/pull/130) 10 | 11 | ### 함수 12 | - [#130] Math.floor는 소수점 내림, parseInt는 소수점을 버림 / 따라서 양수에는 동일하게 작용하나 음수에는 다르게 작용 13 | - parseInt는 정확하게 string 형을 number로 만들어준다. 14 | - string -> number 변환시 박싱 / 언박싱이 일어난다. 15 | - 즉, 메모리 계층에서 기존 메모리 타입인 string을 갖고 있다가, number 형태의 메모리 블록을 만들고 그 후 string을 number로 옮기며 메모리의 비효율이 일어난다. 16 | - Math.floor는 타입캐스팅하지 않고 오롯이 계산만 하기 때문에 박싱 / 언박싱에서 자유로움 17 | 18 | ### 네이밍 19 | ```javascript 20 | intersection(arrayA, arrayB) 21 | ``` 22 | - [#150] 집합 개념을 사용하고 있을 때에는 1,2 보다 A, B가 들어간 이름이 더 적합하다. 23 | 24 | 25 | ### 이벤트 관련 26 | - [#151] input은 값이 들어온 직후 발생되는 이벤트 / keyup은 keyboard에서 떨어질때 발생하는 이벤트 27 | 28 | 29 | ### 반복문 30 | - [#110] 정해진 횟수만큼 반복할 때는 for문, 그러지 않을때에는 while문 사용 31 | 32 | 33 | ### 테스트 34 | - [#110] 변경으로 인해 사이드 이펙트가 없고 해당 로직이 변경되지 않는다는 확신이 있다면 테스트 할 필요성이 떨어진다. 35 | - [#149] 테스트를 위한 분리는 지양하기. 하지만 함수가 하나의 행동만 해야된다는 원칙에 위배된다면 분리해는게 좋은 방향성이라고 생각하는데 단일 행동을 하게 작성하다보면 테스트하기 좋은 구조가 나온다. 36 | ```javascript 37 | import * as Utils from '../utils/util'; 38 | 39 | jest 40 | .spyOn(Utils, 'generateRandomInRange') 41 | // ReturnValue chaining하여 정의 42 | 43 | ``` 44 | - [#149] jest의 spyOn 메서드를 통해 특정 object의 key에 해당하는 함수를 mocking하기 45 | - [#150] [단위테스트vs인수테스트vs통합테스트](https://tecoble.techcourse.co.kr/post/2021-05-25-unit-test-vs-integration-test-vs-acceptance-test/), 하나의 테스트를 위해 여러가지 함수를 불러와서 실행시킨 후, 결과를 테스트한다면 단위테스트가 아니다. 46 | 47 | 48 | 49 | ### DOM 관련 50 | 51 | #### 자식노드 초기화 방법 [#110] 52 | - 부모에서 firstChild가 없을 때 까지 자식노드를 삭제한다 (자식 엘리먼트에서 remove를 사용하거나 부모 엘리먼트에서 removeChild를 활용) => 반복적으로 호출이 필요한 부분이지만 브라우저와 하위 노드 수에 따라 차이가 있겠지만 하위 노드의 수가 1000개를 기준으로 벤치마크에서 innerHTML보다 좋은 성능 53 | - textContent나 innerHTML을 “”로 변경 54 | 추가적으로 엘리먼트를 createElement로 생성했다면 replaceChildren으로 생성한 노드로 교체할 수 있다. 55 | - [#130] aria label은 해당 section이 어떤 것을 의미하는지 알려주고 웹에 더 정확한 정보를 전달하기 위해 사용한다. 56 | 57 | ### CSS 58 | - [#130] position 속성이 지정된 엘리먼트보다 position 속성이 지정되지 않은 엘리먼트가 먼저 렌더링 되어서 따로 z-index 설정을 할 필요가 없다. 59 | - 따라서 position이 absolute, fixed, sticky 등은 position이 지정되지 않은 엘리먼트보다 후에 렌더링한다. 60 | - 쌓임맥락으로 인해 z index 설정이 필요 없고 쌓임맥락 기준으로 그래픽 레이어가 렌더링되기 때문 61 | - 기본적인 position 설정을 안해주면 static이 되는데, static은 쌓임 맥락이 없으므로 가장 바닥에 쌓이고, 쌓임 맥락은 그 위로 쌓이기 때문에 그래픽 레이어 생성시 앞서 보이기 때문이다. [관련 링크](https://developer.mozilla.org/ko/docs/Web/CSS/CSS_Positioning/Understanding_z_index/The_stacking_context) 62 | -------------------------------------------------------------------------------- /lotto/step2/kkojae.md: -------------------------------------------------------------------------------- 1 | # STEP2. 꼬재 담당 크루 PR 링크 2 | 3 | --- 4 | 5 | - [#132꼬재](https://github.com/woowacourse/javascript-lotto/pull/132) 6 | - [#138도리](https://github.com/woowacourse/javascript-lotto/pull/138) 7 | - [#145후이](https://github.com/woowacourse/javascript-lotto/pull/145) 8 | - [#144콤피](https://github.com/woowacourse/javascript-lotto/pull/144) 9 | - [#124나인](https://github.com/woowacourse/javascript-lotto/pull/124) 10 | - [#128비녀](https://github.com/woowacourse/javascript-lotto/pull/128) 11 | - [#164코이](https://github.com/woowacourse/javascript-lotto/pull/134) 12 | 13 | ## 아키텍쳐 분석 14 | 15 | --- 16 | 17 |
18 | 19 | ### #132꼬재 20 | 21 | --- 22 | 23 | #### LottoConsumer 24 | 25 | 로또 구매자 티켓 관련 담당 26 | 27 | #### LottoSeller 28 | 29 | 로또 판매자 티켓 관련 담당 30 | 31 | #### LottoApp 32 | 33 | 이벤트 바인딩과 전체 프로그램 로직을 담당 34 | 35 | #### 그 외 파일 36 | 37 | util, template, dom 요소를 함수로 가지고 있고 필요한 영역에서 import 후 사용 38 | 39 |
40 | 41 | ### #138도리 42 | 43 | --- 44 | 45 | #### Lotto 46 | 47 | 로또 티켓 한 장을 생성하는 객체 48 | 49 | #### PurchasedLotto 50 | 51 | 구매한 전체 로또를 저장하고 있음 52 | 53 | #### LottoApp 54 | 55 | 이벤트 바인딩과 전체적인 프로그램 로직을 담당 56 | 57 | #### view 폴더 58 | 59 | view에 해당하는 로직과 dom 요소, templates들을 분리하여 관리 60 | 61 |
62 | 63 | ### #145후이, #124나인, #128비녀, #164코이, #144콤피 64 | 65 | --- 66 | 67 | #### model 68 | 69 | 나인, 비녀 : 로또 한 장의 객체 / 유저가 구매한 로또들, 당첨 번호를 관리하는 객체 70 | 코이 : 유저가 구매한 로또들, 당첨 번호를 관리하는 객체 71 | 후이 : 로또 한 장의 객체 / 당첨 로또 한 장의 객체 / 유저가 구매한 로또를 관리하는 객체 / 유저가 구매한 로또의 당첨 결과를 관리하는 객체 72 | 73 | 후이) 74 | js에서는 지원하지 않는 protected 사용 \_(언더바를 이용) 75 | 76 | 상속을 통해 로또 한장의 객체와 당첨 로또 한 장의 객체에서 overriding해서 사용해 중복된 코드 제거 77 | 78 | ```js 79 | export default class Lotto { 80 | // _는 protected 입니다. 81 | _lottoNumbers = []; 82 | 83 | generate() { 84 | const { MAX_RANDOM_NUMBER: MAX, MIN_RANDOM_NUMBER: MIN } = LOTTO_SETTING; 85 | const shuffledList = shuffle( 86 | [...Array(MAX - MIN + 1)].map((_, idx) => idx + MIN) 87 | ); 88 | this._lottoNumbers = shuffledList.slice( 89 | 0, 90 | LOTTO_SETTING.LOTTO_NUMBER_LENGTH 91 | ); 92 | 93 | return this; 94 | } 95 | } 96 | ``` 97 | 98 | ```js 99 | export default class WinningLotto extends Lotto { 100 | generate(winningNumbers, bonusNumber) { 101 | try { 102 | checkValidWinningNumberInput(winningNumbers, bonusNumber); 103 | 104 | this._lottoNumbers = [...winningNumbers]; 105 | this.#bonusNumber = bonusNumber; 106 | 107 | return this; 108 | } catch (error) { 109 | throw new Error(error); 110 | } 111 | } 112 | } 113 | ``` 114 | 115 | #### controller 116 | 117 | 전체적인 프로그램 로직을 담당 118 | 119 | - 이벤트 바인딩 120 | - validate 121 | 122 | #### views 폴더 123 | 124 | 각각의 view 별로 파일을 나눠 view를 관리 125 | 126 | #### utils 127 | 128 | document에서 돔요소를 찾는 경우와 부모 엘리먼트에서 돔 요소를 찾을 수 있게 자동화 시킨 부분이 인상 깊어 코드를 가져왔습니다. 129 | 130 | ```js 131 | export const $ = (parentElement, childSelector = null) => { 132 | const target = childSelector || parentElement; 133 | const $parent = childSelector 134 | ? parentElement 135 | : document.getElementById(SELECTOR.ID.APP); 136 | 137 | return $parent.querySelector(target); 138 | }; 139 | 140 | export const $$ = (parentElement, childSelector = null) => { 141 | const target = childSelector || parentElement; 142 | const $parent = childSelector 143 | ? parentElement 144 | : document.getElementById(SELECTOR.ID.APP); 145 | 146 | return $parent.querySelectorAll(target); 147 | }; 148 | ``` 149 | 150 |
151 | 152 | 에러 메시지를 관리할 때 줄 바꿈을 활용하는 부분이 인상 깊어 코드를 가지고 왔습니다. 153 | 154 | ```js 155 | export const ERROR_MESSAGE = { 156 | NEGATIVE_INPUT_ERROR: 157 | "양수를 입력해 주세요! \n (please enter positive number)", 158 | NOT_INTEGER_INPUT_ERROR: 159 | "정수를 입력해 주세요! \n (please enter integer number)", 160 | NOT_MUTIPLE_THOUSAND: 161 | "1000단위로 입력해 주세요! \n (please enter number that is mutiples of thousand)", 162 | }; 163 | ``` 164 | 165 |
166 | 167 | ## 피드백 정리 168 | 169 | ### CSS 170 | 171 | --- 172 | 173 | #### [#145] !important는 지양 174 | 175 | - CSS 케스케이딩에 대해 명확하게 확인해볼 필요가 있음. 176 |
정확하게 정리되어 있는 블로그를 찾지는 못함. 177 | 178 | #### [#145] vw, vh 와 em, rem 의 차이를 알아볼까요? 179 | 180 | 현재 실행중인 스크린 크기에 맞춰 상대적 크기를 반환하겠다는 의미의 vw, vh 181 | 182 | - vw 183 | 184 | - viewport width 185 | - 1vw는 viewport의 넓이의 1%와 같다 186 | 187 | - vh 188 | - viewport height 189 | - 1vh는 viewport의 높이의 1%와 같다 190 | 191 |
192 | 193 | em과 rem은 font-size 속성 값에 비례해서 결정되는 상대단위 194 | 195 | - em 196 | 197 | - 해당하는 단위가 사용되고 있는 요소의 font-size 속성 값이 기준이된다. 198 | 199 | - rem 200 | - 최상위 요소의 font-size 속성 값이 기준이된다. 201 | 202 | #### [#145] 의미있는 단위로 줄 간격을 줘볼까요? 203 | 204 | - 코드 라인수에 제한을 두는 것 보다, 의미있는 단위로 줄 간격을 나눠주는 것이 가독성을 높이는데 더 효율적이다. 205 | 206 | #### [#164] color 값은 css 변수로 관리해보자! 207 | 208 | ```css 209 | :root { 210 | --color-primary: #00bcd4; 211 | --color-light-gray: #b4b4b4; 212 | --color-white: #ffffff; 213 | --color-table-border: #c8c8c8; 214 | } 215 | ``` 216 | 217 | #### [#164] 불필요한 z-index를 사용하지 않는다. 218 | 219 | - z-index를 1로 둔 이유가 있을까요? 220 |
만약 1로 두지 않았다면 어떻게 되었을까요? 221 |
쌓임의 맥락에 대해 한 번쯤 살펴볼 필요가 있다. 222 | 223 | ### JS 224 | 225 | --- 226 | 227 | #### [#132] from을 사용하면 fill,map 두가지 메소드를 사용하지 않고 하나의 메소드 구현할 수 있습니다. 더불어 현재 map에서 첫번째 파라미터 \_를 사용하지않고 있네요! 228 | 229 | ```js 230 | setLottoList(count) { 231 | this.#lottoList = Array(count) 232 | .fill(0) 233 | .map((_, index, list) => (list[index] = this.createLottoList(count))); 234 | } 235 | ``` 236 | 237 | ```js 238 | setLottoList(count) { 239 | this.#lottoList = Array.from({length: count}, () => this.createLottoList(count)); 240 | } 241 | ``` 242 | 243 | #### [#145] setter 가 아닌 addMoney 함수가 더 어울리네요! 그리고 setter 는 안만들수록 좋으니 더더욱 addMoney 가 좋겠네요! 244 | 245 | - setter는 가급적 사용하지 않는게 좋다. 246 | - setter 대신에 생성자에 값을 넘겨주거나, 빌더 패턴을 사용하는 방법이 있다. 247 | 248 | 빌더 패턴이란..?! 249 | 250 | #### [#145] WinningLotto 가 Lotto 를 상속해보면 어떨까요? 251 | 252 | - 위에 코멘트 남긴대로 pushNumberIntoPickedNumbers 가 필요없으니 pushPickNumbers 같은 함수가 생길테니 재사용가능하고, generate 는 재정의하고, pickedNumbers 를 활용하면 bonusNumber 관련 로직만 추가하면 될 것 같은데요! 어떤가요? 253 | 254 | ```js 255 | export default class Lotto { 256 | // _는 protected 입니다. 257 | _lottoNumbers = []; 258 | 259 | generate() { 260 | const { MAX_RANDOM_NUMBER: MAX, MIN_RANDOM_NUMBER: MIN } = LOTTO_SETTING; 261 | const shuffledList = shuffle( 262 | [...Array(MAX - MIN + 1)].map((_, idx) => idx + MIN) 263 | ); 264 | this._lottoNumbers = shuffledList.slice( 265 | 0, 266 | LOTTO_SETTING.LOTTO_NUMBER_LENGTH 267 | ); 268 | 269 | return this; 270 | } 271 | } 272 | ``` 273 | 274 | ```js 275 | export default class WinningLotto extends Lotto { 276 | generate(winningNumbers, bonusNumber) { 277 | try { 278 | checkValidWinningNumberInput(winningNumbers, bonusNumber); 279 | 280 | this._lottoNumbers = [...winningNumbers]; 281 | this.#bonusNumber = bonusNumber; 282 | 283 | return this; 284 | } catch (error) { 285 | throw new Error(error); 286 | } 287 | } 288 | } 289 | ``` 290 | 291 | #### [#145] 상속에 대한 리뷰어님의 생각 292 | 293 | - 아하 상속은 항상 장단점이 있죠! 한 함수만을 추상화한다해도 여러 자식 클래스에서 상속할 부모 클래스라면 그럴 가치는 있죠! 294 |
295 | 다만 프론트는 서버에 비해 변경 가능성이 크기 때문에 (예를 들어 숫자 인풋에서 문자 인풋으로 바뀔 수도 있죠?) 뷰 단까지 상속 구조를 써야 하나..? 내가 실무에서 뷰 단을 상속한 적이 있었나..? 라고 생각했을 땐 케이스가 별로 없네요 😅 296 |
297 | 아마 나중에 api 단을 구현하실 때 상속할 일이 좀 나올거에요! 그 때 더 고민하면서 적용해보시죠! 298 | 299 | #### [#144] 재사용되지 않는 View 들은 굳이 selector 들을 외부에서 주입해줘야할까? 라는 생각이 들긴 하는군요 🤔 300 | 301 | - 재사용되지 않는 인스턴스는 필요한 곳에서 생성해서 사용하는 것이 좋다. 맴버 변수로 관리할 필요가 없기 때문 302 | 303 | #### [#144] 이건 fill 을 이용해 초기화할 수도 있을 거 같습니다! (수정할 필요는 없어용) 304 | 305 | ```js 306 | const output = Array.from( 307 | { length: LOTTO_SETTING.RACKING_START_NUMBER }, 308 | () => 0 309 | ); 310 | const output = Array(LOTTO_SETTING.RACKING_START_NUMBER).fill(0); 311 | ``` 312 | 313 | #### [#144] 오... 이벤트를 한 번만 걸어주고 자동으로 지워주는 게 있군요! 하나 더 배워갑니다ㅎ 314 | 315 | ```js 316 | $element.addEventListener(eventType, callback, { 317 | once: true, 318 | }); 319 | ``` 320 | 321 | ### 컨벤션 322 | 323 | --- 324 | 325 | #### [#144] SELECTOR를 상수화 할 경우 class선택자와 id 선택자를 분리하여 작성해주는게 좋다. 326 | 327 | - 물론, 당연히 SELECTOR, DOM_NAME 의 쓰임새에 대한 '약속'을 했지만, 휴먼에러는 언제든 발생할 수 있으니까요. 328 | ID SELECTOR 인데 CLASS 로 생각해서 iterator 문법을 사용할 수도 있고요 ;ㅁ; 329 | 330 | #### [#132] MONEY를 객체로 만들 필요가 있을까요?ㅎ 혹시 제가 생각못한 확장성 부분이 있을까요? 331 | 332 | - 현재 프로젝트에서는 모두 객체로 사용하지만 실제 상수는 enum, array, 값 등 다양하게 사용됩니다. 그렇기때문에 통일성을 맞추기보다 그 역할에 맞게 작성하시는게 좋을 것 같습니다 ㅎ 333 | 334 | ```js 335 | export const MONEY = { 336 | STANDARD: 1000, 337 | }; 338 | ``` 339 | 340 | ```js 341 | export const MONEY_STANDARD = 1000; 342 | ``` 343 | 344 | ### 테스트 345 | 346 | --- 347 | 348 | #### [#145] 의미 없는 테스트 코드는 지워준다. 349 | 350 | - 유효성 체크를 처음부터 모두 짜놓고 코드를 구현하겠지만, 코드 구현 후 테스트 코드로 돌아왔을 때 막상 필요없는 테스트라면 지우는게 맞지 않을까 싶습니다! 351 | 352 | #### [#132] 모든 테스트 케이스마다 cy.visit('./index.html');가 있는데요 이는 before으로 따로 빼도 좋을 것 같습니다ㅎ 353 | 354 | [before/beforeEach](https://docs.cypress.io/guides/core-concepts/writing-and-organizing-tests#Hooks) 355 | 356 | - before : 같은 describe 안에서 테스트가 많더라도 단 한 번만 실행되는 경우 사용 357 |
358 | cy.visit()의 경우 한 번 방문한 후 계속해서 동작을 이어나갈 수 있다. 359 | 360 | - beforeEach : 같은 describe 안에서 테스트별로 각각 한 번씩 사용되어야할 경우 사용 361 | -------------------------------------------------------------------------------- /lotto/step2/lokba.md: -------------------------------------------------------------------------------- 1 | # Level1 Lotto Step2(담당 PR 번호들) - 록바 2 | 3 | - 분석 담당 코드 4 | - 돔하디 [#126](https://github.com/woowacourse/javascript-lotto/pull/126) 5 | - 민초 [#155](https://github.com/woowacourse/javascript-lotto/pull/155) 6 | - 블링 [#129](https://github.com/woowacourse/javascript-lotto/pull/129) 7 | - 태태 [#125](https://github.com/woowacourse/javascript-lotto/pull/125) 8 | - 하리 [#122](https://github.com/woowacourse/javascript-lotto/pull/122) 9 | - 록바 [#148](https://github.com/woowacourse/javascript-lotto/pull/148) 10 | - 코카콜라 [#137](https://github.com/woowacourse/javascript-lotto/pull/137) 11 | - 안 [#153](https://github.com/woowacourse/javascript-lotto/pull/153) 12 | 13 |
14 | 15 | ## 아키텍처 분석(desc: 담당한 소프트웨어의 아키텍처를 간단히 분석하고 설명합니다) 16 | 17 |
18 | 19 | ### [#122] 하리 20 | 21 | #### 인상 깊은 점 22 | 23 | - 자주 쓰이는 color를 변수로 관리한 것 24 | 25 | ```javascript 26 | :root { 27 | --main-bg-color: #e5e5e5; 28 | --app-bg-color: #ffffff; 29 | --popup-bg-color: #00000080; 30 | ... 31 | } 32 | ``` 33 | 34 |
35 | 36 | - [MDN](https://developer.mozilla.org/ko/docs/Web/CSS/Using_CSS_custom_properties) 내용 - 흔히 보이는 패턴은 :root 의사 클래스에 선언해서 여러분의 HTML 문서 어디에서나 사용자 지정 속성에 접근할 수 있도록 구성하는 것입니다. 37 | 38 | ### [#129] 블링 39 | 40 | #### 인상 깊은 점 41 | 42 | - fieldset과 legend 사용 43 | 44 | ```html 45 |
46 |
47 | 당첨 번호 48 | 57 | ... 58 |
59 |
60 | ``` 61 | 62 | - [MDN] `
` 요소는 HTML 양식 속에서 그룹을 만들 수 있으며 `` 요소로 그룹의 설명(제목)을 제공할 수 있습니다. fieldset 태그는 주로 form 내부에 사용한다. 63 | 64 |
65 | 66 | - 기존의 요소를 직접 생성한 방식을 선언적으로 구현 67 | 68 | ```javascript 69 | // 구현 70 | const createCustomElement = ({ tag, className, id, children }) => { 71 | const element = document.createElement(tag); 72 | Object.assign(element, className && { className }, id && { id }); 73 | if (Array.isArray(children)) { 74 | element.append(...children); 75 | return element; 76 | } 77 | element.append(children); 78 | return element; 79 | }; 80 | 81 | export const div = function createCustomDiv({ className, id, children }) { 82 | return createCustomElement({ tag: 'div', className, id, children }); 83 | }; 84 | 85 | export const p = function createCustomP({ className, id, children }) { 86 | return createCustomElement({ tag: 'p', className, id, children }); 87 | }; 88 | 89 | // 사용 90 | #generateLottoElement(lottoNumberSet) { 91 | return div({ 92 | className: 'lotto', 93 | children: [ 94 | p({ className: 'lotto-image', children: '🎟️' }), 95 | p({ 96 | className: 'lotto-numbers', 97 | children: Array.from(lottoNumberSet).join(', '), 98 | }), 99 | ], 100 | }); 101 | } 102 | ``` 103 | 104 | - 방식이 신선하다. react의 jsx를 모티브 삼아 구현했다고 한다. 105 | - 리뷰어분이 추천해준 react 관련 아티클 [링크](https://pomb.us/build-your-own-react/) 106 | - 만약, depth가 깊은 요소를 만들어야 할 경우 함수 호출이 빈번히 일어나기에 비효율적이라 판단됨. 107 |
108 | 109 | --- 110 | 111 | ## 피드백 정리 112 | 113 | ### 대분류(ex: 아키텍처, 함수/클래스, 컨벤션, DOM, 테스트 등) 114 | 115 | #### 함수/클래스 116 | 117 | - [#148] 이벤트 위임 - [바로가기](https://github.com/woowacourse/javascript-lotto/pull/148/files/397534106e2a8f87c1e4f76ef04723e4f54dd529#r820122291) 118 | 119 | - 수정 전 - (피드백 :각 input 하나하나에 event handler를 등록하지 않고, 이벤트 위임을 활용하자.) 120 | 121 | ```javascript 122 | this.winningNumberInputs.forEach((inputElement, index) => { 123 | inputElement.addEventListener("input", () => 124 | this.handleWinningNumberInputFocus(inputElement, index) 125 | ); 126 | }); 127 | ``` 128 | 129 | - 수정 후 - 최종 통과 130 | 131 | ```javascript 132 | this.winningNumberForm.addEventListener("submit", this.handleWinningNumberFormSubmit); 133 | 134 | handleWinningNumberFormInputFocus(e) { 135 | if (e.target.classList.contains('winning-number-input')) { 136 | ... 137 | } 138 | } 139 | 140 | ``` 141 | 142 |
143 | 144 | - [#125] 모델의 데이터 초기화 방식 - [바로가기](https://github.com/woowacourse/javascript-lotto/pull/125#pullrequestreview-899701248) 145 | 146 | - 초기화하는 메소드를 호출해서 초기화 vs 새로운 인스턴스 생성해서 덮어씌우기 147 | 148 | ```javascript 149 | //1번 150 | this.model.init(); 151 | 152 | //2번 153 | this.model = new Model(); 154 | ``` 155 | 156 | - [리뷰어] 상황에 따라 적절한 방법을 택하면 됩니다. (1번)은 초기화할 대상이 많지 않아 간단히 처리할 수 있는 경우에 적합하고, (2번)는 초기화를 위한 비용이 새로 생성하는 것보다 많이 들 때 적합하겠죠. (2번)의 경우에도 버려진 기존 데이터는 G.C. 대상이 되니 원칙적으로는 걱정할 것이 없구요. (모델 외부 또는 내부 프로세스 중에 해당 데이터의 참조카운트를 증가시키는 사례가 있다면 G.C되지 않으니 주의가 필요하긴 합니다) 157 | 158 |
159 | 160 | - [#125] 함수 분리 vs 함수 통합 - [바로가기](https://github.com/woowacourse/javascript-lotto/pull/125#discussion_r819189317) 161 | - **기능별로 잘게 쪼개놓고 보니 활용하는 곳이 하나뿐인 함수는 다시 하나로 통합하는 리팩터링을 거치는 편이 좋습니다.** 162 | 163 |
164 | 165 | ### 컨벤션 166 | 167 | - [#148] customEvent 관련 유틸 함수 네이밍 - [바로가기](https://github.com/woowacourse/javascript-lotto/pull/148/files/397534106e2a8f87c1e4f76ef04723e4f54dd529#r820119930) 168 | 169 | - 수정 전 - (피드백 : 함수명이 짧다) 170 | 171 | ```javascript 172 | export const on = (target, eventName, handler) => { 173 | target.addEventListener(eventName, handler); 174 | }; 175 | 176 | export const emit = (target, eventName, detail) => { 177 | const event = new CustomEvent(eventName, { detail }); 178 | target.dispatchEvent(event); 179 | }; 180 | ``` 181 | 182 | - 1단계 수정 후 - (피드백 : event라는 변수명이 브라우저에서 흔히 쓰이는 event객체의 인스턴스로 오인할 가능성이 있어, 협업시에 문제 여지 존재) 183 | ```javascript 184 | const event = { 185 | on: (target, eventName, handler) => { 186 | target.addEventListener(eventName, handler); 187 | }, 188 | emit: (target, eventName, detail) => { 189 | const event = new CustomEvent(eventName, { detail }); 190 | target.dispatchEvent(event); 191 | }, 192 | }; 193 | ``` 194 | - 2단계 수정 - 최종 통과 195 | ```javascript 196 | const eventManager = { 197 | on: (target, eventName, handler) => { 198 | target.addEventListener(eventName, handler); 199 | }, 200 | emit: (target, eventName, detail) => { 201 | const event = new CustomEvent(eventName, { detail }); 202 | target.dispatchEvent(event); 203 | }, 204 | }; 205 | ``` 206 | 207 |
208 | 209 | - [#148] 변수명은 요구사항이 변경되어도 수정되지 않도록 네이밍 해야한다. - [바로가기](https://github.com/woowacourse/javascript-lotto/pull/148/files/027ff954b472b18deb84a8365466096f4b947fee#r820450287) 210 | 211 | - 수정 전 (피드백 : isNotDividedIntoUnit로 바꾸는게 어떠냐?) 212 | ```javascript 213 | export const isNotThousandUnit = value => {...} 214 | ``` 215 | 216 |
217 | 218 | - [#122] private과 public의 메소드 순서 정리 - [바로가기](https://github.com/woowacourse/javascript-lotto/pull/122/files/0148a8ca0b693ee52474deb5dd8dc8eac2a285eb#r819318713) 219 | 220 | ```javascript 221 | class PopupView{ 222 | #initDom() {...} 223 | #visible() {...} 224 | #closeHandler() {...} 225 | #addCloseEvent() {...} 226 | addRestartEvent(resetEvent) {...} 227 | render(result, rewardRate) {...} 228 | } 229 | ``` 230 | 231 |
232 | 233 | - [#155] 이것것도 상수로 하자! - [바로가기](https://github.com/woowacourse/javascript-lotto/pull/155#discussion_r820599544) 234 | 235 | ```javascript 236 | //수정 전 237 | movePreviousInput(e) { 238 | if (e.keyCode === 8) {...} 239 | ... 240 | } 241 | 242 | 243 | //수정 후 244 | movePreviousInput(e) { 245 | if (e.keyCode === BACK_SPACE_KEY_CODE) {...} 246 | ... 247 | } 248 | ``` 249 | 250 |
251 | 252 | ### CSS 253 | 254 | - [#148] on/off 상태에 따른 클래스 명 나누기 - [바로가기](https://github.com/woowacourse/javascript-lotto/pull/148/files/397534106e2a8f87c1e4f76ef04723e4f54dd529#r820123190) 255 | - 수정 전 - (피드백 : off인 상태를 기본으로 인지하고 별도로 on이 붙은 경우에만 관리하면 편리해요. on이 안붙어있다면 off일 것이라는 추정도 어렵지 않구요. **관리해야 할 값을 줄이는 것도 읽기 좋은 코드를 만드는 기법 중 하나**입니다.) 256 | ```javascript 257 | classList.contains("switch-off") 258 | ? classList.replace("switch-off", "switch-on") 259 | : classList.replace("switch-on", "switch-off"); 260 | ``` 261 | - 수정 후 - 최종 통과 262 | ```javascript 263 | this.purchasedLottoList.classList.toggle("switch-on"); 264 | ``` 265 | 266 |
267 | 268 | ### 기타 269 | 270 | - [#122] 수학적 계산이 들어간 부분은 주석을 달면 좋다. - [바로가기](https://github.com/woowacourse/javascript-lotto/pull/122/files/0148a8ca0b693ee52474deb5dd8dc8eac2a285eb#r819291894) 271 | 272 | ```javascript 273 | // 수익률 = (총 상금 / 구매 금액)을 퍼센트 환산 274 | // 즉, 원금 상환 === 수익률 100% 275 | const rewardRate = totalReward / this.lottos.length / 10; 276 | ``` 277 | 278 |
279 | 280 | - [#125] modal은 보통 body 태그의 맨 마지막에 둔다. **z-index를 굳이 설정하지 않아도 자연히 맨 위로 올라갈 수 있기 때문이다.** 281 | 282 |
283 | 284 | ### Test 285 | 286 | - [#125] #으로 은닉한 메소드의 경우에는 유닛 테스트를 할 수 없다. - [바로가기](https://github.com/woowacourse/javascript-lotto/pull/125#pullrequestreview-899701248) 287 | - 리뷰어 답변 : **원래부터 private method는 테스트할 수 없도록 하는게 객체지향에서의 본래 취지**입니다. "객체 내부 로직에 대해서는 궁금하지 않고 오직 밖으로 드러난 것만 잘 보이면 된다"는 차원이거든요. '단위테스트'라고 할 때의 '단위'가 함수 내지 메소드단위일 경우도 있지만, '객체'를 하나의 단위로 인식하는게 일반적입니다. 이 객체(인스턴스)에 대한 테스트는 오직 public method를 통해서만 이뤄지도록 하면 되는 것이에요. **public method만으로 기존 테스트를 정상적으로 수행할 수 없다면, 테스트 케이스를 수정하셔야 합니다.** 288 | 289 |
290 | 291 | ### 새로운 접근 292 | 293 | - [#148] validation 로직에서 소개해준 리팩토링 기법 - [바로가기](https://github.com/woowacourse/javascript-lotto/pull/148/files/397534106e2a8f87c1e4f76ef04723e4f54dd529#r820123720) 294 | 295 | - 수정 전 296 | 297 | ```javascript 298 | export const validatePurchaseMoney = (value) => { 299 | if (isZero(value)) { 300 | throw new Error(ERROR_MESSAGE.ZERO_PURCHASE_MONEY); 301 | } 302 | 303 | if (isNotNumber(value)) { 304 | throw new Error(ERROR_MESSAGE.INVALID_PURCHASE_MONEY_TYPE); 305 | } 306 | ... 307 | }; 308 | ``` 309 | 310 | - 추천해준 방식 311 | ```javascript 312 | const purchasedMoneyValidator = [ 313 | { test: isZero, errorMsg: ERROR_MESSAGE.ZERO_PURCHASE_MONEY }, 314 | { test: isNotNumber, errorMsg: ERROR_MESSAGE.INVALID_PURCHASE_MONEY_TYPE }, 315 | ... 316 | ] 317 | const validatePurchaseMoney = value => purchasedMoneyValidator.every(({ test, errorMsg }) => { 318 | if (test(value)) throw new Error(errorMsg); 319 | return true; 320 | }) 321 | ``` 322 | 323 |
324 | 325 | ### 좋은 이야기 326 | 327 | - [#126] 사용자 편의성에 대한 좋은 조언 - [바로가기](https://github.com/woowacourse/javascript-lotto/pull/126#pullrequestreview-900288536) 328 | - **먼저 사용자가 무엇을 해야할지 직관적으로 바로 알 수 없다면 개발자의 잘못**이라고 생각합니다 329 | 직관적으로 알 수 있는 UI가 먼저이고 그럴 수 없는 경우라면 어떻게 해야할지 구체적이고 상세하게 안내해야 된다고 생각합니다 그리고 **악의적인 초등학생이 깨트리려고 해도 절대 깨지지 않는 앱을 만들어야 된다고 생각합니다** (이건 Bsidesoft의 맹대표님 강의 중에 들은 교훈입니다) 330 | 그래서 **`성공하는 일부 케이스만 테스트하는게 아니라 에러를 더 많이 발생시키고 그에 대한 예외처리를 상세히 기술해서 사용성이 깨지는 경우를 방지하려고 노력합니다`** 331 | -------------------------------------------------------------------------------- /lotto/step2/marco.md: -------------------------------------------------------------------------------- 1 | # Level1 Lotto Step2(담당 PR 번호들) - 마르코 2 | 3 | - 분석 담당 코드 4 | - 준찌 [#121](https://github.com/woowacourse/javascript-lotto/pull/121) 5 | - 유세지 [#141](https://github.com/woowacourse/javascript-lotto/pull/141) 6 | - 동키콩 [#143](https://github.com/woowacourse/javascript-lotto/pull/143) 7 | - 온스타 [#131](https://github.com/woowacourse/javascript-lotto/pull/131) 8 | - 위니 [#147](https://github.com/woowacourse/javascript-lotto/pull/147) 9 | - 자스민 [#135](https://github.com/woowacourse/javascript-lotto/pull/135) 10 | - 아놀드 [#127](https://github.com/woowacourse/javascript-lotto/pull/127) 11 | - 빅터 [#152](https://github.com/woowacourse/javascript-lotto/pull/152) 12 | 13 | ## 2. 피드백 정리 14 | 15 | ### 함수분리 16 | 17 | - [#141] 그저 함수가 긴 것 같아서 불필요하게 분리된 짧은 함수는 가독성을 떨어트린다. 18 | 19 | - "확인해보니 함수내 '줄수 제한'은 요구사항이 아닙니다. 애초에 모든 함수가 15줄 이내여야 한다는 건 너무 가혹하기도 하고, 도리어 불필요한 짧은 함수를 남발하여 가독성을 떨어뜨리는 결과를 초래할 수도 있어요." 20 | 21 | - [#131] 함수 분리를 "같은 로직이 반복되어 중복이 발생할 경우에 한정"한다. 긴밀하게 연결된 메서드라면 분리를 위한 분리는 할 필요가 없다. 22 | 23 | ### 테스트코드 24 | 25 | - [#141] 테스트코드에는 경계값 및 여러 경우의 수를 다 넣고 설명도 상세히 쓰자. 26 | - "테스트코드만 봐도 1001, 1000.5, 1500 같은 수들이 다 괜찮을지 어떨지 알 수 있게 해주시면 좋겠어요." 27 | - [#151, #135] jest는 함수의 반환값을 mocking할 수 있다. 예를 들어, lotto 번호를 생성하는 메서드를 `spyOn`이라는 jest함수를 활용하여 mocking할 수 있다. 28 | 29 | - 또한, 당첨 결과 확인 테스트케이스를 커버하고 싶다면, `mockReturnValueOnce`를 체이닝하여 사용하면 호출 시마다 원하는 로또 번호를 입력할 수 있다. 30 | 31 | - [리팩토링한 해당 커밋](https://github.com/woowacourse/javascript-lotto/pull/151/commits/1c301f1fef6ac1133298f3c384ee5fda89ae7268) 32 | 33 | ```js 34 | // model의 메서드 35 | export default class Model { 36 | #cash = 0; 37 | #lottoList = []; 38 | #winningLottoQuantity = { 39 | "3개": 0, 40 | "4개": 0, 41 | "5개": 0, 42 | "5개+보너스볼": 0, 43 | "6개": 0, 44 | }; 45 | 46 | buyLotto(cash) { 47 | this.#lottoList = []; 48 | for (let i = 0; i < cash / LOTTO_PRICE; i++) { 49 | this.#lottoList.push(this.makeLottoNumbers()); 50 | } 51 | } 52 | 53 | makeLottoNumbers() { 54 | return shuffle( 55 | makeAllLottoNumbers(LOTTO_RULE.MIN_NUMBER, LOTTO_RULE.MAX_NUMBER) 56 | ).slice(0, LOTTO_RULE.NUMBERS_COUNT); 57 | } 58 | } 59 | ``` 60 | 61 | ```js 62 | // 테스트코드 63 | describe("당첨 결과 확인 테스트", () => { 64 | test("결과 확인하기 버튼을 누르면, 당첨 갯수와 수익률이 정확히 계산된다", () => { 65 | const pickedNumber = [4, 5, 6, 7, 8, 9, 10]; 66 | 67 | const model = new Model(); 68 | // 해당 메서드를 mocking하여 랜덤 테스트를 조작한다. 69 | model.makeLottoNumbers = jest 70 | .fn() 71 | .mockReturnValueOnce([1, 2, 3, 4, 5, 6]) 72 | .mockReturnValueOnce([4, 5, 6, 7, 8, 9]) 73 | .mockReturnValueOnce([4, 5, 6, 7, 8, 10]) 74 | .mockReturnValueOnce([7, 8, 9, 10, 11, 12]) 75 | .mockReturnValueOnce([13, 14, 15, 16, 17, 18]); 76 | 77 | model.setCash(lottoQuantity * LOTTO_PRICE); 78 | model.buyLotto(lottoQuantity); 79 | model.setWinningLottoQuantity(pickedNumber); 80 | 81 | const winningLottoQuantity = model.getWinningLottoQuantity(); 82 | expect(winningLottoQuantity["3개"]).toBe(2); 83 | expect(winningLottoQuantity["4개"]).toBe(0); 84 | expect(winningLottoQuantity["5개"]).toBe(0); 85 | expect(winningLottoQuantity["5개+보너스볼"]).toBe(1); 86 | expect(winningLottoQuantity["6개"]).toBe(1); 87 | expect(model.calculateProfitRatio()).toBe(40600200); 88 | }); 89 | }); 90 | ``` 91 | 92 | ### 상수화 93 | 94 | - [#141] 여러 곳에서 자주 쓰이는 값들에 대해서만 고민하여 선택적으로 상수화를 하자. 95 | 96 | ### DOM 97 | 98 | - [#141, #143] [setAttribute](https://developer.mozilla.org/en-US/docs/Web/API/Element/setAttribute) vs [classList](https://developer.mozilla.org/ko/docs/Web/API/Element/classList), style 처리를 위한 경우 setAttribute() 메서드보다는 classList 프로퍼티의 메서드(add, remove, toggle, contains, replace)를 사용하는 것이 성능상 더 낫고 편리한 것 같다. 또한, `this.elem.style.display = 'flex'`처럼 js에서 직접 css를 고치기보다는 css의 class를 활용하는 것이 낫다. 99 | 100 | - [#147] dom 요소 탐색 시, root부터 탐색하지 말고 parent부터 탐색하는 방식이 성능상 더 좋다. [리팩토링한 해당 커밋](https://github.com/woowacourse/javascript-lotto/pull/147/commits/88122adf486354c6864f7f881c5584f930788444) 101 | 102 | - [#135] 한글의 경우 keypress 이벤트에서 감지하지 못하는 문제가 있다. 103 | 104 | ### 설계 및 패턴 105 | 106 | - [#121] 커스텀 이벤트를 활용하여 View 이벤트를 바인딩한 MVC 패턴의 경우, 커스텀 이벤트명으로 이벤트 발생을 개발자가 '의도한 시점에서 자유롭게' 이벤트를 일으킬 수 있다. 107 | 108 | - [#121] 클로저 패턴(또는 커링 함수)은 네트워크 요청 데이터 캐싱과 같이 자유변수의 참조값을 캐싱하고자 할 때 많이 쓰고, 스크롤이벤트에 디바운스를 걸 때 활용하기도 한다. 109 | 110 | - [#143] 객체들의 의존 관계를 그림으로 그려보고, 예를 들어 "A객체가 B객체를 꼭 알아야 할까? A객체의 역할은 a인데, b,c 역할에서 수행할 법한 기능을 포함해서 역할이 모호해지네?"등과 같은 고민을 거치면서, 구조를 개선해보자. 111 | 112 | ### 은닉화 113 | 114 | - [#121] 외부에서 접근해서는 안 되는 메소드에 대해서 은닉화한다. 그 접근 가능 여부의 기준에는 "외부에서 접근했을 때 객체의 상태를 직접적으로 변경시키는가?" 또는 "외부에서 써도 안전한 메소드인가?" 등이 있다. 115 | 116 | ### 함수 리팩토링 117 | 118 | - [#147] 배열에 push하는 for문을 Array.from()을 이용하여 리팩토링 119 | 120 | - 참고: [MDN- Array.from()과 화살표 함수 사용하기](https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Array/from#array.from%EA%B3%BC_%ED%99%94%EC%82%B4%ED%91%9C_%ED%95%A8%EC%88%98_%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0) 121 | - 리팩토링 예시 122 | 123 | - ```jsx 124 | // before 125 | const lottos = []; 126 | for (let i = 0; i < this.getLottoCount(); i += 1) { 127 | lottos.push(this.generateLottoNumbers()); 128 | } 129 | return lottos; 130 | ``` 131 | 132 | ```jsx 133 | // after 134 | return Array.from({ length: this.getLottoCount() }, v => 135 | this.generateLottoNumbers() 136 | ); 137 | ``` 138 | 139 | ### 이벤트 위임 활용 140 | 141 | - [#135] 이벤트를 걸려는 element들의 상위 element에 eventListener를 달고, 이벤트 발생에 대한 콜백함수에 이벤트가 발생한 target의 선택자를 확인하는 분기를 만들고 해당 로직을 실행시킨다. 142 | 143 | - 예시 코드 144 | 145 | ```html 146 |
147 |
148 |

🎱 행운의 로또

149 |
150 | 151 | 152 | 158 | 159 |
160 |
161 |
162 |
163 |
164 | ``` 165 | 166 | ```jsx 167 | export default class InputView { 168 | constructor() { 169 | this.$app = $("#app"); 170 | // 생략 171 | } 172 | 173 | bindEvent() { 174 | this.$app.addEventListener("input", this.detectInputState.bind(this)); 175 | } 176 | 177 | detectInputState(e) { 178 | if (e.target.id === "lotto-price-input") { 179 | this.detectMoneyInputState(); 180 | } 181 | if (e.target.classList.contains("winning-number-input")) { 182 | this.detectWinningInputState(); 183 | } 184 | } 185 | } 186 | ``` 187 | 188 | ### 표준 내장 객체 189 | 190 | - [#135] `Number.isNaN`과 `전역 window.isNaN`의 차이 191 | - `전역의 isNaN`은 인수가 Number형이 아닌 경우, 그 값을 먼저 숫자로 강제하고, 그 다음 NaN인지 판단을 한다. `"37"`은 NaN이 아닌 숫자 `37`로 변환되므로 `false`가 반환된다. 그리고 헷갈리게도 `undefined`, `{}`, `'ok'`도 `true(=NaN)`가 반환된다. 192 | - [MDN-혼란스러운 특별 케이스 행동](https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/isNaN#%ED%98%BC%EB%9E%80%EC%8A%A4%EB%9F%AC%EC%9A%B4_%ED%8A%B9%EB%B3%84_%EC%BC%80%EC%9D%B4%EC%8A%A4_%ED%96%89%EB%8F%99) 193 | - 하지만 `Number.isNaN`은 오직 숫자형이면서 NaN인 값만을 true를 반환한다. 예를 들어, `undefined`, `{}`, `'ok'` 같은 것들은 `false(=not NaN)`가 반환된다. 또한, `37`, `"37"`, `""`, `"true"`, `"null"`도 모두 `false`가 반환된다. 194 | - 따라서 보다 엄격하게 `NaN`(`Number.NaN`, `0/0`)만을 판별하고 싶다면 `Number.isNaN`을 사용하는게 좋을 것 같다. 195 | 196 | ### 긍정 조건문이 부정문보다 가독성에 좋다. 197 | 198 | - [#152] 조건에 `!`을 되도록 쓰지 않는다(다만 early return을 사용할때는 부정 조건문을 자주 작성할 수도 있다.). 그리고 식별자명 자체에 부정문이 함축되어 있는 경우도(ex, isNotPositive), 부정문 대신에 부정을 함축한 반의어 단어(ex, negative)를 활용하는 것이 가독성에 더 좋고 실수를 줄일 수 있다. 199 | 200 | - `없는 게 아닐 때`보단 `있을 때`가 좀 덜 헷갈린다. 201 | - `!isEmpty` -> `has~` 202 | 203 | ```plain 204 | !isEnough -> isInsufficientFare 205 | !isValidRangeNumber -> isOverRangeLottoNumber 206 | !isValidRangeNumbers -> isOverRangeLottoNumbers 207 | isNotOverlapped -> isOverlapped 208 | !isValidCount -> isInvalidCount 209 | isNotIncludedWinningNumbers -> isIncludedWinningNumbers 210 | ``` 211 | 212 | ### 순수 함수 213 | 214 | - [#127] 순수하지 않은 함수는 그 작동 과정을 확인하기 위해 다른 함수를 살펴야 해서 좋지 않을 수 있다. 215 | -------------------------------------------------------------------------------- /lotto/step2/movie.md: -------------------------------------------------------------------------------- 1 | # LEVEL 1 행운의 로또 STEP 2 - 무비😎 2 | 3 | ## 분석 담당 코드 4 | 5 | - 마르코 [PR 142](https://github.com/woowacourse/javascript-lotto/pull/142) X 무비 [PR 140](https://github.com/woowacourse/javascript-lotto/pull/140) 6 | - 밧드 [PR 146](https://github.com/woowacourse/javascript-lotto/pull/146) X 우디 [PR 156](https://github.com/woowacourse/javascript-lotto/pull/156) 7 | - 앨버 [PR 136](https://github.com/woowacourse/javascript-lotto/pull/136) X 결 [PR 120](https://github.com/woowacourse/javascript-lotto/pull/120) 8 | - 샐리 [PR 100](https://github.com/woowacourse/javascript-lotto/pull/100) 9 | 10 |
11 |
12 | 13 | ## 피드백 정리 14 | 15 | 😛 링크를 누르면 해당 discussion으로 이동! 😛 16 | 17 |
18 | 19 | ### MVC 패턴 20 | 21 | - (_데이터 변경이 없는 이벤트에 대한 처리는 View에서 해야할까요? Controller에서 해야할까요?_) \* 22 | 1. Controller를 통해 이벤트를 처리할 필요가 없다면 View - [PR 142](https://github.com/woowacourse/javascript-lotto/pull/142#issuecomment-1059778168) 23 | 2. Web API를 활용할 수 있다면 굳이 CustonEvent는 사용할 필요가 없다고 생각합니다. - [PR 136](https://github.com/woowacourse/javascript-lotto/pull/136#issuecomment-1059902711) 24 | 25 |
26 | 27 | ### 구조 28 | 29 | - 구조가 맡는 역할이 모호해지면, 코드가 길어지고 가독성이 떨어진다. - [PR 123](https://github.com/woowacourse/javascript-lotto/pull/123#pullrequestreview-900065374) 30 | 31 |
32 | 33 | ### CSS 34 | 35 | - 앱에서 중복적으로 사용되는 CSS는 변수로 [분리](https://developer.mozilla.org/ko/docs/Web/CSS/:root)해주자 - [PR 142](https://github.com/woowacourse/javascript-lotto/pull/142#discussion_r820071137) 36 | - 버튼이 disable 상태일 때 `cursor: not-allowed;`를 적용해줘야할까? - [PR 142](https://github.com/woowacourse/javascript-lotto/pull/142#discussion_r820072764) \* 37 | - `cursor: not-allowed;`을 적용했을 시 디자인적인 문제가 있다. 38 | - 대부분의 서비스에서 찾아볼 수 없다. 39 | - bootstrap에서 위 같은 문제로 삭제된 [전례](https://github.com/twbs/bootstrap/issues/22222)가 있다. 40 | - 길이에서 `0px`을 표기하고 싶을 때 `0`을 사용하자. 이는 선택이지만 [권장사항](https://www.w3.org/TR/css-values-3/#lengths) - [PR 142](https://github.com/woowacourse/javascript-lotto/pull/142#discussion_r820072820) 41 | - `scroll-behavior: smooth;` 딱 한 줄만으로 [부드러운 스크롤](https://gomakethings.com/smooth-scrolling-links-with-only-css/)을 구현할 수 있습니다. - [PR 140](https://github.com/woowacourse/javascript-lotto/pull/140#pullrequestreview-901771377) \* 42 | - CSS에도 [방법론](https://whales.tistory.com/33)이 존재한다. - [PR 136](https://github.com/woowacourse/javascript-lotto/pull/136#issuecomment-1060243153) \* 43 | - 색상 표기는 통일성이 있게 해보자. - [PR 120](https://github.com/woowacourse/javascript-lotto/pull/120#discussion_r820346138) 44 | 45 | ```css 46 | background-color: white; 47 | background-color: #00bcd4; 48 | /* 색상 이름, 16진수로 작성하는 방법 .. 두 가지의 방법이 혼용되어 사용되고 있었음. */ 49 | ``` 50 | 51 |
52 | 53 | ### HTML 54 | 55 | - [`fieldset`](https://developer.mozilla.org/ko/docs/Web/HTML/Element/fieldset)을 사용하면 여러 `input`과 하나의 `label`을 묶어줄 수 있습니다. 이는 하나의 `form`안에 두는 것이 일반적입니다. - [PR 146](https://github.com/woowacourse/javascript-lotto/pull/146#discussion_r820173289) \* 56 | - template가 반복될 때 `map`을 사용해주자. - [PR 146](https://github.com/woowacourse/javascript-lotto/pull/146#discussion_r820173527) 57 | - label이 가르키는 input과 그 label은 가까이 두자. - [PR 156](https://github.com/woowacourse/javascript-lotto/pull/156#commitcomment-68209662) 58 | - (_DOM은 HTML에서 그려야할까 vs JS에서 그려야할까_) - [PR 120](https://github.com/woowacourse/javascript-lotto/pull/120#discussion_r817445951) 59 | - 작성자의 입장 : 유저의 액션에 의해 DOM을 추가하거나 변경하는 것은 JS가 담당,유저의 액션과 관계없이 항상 존재해야하는 DOM 요소는 HTML에서 작성해야 한다고 생각 60 | - 리뷰어의 입장 : JS자체가 DOM을 조작하기 위해 사용하는 언어인데 JS에서 DOM조작을 최소화하기보다는 좀 더 효과적으로 JS를 활용해 DOM을 조작하는게 좋은 것 같다고 생각 61 | 62 |
63 | 64 | ### Eslint 65 | 66 | - [consistent-return](https://eslint.org/docs/rules/consistent-return)규칙은 함수 안에서 일관되게 `return`을 해주도록 강제한다. 하지만 `return`을 해주지 않아도 되는 상황에서 규칙때문에 억지로 `return`해주는 경우가 있을 수 있다. [코드 분기에 따라 다른 동작을 하게 하려면 이 규칙을 꺼주는 것이 안전하다.](https://eslint.org/docs/rules/consistent-return#when-not-to-use-it) - [PR 142](https://github.com/woowacourse/javascript-lotto/pull/142#discussion_r820124484) \* 67 | 68 |
69 | 70 | ### Class 71 | 72 | - 생성자에서 해줘도 될 일을 굳이 함수로 빼줄 필요는 없다. - [PR 146](https://github.com/woowacourse/javascript-lotto/pull/146#issuecomment-1060542912) 73 | 74 |
75 | 76 | ### Function 77 | 78 | - Boolean을 `return`할 경우 조건을 `return`할 수 있는지 확인해주자. - [PR 142](https://github.com/woowacourse/javascript-lotto/pull/142#discussion_r820095323) 79 | - 두개 이상의 인자를 받을 경우 객체 형태로 넣어주세요. [인자는 2개 이하가 이상적입니다.](https://github.com/qkraudghgh/clean-code-javascript-ko#%ED%95%A8%EC%88%98functions) - [PR 140](https://github.com/woowacourse/javascript-lotto/pull/140#discussion_r820068616) 80 | - 매개변수가 많으면 가독성 측면에서 좋지 않고, 유지보수할 때 실수를 유발할 수 있다. - [PR 136](https://github.com/woowacourse/javascript-lotto/pull/136#discussion_r820088741) 81 | 82 |
83 | 84 | ### 고차 함수 85 | 86 | - 객체의 프로퍼티를 이용하여 계산할 때 [`reduce`](https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Array/Reduce)를 사용해보자. - [PR 142](https://github.com/woowacourse/javascript-lotto/pull/142#discussion_r820230362) \* 87 | 88 | - `entries()`의 사용 89 | 90 | ```js 91 | //PR 142 92 | //before 93 | const winningMoney = 94 | this.#winningCounts.fifthPlace * LOTTO.FIFTH_PRIZE + 95 | this.#winningCounts.fourthPlace * LOTTO.FOURTH_PRIZE + 96 | this.#winningCounts.thirdPlace * LOTTO.THIRD_PRIZE + 97 | this.#winningCounts.secondPlace * LOTTO.SECOND_PRIZE + 98 | this.#winningCounts.firstPlace * LOTTO.FIRST_PRIZE; 99 | 100 | //after 101 | const PRIZE_BY_PLACE = { 102 | fifthPlace: LOTTO.FIFTH_PRIZE, 103 | fourthPlace: LOTTO.FOURTH_PRIZE, 104 | thirdPlace: LOTTO.THIRD_PRIZE, 105 | secondPlace: LOTTO.SECOND_PRIZE, 106 | firstPlace: LOTTO.FIRST_PRIZE, 107 | }; 108 | 109 | //entries()는 key-value쌍 배열을 반환 110 | const winningMoney = Object.entries(this.#winningCounts).reduce( 111 | (previous, [key, value]) => { 112 | return previous + value * PRIZE_BY_PLACE[key]; 113 | }, 114 | 0 115 | ); 116 | ``` 117 | 118 | - `keys()`의 사용 119 | 120 | ```js 121 | //PR 140 122 | //before 123 | const lastValue = 124 | this.prizeCount.first * PRIZE_MONEY.FIRST + 125 | this.prizeCount.second * PRIZE_MONEY.SECOND + 126 | this.prizeCount.third * PRIZE_MONEY.THIRD + 127 | this.prizeCount.fourth * PRIZE_MONEY.FOURTH + 128 | this.prizeCount.fifth * PRIZE_MONEY.FIFTH; 129 | 130 | //after 131 | const lastValue = Object.keys(this.prizeCount).reduce( 132 | (money, rankKey) => 133 | this.prizeCount[rankKey] * PRIZE_MONEY[rankKey.toUpperCase()] + money, 134 | 0 135 | ); 136 | ``` 137 | 138 |
139 | 140 | ### 유용한 문법 141 | 142 | - `try-catch`의 장점은 오류가 있을 경우 즉시 실행을 멈춘다는 점과 에러 핸들링을 할 수 있다는 점이 있다. - [PR 136](https://github.com/woowacourse/javascript-lotto/pull/136#discussion_r820396206) 143 | - `Array.from`을 통해 `map`의 작업도 동시에 할 수 있다. - [PR 136](https://github.com/woowacourse/javascript-lotto/pull/136#discussion_r820091000) \* 144 | 145 | ```js 146 | console.log(Array.from([1, 2, 3], x => x + x)); 147 | // expected output: Array [2, 4, 6] 148 | ``` 149 | 150 |
151 | 152 | ### Array 153 | 154 | - 배열의 마지막 값은 숫자로 명시하지 말고, `length - 1`을 사용해보자 - [PR 142](https://github.com/woowacourse/javascript-lotto/pull/142#discussion_r820108518) 155 | 156 |
157 | 158 | ### Object 159 | 160 | - 객체를 복사하는 방법에는 `JSON.parse(JSON.stringify(object))`가 있다. 주의할 점은 객체안의 메서드는 복사되지 않는다. - [PR 156](https://github.com/woowacourse/javascript-lotto/pull/156#discussion_r820578491) \* 161 | 162 |
163 | 164 | ### 변수 165 | 166 | - 변수가 많으면 관리포인트가 많아진다. 공통으로 관리할 수 있는 부분은 배열과 객체를 이용하자. - [PR 136](https://github.com/woowacourse/javascript-lotto/pull/136#discussion_r820089422) 167 | 168 |
169 | 170 | ### 이벤트 핸들러 171 | 172 | - 비슷한 방식으로 여러 요소의 이벤트를 처리해야할 때 [이벤트 위임](https://ko.javascript.info/event-delegation)을 사용해보자. - [PR 146](https://github.com/woowacourse/javascript-lotto/pull/146#discussion_r820577052) \* 173 | 174 |
175 | 176 | ### 네이밍 177 | 178 | - `init`은 가장 처음을 위한 초기화에 가깝다. (reset이 아님) - [PR 156](https://github.com/woowacourse/javascript-lotto/pull/156#discussion_r820520699) \* 179 | - `submit`은 `form`에 있는 데이터를 제출한다는 의미로 많이 사용된다. toggle 같은 함수에서 붙일 필요가 없다. (예시 - `submitLottoToggle()`라는 이름의 함수) - [PR 120](https://github.com/woowacourse/javascript-lotto/pull/120#discussion_r817495192) 180 | 181 |
182 | 183 | ### 디렉토리 184 | 185 | - 하나의 `index.js`에 많은 `import`를 한다면 가독성이 떨어질 수 있다. 만약 여러개의 View를 하나의 `index.js`에서 `import`하고 있다면 View 디렉토리에 새로운 `index.js`를 만들고 이를 `import`해주는 방식을 고려해보자. - [PR 136](https://github.com/woowacourse/javascript-lotto/pull/136#discussion_r820087835) \* 186 | 187 |
188 | 189 | ### 테스트 190 | 191 | - 랜덤을 위한 테스트에는 `mock`을 사용해보자. - [PR 140](https://github.com/woowacourse/javascript-lotto/pull/140#issuecomment-1060418153) \* 192 | 193 |
194 | 195 | ### Commit 196 | 197 | - (commitlint를 사용했을 때의 gitmoji) gitmoji 자체에 type의 의미를 담기 때문에 별도의 type text를 쓰지 않아도 된다. 😮 - [PR 123](https://github.com/woowacourse/javascript-lotto/pull/123#issuecomment-1059093455) \* 198 | - 좋은 커밋 메시지에는 what과 why가 들어가 있어야 한다. - [PR 123](https://github.com/woowacourse/javascript-lotto/pull/123#pullrequestreview-901594861) \* 199 | - title에 what(무엇을 작성했는 지), 내용에 why(왜 이 커밋을 작성했는 지) 200 | 201 |
202 | 203 | ### PR 리뷰 잘 받는 팁 204 | 205 | - 커밋에서 피드백이 잘 반영되었다는 것을 리뷰어님께 알려드리고 싶다면, comment에 커밋 넘버를 남기자. - [PR 123](https://github.com/woowacourse/javascript-lotto/pull/123#pullrequestreview-901594861) 206 | -------------------------------------------------------------------------------- /racing-car/marco.md: -------------------------------------------------------------------------------- 1 | # [마르코 2.25] 자동차게임 미션 리뷰 분석 2 | 3 | ## 컨벤션 4 | 5 | - 반드시 그래야 하는 것은 아니지만, 업계 전반에 class명의 첫글자는 대문자로 표기하는 관행이 있다. 6 | - 예를 들어, `isCarNameBlank`와 같이 `is` 라는 prefix가 붙은 네이밍은 car name이 비어있는지 여부에 따라서 true나 false를 반환한다고 예상된다. 7 | 8 | ## 클린코드 9 | 10 | - 공통 메서드는 의도가 드러난 한 가지 일만 수행하도록 하는 게 유리하다. 한 함수에서 여러 일을 동시에 하지 않도록 분리한다. 11 | 12 | ## 자료구조를 활용하여 중복 제거 13 | 14 | - Set 자료구조를 통해 중복을 제거하고, 원본의 길이와 비교하면 해당 배열에 중복값이 있는지 쉽게 확인할 수 있다. 15 | 16 | ```jsx 17 | const validateDuplicateCarName = names => { 18 | return new Set(names).size === names.length; 19 | ``` 20 | 21 | ## innerHTML 이슈 22 | 23 | - innerHTML을 사용하면 기존에 매핑된 이벤트가 전부 소멸된다. 매핑된 이벤트를 소멸하지 않고 유지하면서 DOM에 추가하기 위해 insertAdjacentHTML 메서드를 사용해볼 수 있다. 24 | - insertAdjacentHTML 추천 25 | 26 | ## 단축평가 27 | 28 | - or로 이어진 조건들을 some 메서드로 리팩토링 29 | 30 | ```jsx 31 | // before 32 | static isInvalidInput(input) { 33 | if(this.isA(input)) { 34 | return true; 35 | } 36 | if(this.isB(input)) { 37 | return true; 38 | } 39 | if(this.isC(input)) { 40 | return true; 41 | } 42 | return false 43 | } 44 | ``` 45 | 46 | ```jsx 47 | // refactor 1 - 단순화 48 | return (this.isA(input) || this.isB(input) || this.isC(input)); 49 | ``` 50 | 51 | ```jsx 52 | // refactor 2 - or로 이어진 조건들을 some메서드로 리팩토링 53 | return ['isA', 'isB', 'isC'].some(method => this[method](input)); 54 | ``` 55 | 56 | - and로 이어진 조건들을 every 메서드로 리팩토링 57 | 58 | ```jsx 59 | // before 60 | static isInvalidInput(input) { 61 | if(this.isA(input) && this.isB(input) && this.isC(input)) { 62 | return true; 63 | } 64 | return false 65 | } 66 | ``` 67 | 68 | ```jsx 69 | // refactor 1 - 단순화 70 | return (this.isA(input) && this.isB(input) && this.isC(input)); 71 | ``` 72 | 73 | ```jsx 74 | // refactor 2 - and로 이어진 조건들을 some메서드로 리팩토링 75 | return ['isA', 'isB', 'isC'].every(method => this[method](input)); 76 | ``` 77 | 78 | ## 테스트코드 79 | 80 | - 본질적인 방어는 본진에서 해두고, 테스트는 ‘예방차원'에서 안전장치를 마련하는 것이다. 81 | - 테스트코드에서도 자주 쓰이는 string은 상수로 관리하면 휴먼에러를 방지할 수 있다. 82 | - 전역에서 사용하는 `custom commands` 의 경우 문서에서 `support` 폴더에 정의하길 권장한다. `support/commands.js` 파일에 정의하면 좋다. 83 | - {enter} 하면 enter키가 눌리는 테스트코드(cypress) 84 | 85 | ```jsx 86 | cy.get('.input-section').type('east,west,south,north{enter}'); 87 | // .input-section 클래스 엘리먼트에 'east,west,south,north'를 입력하고 enter를 누른다. 88 | ``` 89 | 90 | ## 설계 91 | 92 | - model과 controller를 분리했음에도 controller가 model에 직접 개입하는 것은 바람직하지 않다. model에 값을 던져주고, model이 해당 동작을 수행하게끔 하는 편이 좋다. 그렇지 않으면, 역할 개입에 의해 예상치 못한 문제가 발생하기도 하기 때문이다. 이 때문에 이런 사례가 발생하지 않게 하기 위해 미리부터 model을 캡슐화하는 것도 좋은 전략이다. 93 | - model의 상태를 다른 곳에서 변경하기 보다는 해당 model에게 메세지를 보내서(model의 메서드 호출) model이 직접 스스로 자신의 상태를 변경하는 방식(model의 메서드 내부에 상태 변경 코드)을 추천한다. 상태를 외부에서 직접 변경하는 방식보다는 객체가 스스로 상태를 책임지는 방식이 캡슐화가 더 잘됐다고 할 수 있고, 코드의 의도를 파악하기도 더 좋다. 94 | - 헐리우드 원칙 [https://johngrib.github.io/wiki/hollywood-principle/](https://johngrib.github.io/wiki/hollywood-principle/) 95 | - DOM 요소가 모두 View에 있도록 하기 위해 DOM에서 이벤트 부착을 하였고, 이벤트 발동 시 실행할 콜백함수를 인자로 받는다. 해당 인자는 Controller에서 전달한 것이므로, 이벤트의 콜백함수는 Controller에서 실행된다. 이처럼 함수를 인자로 넘기는 것은 자바스크립트는 함수가 일급객체이기 때문에 가능한 것이며, 이는 할리우드 원칙 패턴과 유사하다. 96 | 97 | 헐리우드 원칙을 활용하면 "의존성 부패(dependency rot)"를 방지할 수 있습니다. 어떤 고수준 구성요소가 저수준 구성요소에 의존하고, 그 저수준 구성요소는 다시 고수준 구성요소에 의존하고, 그 고수준 구성요소는 다시 또 다른 구성요소에 의존하고, 그 다른 구성요소는 또 저수준 구성요소에 의존하는 것과 같은 식으로 의존성이 복잡하게 꼬여있는 것을 의존성 부패라고 부릅니다. 이렇게 의존성이 부패되면 시스템이 어떤 식으로 디자인된 것인지 거의 아무도 알아볼 수 없게 되죠. 98 | 99 | 헐리우드 원칙을 사용하면, 저수준 구성요소에서 시스템에 접속을 할 수는 있지만, 언제 어떤 식으로 그 구성요소들을 사용할지는 고수준 구성요소에서 결정하게 됩니다. 즉, 고수준 구성요소에서 저수준 구성요소에게 "먼저 연락하지 마세요. 제가 먼저 연락 드리겠습니다"라고 얘기를 하는 것과 같죠. 100 | 101 | - controller가 다른 객체인 view의 멤버변수(element)에 직접 접근해서 eventListener를 달으는 것이 올바를까? 102 | - 객체 지향 프로그래밍에서 객체의 데이터는 객체의 외부에서 직접적으로 접근하는 것을 막는다. 객체의 데이터를 외부에서 마음대로 읽고 변경할 경우 객체의 무결성이 깨질 수 있기 때문이다 . 객체의 프로퍼티를 객체 바깥에서 직접 조작하는 행위는 데이터의 유지 보수성을 해치는 주요 원인이다. 103 | - 따라서 view의 멤버변수에 접근하여 eventListener를 다는 메서드는 view 내부에서 해주는 것이 적절하다고 보인다. 또한, 다른 객체의 멤버변수에 접근해야할 경우, getter나 setter 등을 사용하는 것이 적절하다. 104 | 105 | ## 바로 return하는 함수 관련 106 | 107 | - 바로 return을 하는 함수라면 () 안에 return을 내포하고 있어 다음과 같이 리팩토링이 가능하다. 108 | 109 | ```jsx 110 | // before 111 | const func = input => { 112 | return input.split(',').map(input => input.trim()) 113 | }; 114 | ``` 115 | 116 | ```jsx 117 | // refactor 118 | const func = input => (input.split(',').map(input => input.trim())) 119 | ``` 120 | 121 | ## DOM 122 | 123 | - selector 탐색을 root에서부터 시작하는 경우가 많은데, 이럴 경우 탐색하면서 매칭되는 첫 번째 element를 반환한다. 이런 방식으로는 프로젝트 덩치가 커질수록 다음과 같은 두 가지 이슈가 예상된다. 124 | 1. 모든 element를 root에서부터 탐색하기 때무에 element 깊이가 깊어질수록 탐색 속도 저하 125 | 2. 유일한 className이 아니면 의도하지 않은 element 반환 126 | 127 | 이러한 이슈를 방지하기 위해, document root부터 탐색이 아니라 `특정 element` 부터 탐색하는 방법을 고민해보자. 128 | 129 | ```jsx 130 | element = baseElement.querySelector(selector); 131 | ``` 132 | 133 | 134 | ## forEach를 map으로 리팩토링 135 | 136 | - DOM에 렌더링을 하기 위한 HTML String을 준비할 때, 데이터 배열을 돌면서 HTML string을 만드는 경우 forEach를 쓰기도 하는데, forEach 대신 map 메서드를 쓸 수 있다. 137 | 138 | ```jsx 139 | // before : forEach메서드 사용 140 | renderSomething(things) { 141 | let template = ''; 142 | 143 | things.forEach((thing) => { 144 | template += somethingTemplate(thing.data); 145 | }); 146 | 147 | this.$targetElement.insertAdjacentHTML('beforeend', template); 148 | } 149 | ``` 150 | 151 | ```jsx 152 | // after: map으로 리팩토링 153 | renderSomething(things) { 154 | const template = things 155 | .map((thing) => somethingTemplate(thing.data)) 156 | .join(''); 157 | this.$targetElement.insertAdjacentHTML('beforeend', template); 158 | } 159 | ``` 160 | 161 | ## class 162 | 163 | - 객체를 생성할 때, class와 생성자 함수 중 어떤 것을 사용하는 것이 더 좋을까? class는 결국 syntatic sugar이긴 하다. 팀 내에서는 섞어서 쓰고 있다. 요즘에는 class가 `private` 이나 `상속` 등 사용 측면에서 유리한(편한) 면이 더 있다고 생각해서, 팀내에서 class를 더 선호하기도 하다. 164 | 165 | ## 파일 분리 166 | 167 | - 파일을 분리하되 응집력있게 모아두면 괜찮다. 함수가 쪼개지고 파일까지 쪼개지다 보면 오히려 역할이나 배경, 의도를 파악하기가 어려울 때가 종종 있다. (파일도, 함수도, 모두 마찬가지이다.) 168 | 169 | ## 유효성 검사 170 | 171 | - 데이터에 대한 유효성 검증방법은 데이터 관련 `Model` 에서 제공하고, 검증은 `Controller` 에서 하는 방법도 있을 것 같다. 172 | 173 | ## 고민 174 | 175 | - **개발이란 게 정답이 없다.** 교육기간에 너무 베스트 프랙티스나 정답을 따라서 하기보다는, 이것저것 삽질하면서 베스트 프랙티스로 향하거나 본인만의 답을 찾는 게 가장 중요하다. 고민하는 근력이야말로 현업에서 가장 중요하다. 176 | 177 | ## sort 메서드와 immutable 178 | 179 | - 복사본 없이 sort 메서드로 모델의 데이터를 직접적으로 정렬시키는 것은 주의해야 한다. immutable하게 모델을 유지하는 것을 추천한다. 프로그램의 복잡도가 올라갈수록 `직접적으로 객체를 변경하는 것`은 의도치 않은 사이드이펙트를 가져올 수 있기 때문이다. 180 | 181 | ```jsx 182 | // before 183 | function sortCars(cars) { 184 | return cars.sort((a,b) => b.position - a.position); 185 | } 186 | ``` 187 | 188 | ```jsx 189 | // after - 복사본 만들어서 sort 190 | function sortCars(cars) { 191 | const carsCopy = [...cars]; 192 | return carsCopy.sort((a,b) => b.position - a.position); 193 | } 194 | ``` 195 | 196 | ## 올바른 변수 이름 짓는 법 197 | 198 | [https://youtu.be/ZtkIwGZZAq8](https://youtu.be/ZtkIwGZZAq8) 199 | 200 | - 간결하면서도 의미가 전달되는 것이 중요하다. 201 | - 변수 이름에는 동사를 넣지 않는다. 202 | - 단수형, 복수형을 구분한다. 변수의 단수형에는 관사(a, the)를 넣지 않는다. 복수형은 ‘s’를 뒤에 붙인다(또는 list로 하는 경우도 있다). 203 | - 전치사는 최대한 생략한다. `nubmerOfUsers` 같은 변수명보다는 더 간결히 `userCount`가 더 낫다. 또는 `number`를 앞에 쓰고 싶다 한다면, 약간 문법 파괴를 하더라도 `numUsers` 같이 짧게 쓰는 경우도 있다. 아무튼 `numberOfUsers` 처럼 전치사를 넣는 네이밍은 피하는 것이 좋다. 204 | - 좋은 오픈소스의 코드들을 보면서 변수명이 어떻게 지어졌는지 보고 익혀보자. 205 | 206 | ## 명령형과 선언형 207 | 208 | - 명령형(imperative, HOW) : “어떻게 구현하는가"를 설명한다. 어떤 방법으로 해야 하는지를 나타낸다. 어떤 동작을 할 것인지, 수행할 명령을 순서대로 써놓은 것. 209 | - 선언형(declartive, WHAT) : “무엇"이 일어나는지, 무엇과 같은지를 설명한다. 210 | 211 | ## 상수화 212 | 213 | - 어디까지 상수화하는 것이 좋은지에 대해 여러 사람들의 생각을 묻고 고민해보자. 상수화하면 어떤 이점이 있을까? 214 | - 상수화 시, ID의 `#`, class의 `.` 를 이런 함수를 만들어서 편하게 사용할 수도 있다(전역 프로토타입 객체를 건드리는 것은 권장하는 방법까진 아니나, 이 정도 미션에서는 가능할 수도 있 다.). 215 | 216 | ```jsx 217 | const DOM = { 218 | CAR_NAME_INPUT = 'car_name_input', 219 | } 220 | 221 | String.prototype.toID = function() { 222 | return `#${this}` 223 | } 224 | 225 | console.log(DOM.CAR_NAME_INPUT.todID()); // #car_name_input 226 | ``` 227 | 228 | 229 | 230 | ## prettier 환경 설정 231 | 232 | - trailingComma의 값에 ‘all’ 설정을 하게 되면, 코드리뷰 시 실제로 코드가 변경된 부분만 표시가 될 수 있으므로, 코드리뷰할 때 편하다는 장점이 있다. 233 | 234 | ## 은닉화 235 | 236 | - class의 속성이나 메서드에 해쉬 # prefix를 추가하여 private class fileds를 선언할 수 있다. 다른 클래스에게 노출하지 않아야 하는 것임을 알 수 있게 한다. 237 | 238 | ## jsDoc 239 | 240 | - **JSDoc을 사용하여 자바스크립트에 타입 힌트 제공하기** 241 | - [https://velog.io/@yijaee/JSDoc을-사용해-JavaScript-파일-문서화하기](https://velog.io/@yijaee/JSDoc%EC%9D%84-%EC%82%AC%EC%9A%A9%ED%95%B4-JavaScript-%ED%8C%8C%EC%9D%BC-%EB%AC%B8%EC%84%9C%ED%99%94%ED%95%98%EA%B8%B0) 242 | - [https://jsdoc.app/](https://jsdoc.app/) 243 | 244 | ## class 245 | 246 | - static 메서드만 존재하는 class가 class로서 의미가 있을까? 객체로 만들어도 충분할 수 있다. class는 어디까지나 “인스턴스를 생성하기 위한 틀 또는 추상적 객체"라는 원래의 목적에 맞게 쓰는 것이 좋다. 왜냐하면, static 멤버로만 구성된 클래스는 실제로 new 연산자와 함께 호출하여 인스턴스를 생성할 일이 없으니, ‘공통 속성을 뽑아낸 추상적 범주'라기보다는 그 자체로 특정 역할을 부여한 “구체적 객체"와 다름없다고 볼 수 있다. 247 | 248 | ## 유틸 함수 249 | 250 | - 범용성이 있는 함수는 유틸 폴더로 옮기는 것이 낫다. 유틸에는 보통 도메인에 대한 정보가 없다. 251 | 252 | ## 드모르간 법칙으로 if문 없애기 253 | 254 | - 조건 충족시 false, 불충족시 true를 반환하는 함수라면 이런 식으로 if문을 없앨 수 있다. 255 | 256 | ```jsx 257 | // (1) 258 | if(!conditionA || conditionB) return false 259 | return true 260 | 261 | // (2) 262 | if (conditionA && !conditionB) return true 263 | return false 264 | 265 | // (3) 266 | return (conditionA && !conditionB) 267 | ``` 268 | 269 | ## 입력된 값들에 빈 값이 있는지 체크 270 | 271 | ```jsx 272 | hasSpaceInName = (names) => { 273 | names.some((name) => Array.from(anme).some((ch) => ch.match(/ /))); 274 | } 275 | ``` 276 | 277 | ## 시맨틱 태그 278 | 279 | - 웹접근성을 위해 div 대신에 section이나 article을 사용하여 시맨틱하게 구현도 고려하기 280 | - section 안에 타이틀 태그를 넣어서 스크린리더기가 해당 섹션의 설명을 읽을 수 있도록 도와줄 수 있다. 타이틀 태그를 화면에 표시하고 싶지 않다면 hidden 어트리뷰트를 사용한다. 281 | 282 | 283 | ## 기타 284 | 285 | - js 파일을 마지막에 배치한 이유 286 | - html을 읽고 DOM 트리가 모두 생성된 후에 js가 실행되어야 오류가 발생하지 않는다. 287 | 288 | 289 | ## null, “”(empty), undefined 290 | 291 | - null, “”(empty), undefined는 구분된다. 292 | - `null` 293 | - object타입 294 | - 변수의 선언과 데이터가 없는 상태로서 메모리의 변수의 위치가 정해지고 공간은 텅 빈 상태 295 | - null은 원시값(Primitive Type) 중 하나로, 어떤 값이 의도적으로 비어있음을 표현한다. undefined는 값이 지정되지 않은 경우를 의미하지만, null의 경우에는 해당 변수가 어떤 객체도 가리키고 있지 않다는 것을 의미 296 | - `""` (empty) 297 | - string 타입 298 | - 메모리의 변수의 위치가 정해지고 공간에 빈 문자열 하나만 있는 상태 299 | - `undefined` 300 | - undefined 타입 301 | - 선언한 후에 값을 할당하지 않은 변수나 값이 주어지지 않은 인수에 자동으로 할당됨 302 | -------------------------------------------------------------------------------- /vending-machine/README.md: -------------------------------------------------------------------------------- 1 | ## vending-machine pair 2 | --- 3 | 4 | 스크린샷 2022-03-22 오전 11 09 29 5 | 6 | ## 담당 페어 7 | 8 | ### 꼬재 9 | - 소피아, 콤피 10 | - 위니, 티거 11 | - 동키콩, 블링, 록바 12 | 13 | ### 마르코 14 | - 민초, 무비 15 | - 엘버, 비녀 16 | - 태태, 빅터 17 | - 유세지, 콜라 18 | 19 | ### 록바 20 | - 병민, 나인 21 | - 자스민, 호프 22 | - 준찌, 셀리 23 | - 해리 24 | 25 | ### 무비 26 | - 후이, 꼬재 27 | - 온스타, 밧드 28 | - 도리, 우디 29 | - 코이 30 | 31 | ### 호프 32 | - 안, 우연 33 | - 하리, 시지프 34 | - 돔하디, 마르코 35 | - 아놀드 36 | -------------------------------------------------------------------------------- /youtube/README.md: -------------------------------------------------------------------------------- 1 | # 나만의 유튜브 강의실 미션 2 | ![image](https://user-images.githubusercontent.com/59413128/158216977-521c6d88-1510-4f00-842b-c061862bab60.png) 3 | 4 | ## 담당 페어 코드 5 | ### 마르코 6 | - 마르코, 위니 7 | - 병민, 해리 8 | - 코카콜라, 후이 9 | - 샐리 10 | 11 | ### 호프 12 | - 호프, 콤피 13 | - 돔하디, 블링 14 | - 밧드, 나인 15 | - 안 16 | 17 | ### 록바 18 | - 록바, 앨버 19 | - 태태, 결 20 | - 자스민, 동키콩 21 | - 도리, 소피아 22 | 23 | ### 무비 24 | - 무비, 티거 25 | - 우디, 시지프 26 | - 하리, 민초 27 | - 비녀, 코이 28 | 29 | ### 꼬재 30 | - 꼬재, 유세지 31 | - 우연, 아놀드 32 | - 빅터, 준찌 33 | - 온스타 34 | -------------------------------------------------------------------------------- /youtube/step1/hope.md: -------------------------------------------------------------------------------- 1 | 2 | # 구조 분석 3 | 4 | ## [#95](https://github.com/woowacourse/javascript-youtube-classroom/pull/95) & [#96](https://github.com/woowacourse/javascript-youtube-classroom/pull/96) 콤피 & 호프 5 | 6 | 1. Display 에서 구독 할 Store 저장 7 | 2. Display 내 이벤트 핸들링 시 필요 시 Store 에 Dispatch 하여 상태 변경 8 | 3. Store 에서 Dispatch 호출하여 상태 변경 및 비즈니스 로직 처리 9 | 4. Store 에서 상태 변경시, 구독 중인 Display 내 함수 호출 10 | 11 |
12 | 13 | 14 | ## [#97](https://github.com/woowacourse/javascript-youtube-classroom/pull/97) & [#78](https://github.com/woowacourse/javascript-youtube-classroom/pull/78) 블링 & 돔하디 15 | 16 | ![](https://user-images.githubusercontent.com/43166681/157829860-726c03d8-5ace-47eb-83ed-085680d18c13.png) 17 | 18 | - 위와 같은 구조였는데 피드백 이후 변경됨 19 | - 중간의 검색 요청 전달, 저장 요청 전달하는 Sender class 레이어를 제거하여 UI 에서 도메인으로 직접 요청 20 | 21 | ### View 22 | - 이벤트 바인딩 및 처리를 담당한다. 23 | - 이벤트 처리시, 생성자 인자로 받은 search 와 saveVideos 도메인에 요청을 보내고 값을 리턴받아 현재 화면에 그려준다. 24 | 25 | 26 | ### Domain 1. Search 27 | - 키워드와 nextPageToken 을 멤버변수로 저장한다. 28 | - search 요청을 받으면, api 요청을 보낸 후 , 반환된 nextPageToken 와 사용자가 입력한 keyword 를 저장한다. 29 | - 반환된 video Items 는 한번 가공 후, 리턴해준다. 30 | 31 | ```javascript 32 | #getVideoObjectArray(items, savedVideos) { 33 | if (items.length === 0) throw new Error(ERROR_MESSAGES.NO_RESULT); 34 | return items.map((item) => { 35 | const { snippet, id } = item; 36 | return { 37 | videoId: id.videoId, 38 | thumbnail: snippet.thumbnails.medium.url, 39 | title: snippet.title, 40 | channelTitle: snippet.channelTitle, 41 | publishedAt: snippet.publishedAt, 42 | isSaved: !!savedVideos.includes(id.videoId), 43 | }; 44 | }); 45 | } 46 | ``` 47 | 48 | - 위와 같이 items 를 가공한다. 49 | - savedVideo 는 로컬스토리지에서 가져온다. 50 | - 가공해줌으로써, 뷰에서 처리하기가 쉬워진다. 51 | 52 | ### Domain 2. Storage 53 | - localStorage 에 데이터를 save 하고 get 하는 연산을 한다. 54 | 55 | 56 | 57 |
58 | 59 | 60 | ## [#102](https://github.com/woowacourse/javascript-youtube-classroom/pull/102) & [#91](https://github.com/woowacourse/javascript-youtube-classroom/pull/91) 밧드 & 나인 61 | 62 | 63 | ### View 64 | - MainView 65 | - callback 함수를 인자로 받아서 Modal Open 이벤트를 등록 할 수 있는 메서드가 있음 66 | - ModalView 67 | - Modal Close/ Search Button Click 이벤트를 등록 할 수 있는 메서드가 있음 68 | - Scroll 이벤트를 등록 할 수 있는 메서드가 있음 69 | - 비디오 저장 이벤트를 등록 할 수 있는 메서드가 있음 70 | - VideoItemView 71 | - Modal View 내부에서 실행되어 Template 반환 72 | 73 | 74 | ### Manager 75 | - 클래스로 만들지 않고 객체로만들어서 객체 메서드를 선언 76 | - ApiManager 77 | - api 요청 78 | - queryString 생성 79 | - 범용적인 메서드 80 | - StorageManager 81 | - storage 저장/ 유무 확인 82 | - VideoAPICaller 83 | - 위의 apiManager 를 사용하여 유튜브 api를 호출 84 | - 받아온 video Data를 파싱하여 리턴 85 | 86 | ### EventHandler 87 | - 위의 MainView 와 ModalView 인스턴스 생성 88 | - 여기서 이벤트 핸들러 정의하여 위의 View 내부 메서드에 연결 89 | - 따라서, Manager 와 View 를 연결시켜주는 역할 90 | 91 | 92 |
93 |
94 | 95 | # 리뷰 정리 96 | 97 | ## 1. 테스트 98 | - [#97] - localStorage 는 직접 구현하신게 아닌 브라우저 기능인데, 굳이 테스트해야할까? 99 | e2e 테스트였다면 비디오 저장 후 새로고침했을 때 저장된 비디오가 있는지, 그리고 2단계에서는 비디오가 잘 노출되는지를 테스트 할 듯하다. 100 | - jest 에서 localStorage를 테스트하려면 localStorage 를 mocking 해야하는데, 사실상 개발자가 mocking 한 localStorage를 테스트 하는게 의미가 있는가? - [바로가기](https://github.com/woowacourse/javascript-youtube-classroom/pull/97#discussion_r825449991) 101 | - (의견) 테스트 한다면 localStorage 앞 뒤에 붙어있는 데이터 가공 및 요청 함수를 테스트하는게 맞다고 생각한다. 102 | 103 | - [#97] - api 가 올바른 형태의 응답값을 리턴하는지 확인하는 테스트에 관하여 - 104 | - 특별한 로직이 있는게 아니라 바이패스인 수준이기 때문에 (예를 들어 2 * 5 = 10 처럼 2, 5를 넣으면 10이 반환되는 함수인지를 검증하는 로직같은) 굳이 테스트하지 않을 것 같다. api 응답값을 잘 가공하는지 보다, 응답값 중 localstorage에 저장되어있는 비디오라면 isSaved 가 true 일테니 localstorage 에 있는 video 갯수와 isSaved 가 true 인 video 의 갯수가 같은지 정도 테스트해볼 수 있을 것 같다. - [바로가기](https://github.com/woowacourse/javascript-youtube-classroom/pull/97#discussion_r825450961) 105 | 106 | 107 | - [#102] 테스트 파일 구조 - 테스트 파일이 분리가 되면 좋겠다는 생각을 했다. 108 | __test__ 에 모아둔다면 테스트 파일들을 모으는 것 외에는 별다른 장점이 없다. 좋은 코드 중 하나를 비슷한 역할을 하는 코드들을 모아 보기 좋게/ 유지 보수에 좋게 만드는 것이라고 생각하는데, 테스트 코드도 마찬가지로 __test__ 에 모으는 게 아니라 validator.js 와 동일한 뎁스에 validator.test.js 를 위치시키는게 좋다. - [바로가기](https://github.com/woowacourse/javascript-youtube-classroom/pull/102#discussion_r825956918) 109 | 110 | - [#91] 테스트 제목 ‘입력 없이 버튼을 눌렀다면 error를 throw한다.’,버튼을 눌렀다 가 함축한 내용이 많다. 검색어가 무엇일 때 버튼을 눌렀고 어떤 Error 가 throw 되는지 구체적으로 적는게 좋다. 111 | 그리고 입력버튼을 눌렀다 는 e2e 관점에 가깝다, 검색어가 비었을 때 검색하면 ~~~에러가 발생한다 와 같이 기능 관련된 테스트 명으로 수정하는게 좋다 - [바로가기](https://github.com/woowacourse/javascript-youtube-classroom/pull/91#discussion_r825455932) 112 | 113 | 114 | - [#96] isSameKeyword 같이 단순 문자열을 비교하는 함수를 테스트해줘야할까? 115 | 테스트코드도 비용이다. e2e 테스트나 통합테스트로 작성한 다음 다른 사이드이펙트 방지를 테스트하는 건 의미가 있을 순 있겠으나 === 만 수행하는 테스트를 해야하나? - [바로가기](https://github.com/woowacourse/javascript-youtube-classroom/pull/96#discussion_r825312522) 116 | 117 | - [#96] 웹스토리지 자체를 테스트할 필요는 없다고 생각해요. 다만 웹스토리지에서 *가져와서 데이터를 조작하는 비즈니스 로직 부는 테스트해야합니당.* 만약 비즈니스 로직이 웹스토리지가 필요하다면 디펜던시를 줄여서 모킹할 수 있는 구조로 변경 해야한다. - [바로가기](https://github.com/woowacourse/javascript-youtube-classroom/pull/96#issuecomment-1066917848) 118 | 119 | - [#96] 널리 쓰이는 유틸성 함수같은 경우, 현업에서도 있는 라이브러리를 사용할 때도 있고, 재미상 직접 만들어 쓰는 경우도 (사실 더 많음) 있다. 그럴 때 비슷한 유틸 라이브러리를 찾아서 테스트 코드를 보면서 엣지 케이스나 고려할 점들을 참고하면서 테스트 코드를 짠다. 120 | 그게 아니라 프로덕트 내에 특정 상황을 다루는 유틸 함수라면 기획이나 엣지 케이스를 직접 만들어서 최대한 범위를 맞춰야한다. 121 | 테스트 코드는 어디까지나 우리 개발자가 작성하는 거기 때문에 완벽할 순 없고, 개발자 역량과 경험에 따라 그 촘촘함도 달라진다. 이 부분은 다른 테스트코드를 많이 보면서 경험을 쌓는 것 밖에 없다고 답을 드릴 수 밖에.. - [바로가기](https://github.com/woowacourse/javascript-youtube-classroom/pull/96#issuecomment-1066917848) 122 | ## 2. 함수 123 | - [#97] - 첫 비디오 리스트 요청 함수와 이 후 무한스크롤 시 비디오 요청 함수를 분리한 상황 - (이런 기능을 Pagination 이라고한다 )보통은 서버에서 size 와 page 값을 받고 그에 맞는 데이터를 내려주는데요, page 와 size 가 다르다고 해서 함수가 여러개라면 유지보수가 힘들 것이다. [바로가기](https://github.com/woowacourse/javascript-youtube-classroom/pull/97#discussion_r825633485) 124 | 125 | - [#97] isEndOfResult 라는 함수명이 모호하다. 검색결과가 없다라는 것을 나타내야한다. 126 | 그리고 그 것을 굳이 함수로 빼야할까?[바로가기](https://github.com/woowacourse/javascript-youtube-classroom/pull/97#discussion_r825453334) 127 | ```javascript 128 | if (this.#isEndOfResult(searchResultArray)) return; 129 | ``` 130 | 131 | 132 | ## 3. 로컬스토리지 133 | - [#97] localStorage 는 기본적으로 item 이 존재하지 않을 수 있다는 가정이 있다. 그래서 예외처리가 필요하다. 134 | 예를 들어 저장된 아이템이 없는 상태에서 getSavedVideos()[2].videoId 를 하면 런타임에러가 발생해 서비스 장애상황이 생긴다. - [바로가기](https://github.com/woowacourse/javascript-youtube-classroom/pull/97#discussion_r825452143) 135 | ```javascript 136 | const savedVideos = storage.getSavedVideos() || {}; 137 | ``` 138 | 139 | 140 |
141 | 142 | ## 4. 네이밍 143 | - [#97] - Intersection Observer 실행 함수명:lastItemOfListObserver - 꼭 lastItemOfList 가 들어가야하나 싶다. 144 | 추후에 기능이 바껴서 마지막줄 전체로 observer 를 옮길 수도 있고, 요점은 어느 누구를 observe 하는것을 나타내는 것이 아니라 intersecting 됐을 때 어떤일을 하는 옵저버인지를 나타내면 좋을 것 같다. 현재 서비스로 말하자면 비디오를 더 불러오는 옵저버라는 것을 나타내면 좋겠다. 145 | 146 | - [#102] 보통 이벤트핸들러는 onXXXClick/Change, handleXXXClick/Change 147 | on/handle + {target} + {eventType} 으로 네이밍한다.- [바로가기]( https://github.com/woowacourse/javascript-youtube-classroom/pull/102#discussion_r825408409) 148 | 149 | - [#97] Manager 라는건 어떤 데이터를 관리해주는 주체에게 붙은 이름 - [바로가기](https://github.com/woowacourse/javascript-youtube-classroom/pull/91#discussion_r825457653) 150 | 151 | - [#97] is~~~() / check~~ () 는 boolean 값을 리턴예상[바로가기](https://github.com/woowacourse/javascript-youtube-classroom/pull/91#discussion_r825458109) 152 | 153 | - [#80] handleSaveButtonClick -> handleSaveVideos 로 함수명 변경 (이벤트 위임의 의도를 드러내기 위해) 원래의 로직에서 네이밍만 교체하는 것도 좋다. 154 | 마치 이벤트 위임을 위한 DOM 셀렉팅이 함수 네이밍까지 온 느낌인데, 155 | 나중에 UI가 변경되어 셀렉팅이 바뀌게되면 함수명도 변경해야할까? [바로가기](https://github.com/woowacourse/javascript-youtube-classroom/pull/80#discussion_r828758902) 156 | 157 | 158 |
159 | 160 | ## 5. 클래스 161 | 162 | - [#91] AppClass 정의 -[바로가기](https://github.com/woowacourse/javascript-youtube-classroom/pull/91#discussion_r825456612) 163 | 1. 한번도 사용하지 않을 this.EventHandler 는 왜 저장하는건가요? 164 | 2. 그리고 멤버변수도 사용하지않고, 멤버함수도 존재하지 않는 App class 는 어떤 이유에서 필요한가요? 165 | 3. 멤버변수의 이름은 소문자로 시작해야 합니다. 166 | ``` 167 | index.js 168 | HTML 템플릿 및 JavaScript의 컴포넌트를 조합하여 렌더링하고 실제 표시한다. 169 | 170 | App.js 171 | 컴포넌트를 정의하는 프로그램이다. 실제로 화면에 표시되는 내용 등은 여기에서 정의된다. 172 | 173 | ``` 174 | 175 | - [#95] 추상클래스 인스턴스화 막기 - [바로가기](https://github.com/woowacourse/javascript-youtube-classroom/pull/95#discussion_r825238162) 176 | ```javascript 177 | if (this.constructor === Display) { 178 | throw new Error('추상 클래스는 인스턴스화 할 수 없습니다.'); 179 | } 180 | 181 | class Display { 182 | constructor() { 183 | if (this.constructor === Display) { 184 | throw new Error('추상 클래스는 인스턴스화 할 수 없습니다.'); 185 | } 186 | } 187 | } 188 | class D2 extends Display {} 189 | D2.prototype.constructor = Display 190 | const d = new D2() // Error: 추상 클래스는 ... 191 | ``` 192 | - 위에서 D2는 추상클래스가 아님에도, constructor 를 덮어 씌움으로써 같은 에러를 만나게 된다. 193 | - [new.target - JavaScript | MDN](https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Operators/new.target) 속성을 이용할 수 있다. 194 | - new target 속성은 함수 또는 생성자가 new 연산자를 사용하여 호출되었는지 확인한다. 195 | - 일반함수 호출에서 new.target 은 undefined 196 | ```javascript 197 | if (new.target.name === Display.name) { 198 | throw new Error('추상 클래스는 인스턴스화 할 수 없습니다.'); 199 | } 200 | ``` 201 | 202 | 203 | 204 |
205 | 206 | ## 6. 그 외 207 | ### 스크롤 이벤트 시 offset 지정 208 | - [#91] 보통 스크롤 값들로 무한스크롤을 구현할 때는 정확히 하단에 도착했을 때가 아닌 offset 을 준다. 209 | - `scrollTop + clientHeight >= scrollHeight - ${SCROLL_OFFSET}` 210 | - [바로가기](https://github.com/woowacourse/javascript-youtube-classroom/pull/91#discussion_r825458566) 211 | 212 | 213 | ### 비동기 214 | - [#91] 먼저, javascript는 동기적 언어. 싱글 스레드로 한 번에 하나의 작업을 수행한다. 하지만 이런 방식은 웹 페이지에 치명적이다. 동기적인 방식으로 하면 하나의 동작이 끝날 때까지 다음 동작을 수행하지 못하게 된다! (엄청 버벅 거리거나 로딩이 오래 걸린다) 그래서 비동기를 통해 요청을 끝날 때까지 기다리지 않고 바로 다음 동작이 실행될 수 있도록 한다. - [바로가기](https://github.com/woowacourse/javascript-youtube-classroom/pull/91#issuecomment-1067049781) 215 | 216 | 217 | ### reduce 로 객체 생성하기 218 | - [#96] - [바로가기](https://github.com/woowacourse/javascript-youtube-classroom/pull/96#discussion_r825314353) 219 | ```javascript 220 | const output = Object.entries(origin).reduce( 221 | (acc, [key, value]) => ({ ...acc, 222 | [key]: value.substr(1), 223 | }), 224 | {}, 225 | ); 226 | ``` 227 | 228 | 229 |
230 | 231 | ### 셀렉터 상수화 232 | - [#96] 사실 현업에서는 DOM 을 Selector 로 조작할 일은 거의 없다. 233 | 물론 상수를 사용함으로써 변경에 용이하겠지만, HTML 에도 상수를 사용하는 게 아니라 개인적으론 가독성이 크게 떨어지는 것 같아서 사용하진 않는다. - [바로가기](https://github.com/woowacourse/javascript-youtube-classroom/pull/96#discussion_r826001310) 234 | 235 | - [#95] “자바스크립트 내에서 사용 중인 선택자들을 다른 개발자에게 알려주어, 템플릿 수정 시 주의를 요하고 싶었습니다.” 이 부분은 선택자를 사용하는 코드가 도처에 널려있을 경우에는 의미가 있다. 당연히 선택자가 수정될 상황이 생긴다면, html, css, js 모두에 수정이 필요하다. 그런 때에 js에서 해당 값을 찾아 변경하는 작업이 필요한 것도 맞다. 그런데 각 선택자가 오직 한군데씩만 쓰이고 있다면, 그 곳을 찾아서 변경하는 것과 상수를 찾아가서 수정하는 것 사이에 얼마나 큰 차이가 있을까? IDE의 일괄검색/바꾸기 기능을 쓰면 constant를 변경하는 것과 아무런 차이가 없지 않은가? 저는 일반론적인 말씀을 드린 것이 아니다. 지금 이 프로젝트에서의 효용성을 논하고 싶었던 것.오버엔지니어링이 아닌지 고민해보자는 취지이다. - [바로가기](https://github.com/woowacourse/javascript-youtube-classroom/pull/95#discussion_r825233667) 236 | 237 | 238 | 239 |
240 | 241 | ### 상수 파일 분리 242 | - [#96] 243 | 프로젝트 규모가 작다면 그냥 constants.js 에 모두 정의할 때도 있고, 점점 커져 역할에 나눌 필요가 있다면 분리할 때도 있다. 개인적으론 작을 땐 constants.js 에 정의하고 커짐에 따라 개선하는 걸 선호합니다! [YAGNI](https://martinfowler.com/bliki/Yagni.html) 원칙을 좋아합니다 그리고 범용성있게 사용되지 않는 상수는 보통 그 상수를 사용하는 파일 내에 정의할 때가 많다! - [바로가기](https://github.com/woowacourse/javascript-youtube-classroom/pull/96#discussion_r826005848) 244 | 245 | 246 |
247 | 248 | ### addEvent 함수에서 preventDefault 를 매개변수로 받아줘야할까? 249 | ```javascript 250 | addEvent({ eventType, selector, handler, isPreventedDefault = false }) { 251 | const children = [...this.container.querySelectorAll(selector)]; 252 | const isTarget = target => children.includes(target) || target.closest(selector); 253 | 254 | this.container.addEventListener(eventType, event => { 255 | if (isPreventedDefault) event.preventDefault(); 256 | if (!isTarget(event.target)) return false; 257 | handler(event); 258 | }); 259 | } 260 | ``` 261 | 262 | - [#96] 그리고 저는 preventDefault 는 사용하는 핸들러에서 명시적으로 사용하는 게 더 나을 것 같다 ! 보통 event.preventDefault 는 핸들러 내부에서 호출하는데, 그 코드가 한 depth 더 위에 있기 때문에 코드 파악하는 데 어려움이 있을 수도 있다! - [바로가기](https://github.com/woowacourse/javascript-youtube-classroom/pull/96#discussion_r826011710) 263 | 264 | 265 |
266 | 267 | ### 추상화 268 | - [#95] 추상화에 많은 공을 들이신 것 같아요. 개발하면서 즐거우셨을 것 같습니다. 다만 이번과 같은 규모가 작은 프로젝트에서는 이런 추상화/패턴화가 오히려 가독성 / 빠른 개발을 해치는 것은 아닐지에 대해서도 한 번쯤 생각해 보시면 좋겠습니다. 269 | - [#95] 전체 코드를 둘러보니, Display의 구현클래스들도 실상은 인스턴스로서의 가치가 없이 오직 초기화 용도로만 클래스를 활용하고 있더군요. 이런 경우에까지 클래스를 사용하는 것이 맞는지 고민해보시면 좋겠어요. - [바로가기](https://github.com/woowacourse/javascript-youtube-classroom/pull/95#discussion_r825242959) 270 | 271 | 272 |
273 | 274 | ### Object.freeze 275 | - Object.freeze() 메서드는 객체를 동결합니다. 동결된 객체는 더 이상 변경될 수 없습니다. 즉, 동결된 객체는 새로운 속성을 추가하거나 존재하는 속성을 제거하는 것을 방지하며 존재하는 속성의 불변성, 설정 가능성(configurability), 작성 가능성이 변경되는 것을 방지하고, 존재하는 속성의 값이 변경되는 것도 방지합니다. 또한, 동결 객체는 그 프로토타입이 변경되는것도 방지합니다. 276 | - [#95] 상수에 Object.freeze를 쓰지 않으면 협업시 다른 사람들이 해당 변수의 내용을 바꾸려는 시도를 하게 될까요? 277 | 콤피님은 나중에 다른 코드상에서 이 변수의 내용을 바꿀 생각이 들 것 같나요? 278 | - 팀 내 컨벤션이 확고하여 약속을 지키지 않을 가능성이 현저히 적은 경우 -> 굳이 freeze를 할 필요까지는 없을 듯. - [바로가기](https://github.com/woowacourse/javascript-youtube-classroom/pull/95#discussion_r825232776) 279 | 280 | 281 |
282 | 283 | ### 예외처리 284 | - [#95] `if (saveItemsCount === CLASS_ROOM_SETTING.MAX_SAVE_NUMBER)` 이런 코드보다 `if (saveItemsCount >= CLASS_ROOM_SETTING.MAX_SAVE_NUMBER)` 이게 더 안전하다. 285 | - 이유는 ? (나의 생각) 사용자가 버튼을 빠르게 눌러서 두번 저장될 수도 있는데 그러면 101 은 예외사항으로 처리하지 못하기 때문이다. [바로가기](https://github.com/woowacourse/javascript-youtube-classroom/pull/95#discussion_r825242511) 286 | 287 | 288 |
289 | 290 | ### 일정 시간후 fetch 요청 취소하기 - timeout 291 | - [#96] [바로가기](https://github.com/woowacourse/javascript-youtube-classroom/pull/96#issuecomment-1068810922) 292 | - AbortController 웹 API 를 사용하여 구현 가능 293 | - signal 객체를 fetch option으로 넣어준다. 294 | - 사용자가 지정한 timeout 시간 이후 응답이 없으면 fetch 요청 취소 295 | 296 | ```javascript 297 | const controller = new AbortController(); 298 | const { signal } = controller; 299 | const timeout = 8000; 300 | 301 | const request = async (uri, options) => { 302 | const timer = setTimeout(() => controller.abort(), timeout); 303 | const response = await fetch(uri, { ...options, signal }); 304 | 305 | if (!response.ok) throw new Error('서버 오류'); 306 | const data = await response.json(); 307 | clearTimeout(timer); 308 | return data; 309 | }; 310 | 311 | ``` 312 | 313 | 314 | -------------------------------------------------------------------------------- /youtube/step1/img/class_diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woowacourse-study/2022-code-review-study/35261e14ace715a90b33c211f885ac269a6ee996/youtube/step1/img/class_diagram.png -------------------------------------------------------------------------------- /youtube/step1/kkojae.md: -------------------------------------------------------------------------------- 1 | # STEP1. 나만의 유튜브 강의실 정리 2 | 3 | --- 4 | 5 | ## 담당 크루 6 | 7 | --- 8 | 9 | - 준찌 [#83](https://github.com/woowacourse/javascript-youtube-classroom/pull/83) 10 | - 빅터 [#90](https://github.com/woowacourse/javascript-youtube-classroom/pull/90) 11 | - 꼬재 [#98](https://github.com/woowacourse/javascript-youtube-classroom/pull/98) 12 | - 우연 [#109](https://github.com/woowacourse/javascript-youtube-classroom/pull/109) 13 | - 유세지 [#99](https://github.com/woowacourse/javascript-youtube-classroom/pull/99) 14 | - 온스타 [#87](https://github.com/woowacourse/javascript-youtube-classroom/pull/87) 15 | - 아놀드 [#106](https://github.com/woowacourse/javascript-youtube-classroom/pull/106) 16 | 17 | ## 아키텍처 분석 18 | 19 | --- 20 | 21 | 준찌, 빅터 22 | 23 | --- 24 | 25 | 커스텀 이벤트를 통해 Components와 business의 의존관계를 끊음 26 | 27 | - AppComponent: SearchForm, SearchModal, SkeletonList, Video, VideoContainer로 분리하여 component를 관리 28 | - business: 비즈니스 로직 관리 29 | 30 | 클로저 활용 - state, components, notify를 은닉화한 코드 31 | 32 | ```js 33 | export const { subscribe, setState, getState } = (function () { 34 | const state = { 35 | [STATE_STORE_KEY.IS_MODAL_SHOW]: false, 36 | [STATE_STORE_KEY.SEARCH_RESULT]: { 37 | videoList: [], 38 | keyword: null, 39 | prevVideoListLength: 0, 40 | nextPageToken: null, 41 | }, 42 | [STATE_STORE_KEY.IS_WAITING_RESPONSE]: false, 43 | [STATE_STORE_KEY.SAVED_VIDEO]: 44 | webStore.getData(WEB_STORE_KEY.SAVED_VIDEO_LIST_KEY) ?? [], 45 | }; 46 | 47 | const components = { 48 | [STATE_STORE_KEY.IS_MODAL_SHOW]: new Set(), 49 | [STATE_STORE_KEY.SEARCH_RESULT]: new Set(), 50 | [STATE_STORE_KEY.IS_WAITING_RESPONSE]: new Set(), 51 | [STATE_STORE_KEY.SAVED_VIDEO]: new Set(), 52 | }; 53 | 54 | function notify(stateKey) { 55 | const subscribedComponents = components[stateKey]; 56 | subscribedComponents.forEach((component) => component.render(stateKey)); 57 | } 58 | 59 | return { 60 | subscribe: (key, component) => { 61 | components[key].add(component); 62 | return state[key]; 63 | }, 64 | setState(key, valueOrFunction) { 65 | if (typeof valueOrFunction === "function") { 66 | state[key] = valueOrFunction(state[key]); 67 | notify(key); 68 | } 69 | if (typeof valueOrFunction !== "function") { 70 | state[key] = valueOrFunction; 71 | notify(key); 72 | } 73 | }, 74 | getState(key) { 75 | return state[key]; 76 | }, 77 | }; 78 | })(); 79 | ``` 80 | 81 | ## 피드백 정리 82 | 83 | --- 84 | 85 | ### HTML 86 | 87 | --- 88 | 89 | [#87](https://github.com/woowacourse/javascript-youtube-classroom/pull/87#discussion_r825440742) 사용자가 검색할 검색결과가 존재하지 않을지 / 존재할지 를 모르는 상태에서 미리 dom 을 그려놓으면, not_found.png 파일이 100MB 일 때 어떤 부작용이 있을지 생각해보면 좋겠네요! 타 html 태그와는 달리 img 는 기본적으로 추가해놓을 때 한 번쯤은 고민해볼 주제인 것 같네요! 90 | 91 | - lazy loading, 필요한 시점에 dom element를 생성해 추가하는 방식 두가지중 선택하면 좋다. 92 | 93 | ### Lazy loading 94 | 95 | Lazy loading이란 무엇인가? 96 | 97 | 페이지를 로드할 때, 모든 이미지를 로드하는 것이 아니라 중요하지 않은 자원 또는 당장 필요 없는 자원의 경우 서버에 요청을 미루고 필요한 경우 해당 자원을 요청 받는 방법을 말한다. 98 | 99 | Lazy loading이 필요한 이유 100 | 101 | 1. 데이터의 낭비를 막을 수 있다. 102 | 103 | 서버로부터 모든 자원을 요청하는 것은 잠재적으로 사용자들이 사용하지 않거나 볼 가능성이 적은 모든 자원들까지 요청받는 것이다. 104 | 따라서 서버로부터 필요한 자원만 요청 받고, 필요할 때만 해당 자원을 요청 받는 것이 효율적이다. 105 | 106 | 2. 브라우저의 랜더링 시간을 줄여준다. 107 | 108 | 브라우저는 서버로부터 자원을 요청받고 난 뒤에 화면에 랜더링을 한다. 따라서 불필요한 자원의 다운로드를 막는 것만으로도 프로세스 시간이 단축될 수 있다. 109 | 110 | loading 속성 값은 다음 3가지와 같다. 111 | 112 | - auto : 디폴트 값으로, 속성값을 지정하지 않은 것과 동일하다. 113 | - lazy : 뷰포트 상에서 해당 이미지의 위치를 계산하여 이미지 자원을 요청함 114 | - eager : 어느 위치에 있든지 이미지 자원을 바로 요청받음 115 | 116 | 사용법 117 | 118 | ```html 119 | 120 | ``` 121 | 122 | 주의 사항 123 | 124 | - 해당 속성을 사용할 경우 되도록 해당 이미지 영역의 크기를 지정하는 것이 권장된다. 125 | - 영역의 크기에 대한 정보가 없으면, 브라우저는 해당 영역의 크기를 알 수 없어 해당 영역을 0x0으로 처음에 인식하게 된다. 126 | - 만약 해당 이미지 영역으로 스크롤 할 경우 이미지가 로드 되면서 layout shift가 일어날 수 있기 때문에 되도록 해당 img 태그에 명시적으로 높이/너비 값을 지정해야 함. 127 | - 페이지의 첫 시작부터 보이는 이미지에 대해서는 lazy-loading을 사용하지 말아야 한다. 128 | - background-image에서는 적용 안 된다. 129 | 130 | > [우테코 LMS lazy-loading](https://techcourse.woowahan.com/s/dSWvXWYI/ls/MlRcHlFG), [웹 성능을 위한 이미지 최적화](https://velog.io/@hustle-dev/%EC%9B%B9-%EC%84%B1%EB%8A%A5%EC%9D%84-%EC%9C%84%ED%95%9C-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EC%B5%9C%EC%A0%81%ED%99%94) 131 | 132 | ### JS 133 | 134 | --- 135 | 136 | [#99](https://github.com/woowacourse/javascript-youtube-classroom/pull/99#discussion_r826038350) date를 파싱해서 그대로 사용해주셨는데 api에서 넘어오는 시간은 국제 표준시라 그대로 사용하게되면 일자가 안맞을 가능성이 있어보여요. date에 이해하는데 도움될만한 아티클이 있어 읽어보시길 추천드릴게요 😄 137 | 138 | - 다양한 지역에 대한 타임존을 제대로 지원하고 싶다면, 직접 구현하려는 욕심을 버리고 **Moment Timezone과 같은 라이브러리**를 활용하는 것이 나을 것이다. 139 | 140 | > 아티클 : [자바스크립트에서 타임존 다루기1](https://meetup.toast.com/posts/125), [자바스크립트에서 타임존 다루기2](https://meetup.toast.com/posts/130) 141 | 142 | [#106](https://github.com/woowacourse/javascript-youtube-classroom/pull/106#discussion_r825445785) replaceChildren() 도 넘기는 인수 없이 호출하면 하위 노드 요소를 비워줄 수 있어요. 추천 👍 143 | 144 | ### replaceChildren 145 | 146 | element의 기존 자식 요소를 replaceChildren()의 인자값으로 변경해준다. 147 | 148 | - 인자 값은 DOMString이나 Node 객체가 올 수 있다. 149 | - 인자의 값이 비어있는 경우 하위 요소들을 모두 지워준다. 150 | 151 | Syntax 152 | 153 | ```js 154 | element.replaceChildren(...DOMStringOrNodeObject); 155 | ``` 156 | 157 | 리뷰어님 말씀 그대로! 158 | 159 | ```js 160 | element.innerHTML = ""; 161 | 162 | element.replaceChildren(); 163 | ``` 164 | 165 | innerHTML -> replaceChildren()을 사용할 수 있다. 166 | 167 | > [MDN - replaceChilren()](https://developer.mozilla.org/en-US/docs/Web/API/Element/replaceChildren) 168 | 169 | [#90](https://github.com/woowacourse/javascript-youtube-classroom/pull/90#discussion_r825565265) 추가로, DOMContentLoaded 이벤트는 DOM 트리 구축이 완료된 후 호출되는 이벤트라 하고, 해당 이벤트가 호출된 후 app이 실행되는 게 안전하다고 판단하여 사용했습니다. 혹시 제 판단에 잘못된 부분이 있다면 편하게 말씀해주세요😆 170 | 171 | DOMContentLoaded 172 | 173 | - DOMContentLoaded 이벤트는 초기 HTML 문서를 완전히 불러오고 분석했을 때 발생합니다. 스타일 시트, 이미지, 하위 프레임의 로딩은 기다리지 않습니다. 174 | 175 | > [MDN - DOMContentloaded](https://developer.mozilla.org/ko/docs/Web/API/Window/DOMContentLoaded_event) 176 | 177 | load 178 | 179 | - load는 모든 리소스(css, image, font)등등 이 불러드려지고 나서 호출된다. 180 | 181 | > [MDN - load](https://developer.mozilla.org/ko/docs/Web/API/Window/load_event) 182 | 183 | beforeunload 184 | 185 | - beforeunload 이벤트는 문서와 그 리소스가 언로드 되기 직전에 window에서 발생합니다. 이벤트 발생 시점엔 문서를 아직 볼 수 있으며 이벤트도 취소 가능합니다. 186 | - 사용자가 화면을 종료할 때! 발생하는 이벤트 187 | - beforeunload 이벤트를 사용하면 사용자가 페이지를 떠날 때 정말로 떠날 것인지 묻는 확인 대화 상자를 표시할 수 있습니다. 사용자가 확인을 누를 경우 브라우저는 새로운 페이지로 탐색하고, 취소할 경우 탐색을 취소하고 현재 페이지에 머무릅니다. 188 | 189 | > [MDN - beforeunload](https://developer.mozilla.org/ko/docs/Web/API/Window/beforeunload_event) 190 | 191 | unload 192 | 193 | - 사용자가 화면을 종료하고, 모든 리소스들이 unloading 중에 발생하는 이벤트 194 | - 이미지, IFrame 등 모든 리소스는 여전히 존재합니다. 195 | - 최종 사용자는 아무것도 볼 수 없습니다. 196 | - UI 상호작용은 아무 효과도 없습니다. (window.open(), window.alert(), window.confirm(), 등등) 197 | - 오류가 발생해도 언로딩 절차는 중단되지 않습니다. 198 | 199 | > [MDN - unload](https://developer.mozilla.org/ko/docs/Web/API/Window/unload_event) 200 | 201 | [#90](https://github.com/woowacourse/javascript-youtube-classroom/pull/90#discussion_r825306933) notify 할 때, 따로 state 를 전달해줘야할까요? 👀 202 | 203 | 기존 코드 204 | 205 | ```js 206 | function notify(stateKey) { 207 | const subscribedComponents = components[stateKey]; 208 | subscribedComponents.forEach((component) => 209 | component.wakeUp(state[stateKey], stateKey) 210 | ); 211 | } 212 | ``` 213 | 214 | 변경된 코드 +getState 메서드 추가 후 분리 215 | 216 | ```js 217 | function notify(stateKey) { 218 | const subscribedComponents = components[stateKey]; 219 | subscribedComponents.forEach((component) => component.render(stateKey)); 220 | } 221 | ``` 222 | 223 | ### test 224 | 225 | --- 226 | 227 | [#99](https://github.com/woowacourse/javascript-youtube-classroom/pull/99#discussion_r825272614) 테스트의 케이스가 많은건 좋다고 생각하는데 의미있는 테스트인지는 생각해볼 필요가 있어보여요. 유틸리티가 많이 변경될 여지가 없다고 생각되기도 하구요. 테스트를 위한 테스트가 된 느낌입니다 😅 228 | 229 | [#99](https://github.com/woowacourse/javascript-youtube-classroom/pull/99#discussion_r825261609) API를 모킹하는 대표적인 라이브러리 MSW 230 | 231 | > [msw](https://blog.mathpresso.com/msw%EB%A1%9C-api-%EB%AA%A8%ED%82%B9%ED%95%98%EA%B8%B0-2d8a803c3d5c) 232 | 233 | [#98](https://github.com/woowacourse/javascript-youtube-classroom/pull/98#discussion_r825415865) API를 테스트 할 것인지..? API 응답을 처리하는 로직을 테스트할 것인지..? 234 | 235 | 리뷰어님 : API를 테스트하는 것은 BE의 몫이라고 생각한다. 그렇게 때문에 API를 테스트 할 것이 아니라면 실제 API를 호출할 필요는 없다고 생각한다. 236 | 내 생각 : 즉, API를 직접 호출하여 테스트 하는 것 보다는 API호출을 인터셉트하여 모킹된 데이터로 갈아끼워주는 작업을 해줘야 한다. 237 | 238 | [#98](https://github.com/woowacourse/javascript-youtube-classroom/pull/98#discussion_r825416213) Cypress에서는 fixture, intercept를 활용해서 API를 모킹할 수 있다. 239 | 240 | > [cypress-fixture](https://docs.cypress.io/api/commands/fixture), [cypress-intercept](https://docs.cypress.io/api/commands/intercept#Stubbing-a-response) 241 | 242 | ### 구조, 설계 243 | 244 | --- 245 | 246 | [#99](https://github.com/woowacourse/javascript-youtube-classroom/pull/99#discussion_r825284197) 해당 포스팅에서 프론트엔드가 거쳐온 아키텍처를 훑어볼 수 있어 좋아요! 247 | 248 | > [구조에 대한 설계 패턴이 정리된 블로그](https://velog.io/@teo/%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C%EC%97%90%EC%84%9C-MV-%EC%95%84%ED%82%A4%ED%85%8D%EC%B3%90%EB%9E%80-%EB%AC%B4%EC%97%87%EC%9D%B8%EA%B0%80%EC%9A%94) 249 | -------------------------------------------------------------------------------- /youtube/step1/lokba.md: -------------------------------------------------------------------------------- 1 | # Level1 Youtube Step1(담당 PR 번호들) - 록바 2 | 3 | - 분석 담당 코드 4 | - 록바 [#88](https://github.com/woowacourse/javascript-youtube-classroom/pull/88) 5 | - 앨버 [#89](https://github.com/woowacourse/javascript-youtube-classroom/pull/89) 6 | - 태태 [#93](https://github.com/woowacourse/javascript-youtube-classroom/pull/93) 7 | - 결 [#86](https://github.com/woowacourse/javascript-youtube-classroom/pull/86) 8 | - 자스민 [#76](https://github.com/woowacourse/javascript-youtube-classroom/pull/76) 9 | - 동키콩 [#81](https://github.com/woowacourse/javascript-youtube-classroom/pull/81) 10 | - 도리 [#85](https://github.com/woowacourse/javascript-youtube-classroom/pull/85) 11 | - 소피아 [#82](https://github.com/woowacourse/javascript-youtube-classroom/pull/82) 12 |
13 | 14 | ## 아키텍처 분석(desc: 담당한 소프트웨어의 아키텍처를 간단히 분석하고 설명합니다) 15 | 16 | #### [#76, #81] 자스민, 동키콩 17 | 18 | #### 클래스 다이어그램 19 | 20 | 21 | 22 | - 큰 틀로 도메인과 UI를 나누어 설계하였다. 23 | - 도메인 24 | 25 | - SearchMachine 26 | - SearchMachine에서 접근해서 사용하는 파일(VideoFactory, Video, localStorage) 27 | 28 | - UI 29 | 30 | - MainPage 31 | - SearchModal 32 | - Templates 33 | 34 | #### 인상 깊었던 점 35 | 36 | - 1. **template을 관리하는 구조** 37 | 38 | ```jsx 39 | const template = { 40 | videoItems: ({...}) => `...`, 41 | 42 | noSearchResult: () => `...`, 43 | 44 | skeletonItem: () => `...`, 45 | 46 | exceedCapacityErrorImage: () => `...`, 47 | }; 48 | ``` 49 | 50 | - template이라는 객체안에 메소드를 만들어 불러올 수 있게끔 구조. 51 | 52 |
53 | 54 | - 2. **Video 객체를 만들때 사용한 팩토리 패턴과 빌더 패턴** 55 | 56 | ```jsx 57 | // 호출하는 부분 58 | data.items.map((item) => VideoFactory.generate(item)); 59 | 60 | // 팩토리 패턴 61 | class VideoFactory { 62 | static generate(item) { 63 | const { videoId } = item.id; 64 | const isSaved = checkSavedVideo(videoId); 65 | const { 66 | thumbnails: { 67 | high: { url }, 68 | }, 69 | channelTitle, 70 | title, 71 | publishTime, 72 | } = item.snippet; 73 | 74 | return Video.Builder() 75 | .setId(videoId) 76 | .setThumbnails(url) 77 | .setTitle(title) 78 | .setChannelTitle(channelTitle) 79 | .setPublishTime(publishTime) 80 | .setIsSaved(isSaved) 81 | .build(); 82 | } 83 | } 84 | ``` 85 | 86 | - 팩토리 패턴에 대해서 잘 모른다면, [이 영상 : 6분 8초~](https://www.youtube.com/watch?v=q3_WXP9pPUQ&t=81s)을 추천한다. 87 | - 팩토리 패턴에 대해서 잘 모른다면, [게시글](https://readystory.tistory.com/117)을 추천한다. 88 | - 팩토리 패턴을 이용해서, Factory클래스에 접근하는 곳에서 어떠한 일이 일어나는지 예측할 수 없도록 코드를 구상하였다. 89 | - 내가 생각하는 아쉬운 점 : 현재는 Video에 대한 객체만 생성하는데 팩토리 패턴을 굳이 만들 필요가 있었나 싶다. 시도로는 아주 굿이다. 90 | 91 |
92 | 93 | - 3. **팩토리 패턴에서, Video 객체를 생성할때 사용한 클로저 활용 빌더 패턴** 94 | 95 | ```jsx 96 | //호출하는 부분 97 | Video.Builder() 98 | .setId(videoId) 99 | .setThumbnails(url) 100 | .setTitle(title) 101 | .setChannelTitle(channelTitle) 102 | .setPublishTime(publishTime) 103 | .setIsSaved(isSaved) 104 | .build(); 105 | 106 | 107 | //빌더 패턴 108 | export default class Video { 109 | ... 필요없는 부분 생략 110 | 111 | static Builder() { 112 | let id; 113 | let thumbnails = "NO_THUNBMNAILS"; 114 | let channelTitle = "NO_CHANNEL_TITLE"; 115 | let title = "NO_TITLE"; 116 | let publishTime = "1000/01/01"; 117 | let isSaved = false; 118 | 119 | return { 120 | setId: function (value) { 121 | id = value; 122 | return this; 123 | }, 124 | 125 | setTitle: function (name) {...}, 126 | 127 | setThumbnails: function (url) {...}, 128 | 129 | setChannelTitle: function (name) {...}, 130 | 131 | setPublishTime: function (time) {...}, 132 | 133 | setIsSaved: function (saved) {...}, 134 | 135 | build: () => { 136 | if (!id) throw new Error(ERROR_MESSAGE.NO_ID); 137 | return new Video({ 138 | id, 139 | thumbnails, 140 | title, 141 | channelTitle, 142 | publishTime, 143 | isSaved, 144 | }); 145 | }, 146 | }; 147 | } 148 | } 149 | ``` 150 | 151 | - 빌더 패턴에 대해서 잘 모른다면, [게시글](https://readystory.tistory.com/121)을 추천한다. 152 | - 빌더 패턴은 복잡한 객체를 생성하는 방법을 정의하는 클래스와 표현하는 방법을 정의하는 클래스를 별도로 분리하여, 서로 다른 표현이라도 이를 생성할 수 있는 동일한 절차를 제공하는 패턴 153 | - 내가 생각하는 사용한 의도 : Video 객체에 들어가는 property가 많다보니, 가독성과 선언적으로 구성하고 싶어 사용한 것으로 추측됨. 154 | 155 |
156 | 157 | ## 피드백 정리 158 | 159 | ## 대분류(ex: 아키텍처, 함수/클래스, 컨벤션, DOM, 테스트 등) 160 | 161 | ### 1. 아키텍처 162 | 163 | #### 1-1 도메인과 UI 164 | 165 | - [#76] 도메인과 UI를 굳이 왜 나누어야 하는가? - [바로가기](https://github.com/woowacourse/javascript-youtube-classroom/pull/76#pullrequestreview-907964309) 166 | - domain과 ui를 왜 나누어야하는지 질문을 주셨는데 domain과 ui가 코드가 같이 있다면 ui를 얼마나 재사용 할 수 있을까요? 또한 지금 작성하신 searchModal view를 유투브 검색이 아니라 다른 플랫폼영상 검색 view로 재사용 가능한지 생각해보면 좋을것 같습니다. 소프트웨어 설계에서 '결합도(coupling)'에 대해서 공부하시면 도움이 될듯합니다. 167 | - 관련 키워드 : 낮은 결합도와 높은 응집도 ([내가 추천하는 게시글](https://madplay.github.io/post/coupling-and-cohesion-in-software-engineering)) 168 | 169 | --- 170 | 171 | ### 2. 함수 172 | 173 | #### 2-1. 함수분리 174 | 175 | - [#88] throttle하는 함수를 유틸로 분리해보자. - [바로가기](https://github.com/woowacourse/javascript-youtube-classroom/pull/88#discussion_r825303875) 176 | 177 | ```jsx 178 | //적용 후 179 | const throttle = (func, delay) => { 180 | let timerId; 181 | 182 | return () => { 183 | if (!timerId) { 184 | timerId = setTimeout(() => { 185 | timerId = null; 186 | func(); 187 | }, delay); 188 | } 189 | }; 190 | }; 191 | ``` 192 | 193 |
194 | 195 | #### 2-2. 예외처리 196 | 197 | - [#76] JSON.parse 할 수 없는 값이 반환된다면 어떻게 될까요? - [바로가기](https://github.com/woowacourse/javascript-youtube-classroom/pull/76#discussion_r825298848) 198 | 199 | ```jsx 200 | //해당 코드 201 | const getLocalStorage = (key) => JSON.parse(localStorage.getItem(key)) ?? []; 202 | ``` 203 | 204 | - [[MDN](https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse)] 변환할 문자열이 유효한 JSON이 아닐 경우에 syntaxError가 발생한다. 205 | 206 | ```jsx 207 | //수정한 코드 208 | const getLocalStorage = (key) => { 209 | try { 210 | return JSON.parse(localStorage.getItem(key)) ?? []; 211 | } catch { 212 | alert(ERROR_MESSAGE.WRONG_INPUT); 213 | } 214 | }; 215 | ``` 216 | 217 | --- 218 | 219 | ### 3. 클래스 220 | 221 | #### 3.1 내장 메소드 222 | 223 | - [#86] classList.toggle 상세 기능 - [바로가기](https://github.com/woowacourse/javascript-youtube-classroom/pull/86#discussion_r825563206) 224 | 225 | ```jsx 226 | //수정 전 227 | event === "hide" 228 | ? skeletonUi.classList.add("hide-skeleton") 229 | : skeletonUi.classList.remove("hide-skeleton"); 230 | 231 | //수정 후 232 | classList.toggle("hide-skeleton", event === "hide"); 233 | ``` 234 | 235 | - [[MDN](https://developer.mozilla.org/ko/docs/Web/API/Element/classList)] toggle에 두번째 인수가 있을 때 : 두번째 인수가 `true`로 평가되면 지정한 클래스 값을 추가하고 `false`로 평가되면 제거한다. 236 | 237 |
238 | 239 | --- 240 | 241 | ### 4. DOM, 이벤트 242 | 243 | #### 4-1. DOM 244 | 245 | - [#89] document.querySelector vs element.querySelector - [바로가기](https://github.com/woowacourse/javascript-youtube-classroom/pull/89#discussion_r827182868) 246 | 247 | ```jsx 248 | //해당 코드 249 | $(".video-item__thumbnail", element).src = thumbnails; 250 | ``` 251 | 252 | - [리뷰어님 답변] 성능을 위한 이런 소소한 개선 좋습니다 👍 253 | - document.querySelector로 시작한다면 DOM 트리의 최상단에서부터 시작해서 찾게 되고 element.querySelector로 찾게 되면 해당 element부터 시작해서 찾게 됩니다 254 | 물론 요즘이야 워낙 하드웨어가 좋아서 이런 정도는 인지할 수 없을 정도의 미세한 차이지만 **이런 작은 개선들이 조금씩 쌓여서 좋은 프로그램이 될 수도, 반대로 안좋은 미세한 차이들이 쌓여서 나중에는 거대한 똥덩어리가 되기도 하는걸 많이 봤습니다.** 255 | - 추가적으로 작성해주신 방식이 의도하지 않은 element가 select 되는 것을 막을수 있습니다. 예를 들어서 $('.video-itemthumbnail')이 document부터 시작한다면 DOM tree에서 class name이 video-item\_\_thumbnail인 첫 번째 element를 찾게 되서 엉뚱한 element의 src에 thumbnails가 대입되게 됩니다 256 | 257 |
258 | 259 | - [#86] innerHTML 보다는 insertAdjacentHTML 사용해 주세요. - [바로가기](https://github.com/woowacourse/javascript-youtube-classroom/pull/86#discussion_r824851095) 260 | 261 | - 리뷰어님이 첨부해준 [참고링크](https://developer.mozilla.org/ko/docs/Web/API/Element/insertAdjacentHTML) 262 | 263 |
264 | 265 | - [#86] 내용을 지울때 replaceChildren()을 쓰자. 266 | 267 | ```jsx 268 | //수정 전 269 | this.videoListContainer.innerHTML = ""; 270 | 271 | //수정 후 272 | this.videoListContainer.replaceChildren(); 273 | ``` 274 | 275 | - **헷갈림 주의!!** [Node.replaceChild()](https://developer.mozilla.org/ko/docs/Web/API/Node/replaceChild) vs [Element.replaceChildren()](https://developer.mozilla.org/en-US/docs/Web/API/Element/replaceChildren) 276 | - MDN에 따르면, **replaceChildren()모든 자식 노드를 비우기 위한 매우 편리한 메커니즘을 제공합니다.** 277 | 278 |
279 | 280 | #### 4-2. 이벤트 리스너 281 | 282 | - [#89] 이벤트를 매핑하는 영역 - [바로가기](https://github.com/woowacourse/javascript-youtube-classroom/pull/89#discussion_r825416052) 283 | 284 | ```jsx 285 | //해당 코드 286 | document.addEventListener("click", this.handleCloseModal.bind(this)); 287 | ``` 288 | 289 | - 전역에서 사용되는 document에 이벤트 리스너를 추가하게 되면, 불필요한 이벤트 감지가 자주 발생하여 추천하지 않는다. 290 | - 전역에 매핑된 이벤트 리스너는 의도치 않은 동작을 발생시킬 수 있다. 291 | - 이벤트를 매핑하는 영역은 내가 다루고 있는 컴포넌트 영역으로 한정하는 것이 좋다. 292 | 293 |
294 | 295 | - [#85] 리뷰어님이 생각하는 보통 custom event를 사용하는 경우 - [바로가기](https://github.com/woowacourse/javascript-youtube-classroom/pull/85#discussion_r826997741) 296 | - 보통은 A 요소와 B 요소가 별도로 역할이 분리되어 있는데 이벤트를 발생시키는 A요소에 있는 값들을 해당 이벤트를 수신하는 상위 컨테이너 B 요소에서 그 전달받은 값 들로 새로운 값으로 가공하거나 변경해서 다른 C 요소에게 전달해야할 때, 각 요소에 대한 의존성을 분리시키고 이벤트에 의존하게 하면서 좀 더 유연하게 처리할 수 있는 장점이 있는것 같아요. 297 | 298 |
299 | 300 | --- 301 | 302 | ### 5. 클린코드 303 | 304 | #### 5-1. 변수 305 | 306 | - [#86] 변수를 할당해서 명확히 하자. - [바로가기](https://github.com/woowacourse/javascript-youtube-classroom/pull/86#discussion_r824860673) 307 | 308 | - searchResults는 제대로 값이 들어있을지를 의심하게 만드네요. 309 | 310 | ```jsx 311 | //수정 전 312 | await this.searchVideo.handleSearchVideo(this.searchInput.value.trim()); 313 | this.renderSearchVideo(this.searchVideo.searchResults); 314 | 315 | //수정 후 316 | const searchResults = await this.searchVideo.handleSearchVideo( 317 | this.searchInput.value.trim() 318 | ); 319 | this.renderSearchVideo(searchResults); 320 | ``` 321 | 322 |
323 | 324 | #### 5-2. 네이밍 325 | 326 | - [#89] export default할 class/함수명과 파일명을 일치시키는게 관례이다. - [바로가기](https://github.com/woowacourse/javascript-youtube-classroom/pull/89#discussion_r827201820) 327 | 328 | ```jsx 329 | //해당코드 330 | //위치 : src/js/screen/index.js 331 | 332 | export default class ScreenManager{...} 333 | ``` 334 | 335 | - 파일이 많아질 경우 import 된 class/함수명만 보고 파일을 찾아가는 경우가 많다보니 default로 export할 class/함수명과 파일명을 일치시키는게 일반적인 관례이다. 336 | 337 |
338 | 339 | - [#93] 메서드 네이밍 - [바로가기](https://github.com/woowacourse/javascript-youtube-classroom/pull/93#discussion_r824931787) 340 | 341 | - setStorageVideoList 메서드 네이밍과 로직이 매치가 안되는 거 같습니다. (**메서드명만 보면 saveVideoList 상태값을 통으로 바꿔줄 거 같다는 느낌을 받았습니다**) 342 | 343 | ```jsx 344 | //수정 전 해당코드 345 | setStorageVideoList(videoId) { 346 | this.saveVideoList = [videoId, ...this.saveVideoList]; 347 | localStorage.setItem(VIDEO_ID_LIST_KEY, JSON.stringify(this.saveVideoList)); 348 | } 349 | 350 | //수정 후 해당코드 351 | saveVideoIdToStorage(videoId) { 352 | this.saveVideoList = [videoId, ...this.saveVideoList]; 353 | localStorage.setItem(VIDEO_ID_LIST_KEY, JSON.stringify(this.saveVideoList)); 354 | } 355 | ``` 356 | 357 |
358 | 359 | - [#86] 제 삼자가 변수명만 보고 하고자하는 바를 대충이나마 유추할 수 있도록 하자. - [바로가기](https://github.com/woowacourse/javascript-youtube-classroom/pull/86#pullrequestreview-907390620) 360 | 361 | - `최대 10개`가 아닌, `한 번에 요청하는 개수`가 10개 이기 때문에 수정 - [바로가기](https://github.com/woowacourse/javascript-youtube-classroom/pull/86#discussion_r824836900) 362 | 363 | ```jsx 364 | //수정 전 365 | const MAX_VIDEO_COUNT = 10; 366 | 367 | //수정 후 368 | const GET_VIDEO_COUNT = 10; 369 | ``` 370 | 371 | - **disabled는 어떤 이유에서건 누를수 없는 모든 경우를 아우르는 표현**이니, 좀더 구체적인 의미를 지니는 단어로 표현하자. - [바로가기](https://github.com/woowacourse/javascript-youtube-classroom/pull/86#discussion_r824837973) 372 | 373 | ```jsx 374 | //수정 전 375 | const BUTTON_DISABLED_TEXT = "저장됨"; 376 | 377 | //수정 후 378 | const ALREADY_SAVED_VIDEO = "저장됨"; 379 | ``` 380 | 381 |
382 | 383 | --- 384 | 385 | ### 6. HTML, CSS 386 | 387 | #### 6-1. HTML 388 | 389 | - [#81] modal에 해당하는 semantic tag는 없을까요? - [바로가기](https://github.com/woowacourse/javascript-youtube-classroom/pull/81#discussion_r825378003) 390 | - [동키콩 답변] 공식문서를 확인 해보니, ``태그가 존재한다. 391 | - [[MDN](https://developer.mozilla.org/ko/docs/Web/HTML/Element/dialog)] HTML `` 요소는 닫을 수 있는 경고, 검사기, 창 등 대화 상자 및 기타 다른 상호작용 가능한 컴포넌트를 나타냅니다. 392 | 393 |
394 | 395 | - [#76] button의 default type은 무엇일까요? - [바로가기](https://github.com/woowacourse/javascript-youtube-classroom/pull/76#discussion_r825261769) 396 | - button의 기본 default type은 submit이다. 397 | 398 |
399 | 400 | #### 6-2. CSS 401 | 402 | - [#88] 고정된 background, color 값들은 변수로 빼주세요. - [바로가기](https://github.com/woowacourse/javascript-youtube-classroom/pull/88#discussion_r825301665) 403 | 404 |
405 | 406 | - [#88] NO_RESULT(결과 없음을 나타내는 페이지)는 항상 동일한 화면을 보여주기에 html에 있고, 상태에 따라 show, hide하는 방식을 이용하는게 어떨까요? - [바로가기](https://github.com/woowacourse/javascript-youtube-classroom/pull/88#discussion_r825304921) 407 | ```jsx 408 | //해당 코드 409 | this.searchResult.insertAdjacentHTML("beforeend", NO_RESULT_TEMPLATE); 410 | ``` 411 | 412 | --- 413 | 414 | ### 7. 테스트 415 | 416 | - [#89] TDD는 어디까지나 더 나은 코드를 작성하기 위해 도움을 줄 수 있는 하나의 방법론일 뿐이지 이 자체가 목적이 되어서는 안된다. - [바로가기](https://github.com/woowacourse/javascript-youtube-classroom/pull/89#pullrequestreview-908108852) 417 | 418 |
419 | 420 | - [#89] **테스트의 목적** - [바로가기](https://github.com/woowacourse/javascript-youtube-classroom/pull/89#discussion_r827155936) 421 | - 테스트의 목적은 단순히 기능을 검증하는 데에만 있지 않다. 422 | - 테스트 코드는 추후에 다른 개발자 혹은 현재의 flow를 망각한 자신에게 테스트 기능에 대한 어떤 flow가 필요한지 보여주는 설명서가 된다. 423 | - 또 다른 목적은 변경의 감지이다. 코드가 변경됐을 때 그 코드에 의존하고 있던 다른 코드가 변경된 내역을 반영하지 않고 있는 경우를 테스트 코드를 통해 발견할 수 있습니다. 424 | 425 |
426 | 427 | - [#93] 일관성 있는 테스트 결과를 내려고 하자. - [바로가기](https://github.com/woowacourse/javascript-youtube-classroom/pull/93#discussion_r824926586) 428 | ```jsx 429 | //해당코드 430 | try { 431 | await searchVideo.handleSearchVideo("playlist"); 432 | expect(searchVideo.searchResults.length).toEqual(MAX_VIDEO_COUNT); 433 | } catch (error) { 434 | expect(error.message).toEqual(ERROR_MESSAGE.CANNOT_GET_YOUTUBE_VIDEO); 435 | } 436 | ``` 437 | - 에러가 무조건 나는 케이스와 절대 에러 나지 않는 케이스로 각각 나눠서 테스트하면 좋을 것 같습니다. 438 | 439 | --- 440 | 441 | ### 9. 기타 442 | 443 | #### 9-1. 패키지 444 | 445 | - [#89] default 값에 의존하는 것보다는 tab size 같은 최소한의 정의는 명시적으로 표현하는게 좋을 것 같습니다. - [바로가기](https://github.com/woowacourse/javascript-youtube-classroom/pull/89#discussion_r827184610) 446 | 447 |
448 | 449 | - [#81] `printWidth : 100`을 한 이유가 있을까요? - [바로가기](https://github.com/woowacourse/javascript-youtube-classroom/pull/81#discussion_r825377862) 450 | - prettier 공식 문서에 따르면, **For readability we recommend against using more than 80 characters:** 451 | 452 |
453 | 454 | - [#81] --no-cache로 설정한 이유는 있을까요? - [바로가기](https://github.com/woowacourse/javascript-youtube-classroom/pull/81#discussion_r825378085) 455 | ```jsx 456 | //해당코드 457 | "scripts": { 458 | "test": "jest --watch --no-cache", 459 | ``` 460 | - [동키콩 답변] 공식문서를 다시 살펴보니 --no-cache를 사용할 경우 jest가 최소 두배이상 느려져 캐싱문제가 생길경우에만 --no-cache를 사용하라고 추천하고 있네요 461 | 462 |
463 | 464 | - [#81] 버전을 1.0.0으로 하신 이유가 있을까요? - [바로가기](https://github.com/woowacourse/javascript-youtube-classroom/pull/81#discussion_r825378158) 465 | ```jsx 466 | //해당코드 467 | { 468 | "name": "javascript-youtube-classroom", 469 | "version": "1.0.0", 470 | ``` 471 | - package.json 처럼 쓰이는 버전 양식을 [semver](https://semver.org/lang/ko/) 라고 합니다. (**Sementic Versioning의 약자**) 제가 보기엔, 완성된게 아니므로 메이저 버전이 1이 될 수 없을 것 같아요! 그렇기에 0.x.x 요렇게 표기되어야 하지 않을까 싶네요~ 472 | 473 |
474 | 475 | #### 9-2. 좋은 말 476 | 477 | - [#81] [동키콩 질문] 어디까지 테스트를 해야하는지 감이 안온다. - [바로가기](https://github.com/woowacourse/javascript-youtube-classroom/pull/81#pullrequestreview-911456941) 478 | - **테스트 코드는, 직접 "이게 맞나..? 이렇게 디테일 해야해..?" 까지 짜보시는걸 추천드릴께요!** 항상 이야기하면 감이 잘 안오는데, 최대한 할 수 있는데로 다 나눠보시는걸 추천드릴께요! 479 | -------------------------------------------------------------------------------- /youtube/step1/marco.md: -------------------------------------------------------------------------------- 1 | # Level1 Youtube Step1(담당 PR 번호들) - 마르코 2 | 3 | - 분석 담당 코드 4 | - 마르코 [#100](https://github.com/woowacourse/javascript-youtube-classroom/pull/100) 5 | - 위니 [#103](https://github.com/woowacourse/javascript-youtube-classroom/pull/103) 6 | - 병민 [#111](https://github.com/woowacourse/javascript-youtube-classroom/pull/111) 7 | - 해리 [#112](https://github.com/woowacourse/javascript-youtube-classroom/pull/112) 8 | - 코카콜라 [#77](https://github.com/woowacourse/javascript-youtube-classroom/pull/77) 9 | - 후이 [#105](https://github.com/woowacourse/javascript-youtube-classroom/pull/105) 10 | - 샐리 [#84](https://github.com/woowacourse/javascript-youtube-classroom/pull/84) 11 | 12 | # 1. 코드 분석 13 | 14 | ## 병민[#111], 해리[#112] 15 | 16 | - 진입점: index.js 17 | - css 및 App 클래스 실행 18 | - 페이지별, 이벤트의 흐름대로 설계됐다. 메인 페이지와 검색 모달창을 담당하는 두 클래스가 중심이 된다. 19 | - App 클래스 20 | 21 | - 의존하는 클래스 22 | - SearchModal, VideoStorage, VideoItem 23 | - constructor() 24 | - 선택한 dom요소를 멤버변수로 할당 25 | - 이벤트리스너 등록 26 | - 대상: 모달열기버튼 -`openModal`, 본영상 볼영상 필터버튼 래퍼 - `handleFilterClick`, 저장 영상 목록 래퍼(하위요소에 이벤트 위임하기 위해) - `handleClickVideoList` 27 | - 로컬스토리지 관련 인수를 받은 VideoStorage 클래스의 인스턴스(이하 this.video)를 SearchModal 클래스의 인스턴스에게 인수로 전달한다. 또한 SearchModal 클래스의 인스턴스는 App의 (미래에 생성될)인스턴스를 `this`를 통해 인수로 전달받고, 초기실행(init 메서드)된다. 28 | - `this.storage`에서 저장된 비디오의 id 배열을 가져와서 `requestVideos(ids)`메서드에 전달하고 api에서 fetch된 데이터 목록를 받는다. 데이터 목록을 받으면, `renderVideoItems(videoList)`메서드에 데이터 목록을 전달하고 호출하여 저장되어 있는 영상 목록을 메인 화면에 렌더링하도록 한다. 29 | - searchResultTemplate() 30 | - 검색결과 htmlString 31 | - openModal() 32 | - modal의 클래스 hide 여부 toggle 33 | - handleClickVideoList() 34 | - 이벤트 위임을 하위 요소 중 button 태그에 대해서만 줄 것이므로, target의 태그가 button이 아니면 처음부터 early return하도록 if문을 주었다. 35 | - `상태변경` 버튼이나 `삭제`버튼에 대해서만 이벤트 위임을 하도록 분기를 짰고, 분기에 따라 각각의 로직(model, view 모두)이 내부에 구현되어 있다. 36 | - handleFilterClick() 37 | - '볼 영상', '본 영상' 탭 버튼 클릭 시, 이벤트에 대한 콜백함수 38 | - renderVideoItems(videoList) 39 | - 영상 데이터 목록을 전달받아서, map을 통해 아이템 한 개씩 videoItemTemplate메서드에게 전달하여 반환된 html string을 하나로 합친(join) 뒤, 이를 메인 화면에 렌더링한다. 40 | - videoItemTemplate() 41 | - 영상 아이템 한 개의 htmlString 42 | - requestVideos(ids) 43 | - async/await 적용, api로부터 fetch하여 받은 영상 목록 배열에 대해 map을 돌려, VideoItem 인스턴스들를 만들고, 이들을 배열로 합쳐서 반환한다. 44 | 45 | - SearchModal 클래스 46 | 47 | - 영상 검색 모달에 대한 view의 역할(이벤트바인딩, 렌더링 등) 및 바인딩된 이벤트의 콜백함수 등이 함께 있다. 48 | - 의존 49 | - VideoItem 클래스에 의존한다. 50 | - App 클래스에도 의존한다고 보여진다. 51 | - 왜냐하면, App 클래스의 인스턴스를 본 클래스에서 전달받고 멤버변수로 할당하여 App의 메서드를 호출하고 있기 때문이다. 52 | - constructor(storage, delegate) 53 | - App 클래스의 constructor에서 SearchModal 클래스의 인스턴스의 인수에 VideoStorage 클래스의 인스턴스와 App의 (미래에 생성될)인스턴스를 전달하였다. 54 | - init() 55 | - App 클래스의 constructor에서 SearchModal의 인스턴스의 init 메서드를 실행했었다. 56 | - 선택한 dom요소(검색버튼, 검색결과)를 멤버변수로 할당하고 각각의 이벤트에 콜백함수(handleClickSearchButton, handleClickVideoList)를 바인딩하였다. 57 | - loadMoreObserver() 메서드를 멤버변수로 할당했다. 58 | - loadMoreObserver() 59 | - IntersectionObserver의 인스턴스를 반환한다. 60 | - requestVideos() 호출 61 | - handleClickSearchButton() 62 | - 검색 버튼 클릭 시 콜백함수, requestVideos() 호출 63 | - handleClickVideoList(event) 64 | - 이벤트 위임을 통해 버튼 태그만 골라서 handleClickSaveButton 메서드에 event 인수를 전달하여 호출한다. 65 | - handleClickSaveButton(event) 66 | 67 | - 저장 버튼 클릭 시 콜백함수 68 | - this.delegate.handleSaveVideo(videoId) 호출 69 | - App 클래스의 메서드이며, 저장 버튼 클릭하면 해당 id를 fetch하여 메인화면(본영상 탭)에 렌더링한다. 70 | 71 | - renderVideoItems(videos) 72 | - handleClickSearchButton, loadMoreObserver에서 호출된다 73 | - renderSkeletonItems(videoCount) 74 | - init, resetSearchResult에서 호출된다 75 | 76 | - Storage 폴더 77 | - LocalStorage 클래스 78 | - constructor(key) 79 | - key 하나만 멤버변수로 갖는다. 80 | - 브라우저의 localStorage 관련 load(), save() 메서드를 갖고 있다. 81 | - VideoStorage 클래스 82 | - model 역할에 가까운 것 같다. 83 | - LocalStorage 클래스를 상속받아서, load(), save() 메서드를 사용한다. 84 | - 멤버변수로 maxCount와 cache를 갖고 있고, cache는 로컬스토리지에서 load된 데이터로 초기화한다. 85 | - saveVideo(videoId) 86 | - toggleWatchStatus(videoId) 87 | - removeVideo(videoId) 88 | - clear() 89 | - VideoItem 클래스 90 | - constructor(item, isWatched)만 존재한다. 91 | - 멤버변수로 id, publishedAt, title 등이 있고, 이 클래스는 인스턴스로 생성될 때마다 개별 영상 아이템 한 개의 프로퍼티들만 관리된다. 92 | - validation 폴더 93 | - constants 폴더 94 | - utils 파일 95 | - requestYoutubeVideos(url, params) 함수 등 96 | 97 | # 2. 피드백 정리 98 | 99 | ## 1. 함수 100 | 101 | ### 1-1. 함수분리 102 | 103 | - [#100] 분리함수 분리 기준 104 | 105 | - 로직이 길어지는 경우(코드 10줄 초과) 106 | - 두 번 이상 재사용될 수 있는 경우 107 | - 그리고 함수가 길다고 느껴진다면 함수 분리 전에, 함수명이 잘못되었을 수도 있으니 함수명과 그 기능을 다시 잘 살펴보자. (MVC에서 controller는 model과 view가 만나는 전쟁터라서 비대해지기 쉽다 😿 ) 108 | 109 | - [#111] 메소드들을 분리하는 실익이 있나요? - [바로가기](https://github.com/woowacourse/javascript-youtube-classroom/pull/111/files/23d6739edc86e14986bd35b4bf2b276833475326#r825246805) 110 | 111 | - 아래 예제 코드에서 생각해볼만한 의견 : "기능별로 분리한 취지는 이해하지만, 실제로는 '초기화' 프로세스 중 일부에 불과하고 달리 따로 호출될 일이 없는데 말이에요. 불필요한 prototype 메모리만 차지하는 것이 아닐까요? 가독성을 위해 희생할 가치가 있다고 보시나요?" 112 | 113 | - 함수를 분리한 경우 114 | 115 | ```js 116 | class MyView1 { 117 | constructor() { 118 | this.render(); 119 | this.bindElements(); 120 | this.addEvents(); 121 | } 122 | 123 | templateList() { 124 | return `
  • A
  • B
  • C
  • D
  • `; 125 | } 126 | 127 | template() { 128 | return `
      ${templateInner()}
    `; 129 | } 130 | 131 | render() { 132 | document.querySelector(".container").innerHTML = this.template(); 133 | } 134 | 135 | bindElements() { 136 | this.$container = document.querySelector(".container"); 137 | this.$button = this.$container.querySelector(".my-btn"); 138 | this.$list = this.$container.querySelector(".my-list"); 139 | } 140 | 141 | addEvents() { 142 | this.$button.addEventListener("click", this.clickHandler); 143 | this.$list.addEventListener("scroll", this.scrollHandler); 144 | } 145 | } 146 | ``` 147 | 148 | - 초기화 단계에서 전부 처리해주는 경우(위 코드와 비교) 149 | 150 | ```js 151 | class MyView2 { 152 | constructor() { 153 | this.$container = document.querySelector(".container"); 154 | this.$container.innerHTML = this.template(); 155 | this.$button = this.$container.querySelector(".my-btn"); 156 | this.$list = this.$container.querySelector(".my-list"); 157 | this.$button.addEventListener("click", this.clickHandler); 158 | this.$list.addEventListener("scroll", this.scrollHandler); 159 | } 160 | 161 | templateList() { 162 | return `
  • A
  • B
  • C
  • D
  • ... `; 163 | } 164 | 165 | template() { 166 | return `
      ${templateInner()}
    ... `; 167 | } 168 | } 169 | ``` 170 | 171 | - 반드시 코드 길이와 가독성이 연관된다고 보긴 어렵다. 172 | - 필요에 따라 적절한 구획을 나누어 줄바꿈 처리만 해주어도 가독성은 월등히 높아질 수 있다. 173 | - 리액트를 예로 들면, 함수형 컴포넌트는 실제로는 '하나의 함수' 이다. return구문의 위쪽에 매우 많은 로직이 들어가게 되는데, 단지 이것만 가지고 가독성이 떨어진다고 하지는 않는다. 174 | 175 | ### 1-2. JSON.parse() 176 | 177 | - [#105] JSON.parse()의 인자로 parse하지 못하는 값이 넘겨서 온다면? 178 | - JSON.parse() 는 문자열을 JSON으로 파싱한다. 이 문자열은 유효한 JSON 형태의 문자열이어야 하며, 유효하지 않을 경우 에러가 발생한다. 179 | - `\r, \n, \t, \f `가 포함되면 안된다. 180 | - 여분의 콤마도 허용하지 않는다. 181 | - 프로퍼티 이름은 반드시 쌍따옴표로 표현해야 한다. 182 | - 01 처럼 리딩제로를 사용할 수 없고, 십진 소수점 뒤에는 최소한 하나의 숫자는 등장해야 한다. 183 | > [MDN-JSON bad parse](https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Errors/JSON_bad_parse) 184 | 185 | ## 2. 클래스와 메서드 186 | 187 | ### 2-1. 이벤트핸들러로 등록할 메소드는 bind보다 class fields(화살표함수) 188 | 189 | - [#111] 이벤트핸들러로 등록할 메소드는 `bind(this)` 대신 class fields에 `화살표함수`를 사용할 것을 추천한다. - [바로가기](https://github.com/woowacourse/javascript-youtube-classroom/pull/111/files/23d6739edc86e14986bd35b4bf2b276833475326#r825246994) 190 | - 왜냐하면 bind(this)를 사용하면 메소드 선언 방식에 의해 prototype에 원본메소드가 남아있는 채로, 실제로 호출되는건 `새로 만들어진 bound 메소드`이다. 사용하지 않을 불필요한 메소드가 남아있어 메모리가 낭비된다고 볼 수 있다. 191 | - 반면 class field를 이용하면 prototype에는 남아있지 않고, 인스턴스 자신이 사용할 함수만 들고 있을 수 있다. 192 | 193 | ## 3. DOM 194 | 195 | ### 3-1. element show/hide 196 | 197 | - [#111] element를 빈번하게 show/hide해야 하는 경우, 해당 element를 삭제했다가 다시 삽입하는 방식보다는 보여줄 요소를 처음부터 맨 뒤에 만들어두고, css로 show/hide만 하는 방식이 성능상 더 좋을 수 있다. - [바로가기](https://github.com/woowacourse/javascript-youtube-classroom/pull/111/files/23d6739edc86e14986bd35b4bf2b276833475326#r825247862) 198 | - [#77] 리스트에 처음부터 skeleton item을 배치해두고 숨김처리한뒤 검색했을 때 skeleton 을 보이게 한 뒤 skeleton 맨 첫요소 앞에 검색 결과 리스트를 insert 해보세요. 그럼 굉장히 심플해져요. [바로가기](https://github.com/woowacourse/javascript-youtube-classroom/pull/77#discussion_r825409052), [커밋](https://github.com/woowacourse/javascript-youtube-classroom/pull/77/commits/9a2b69ff2adf55cdd3b2e1c4a55d360221077a14) 199 | 200 | ### 3-2. button의 default type은? 201 | 202 | - [#105] 브라우저 별로 button의 default type은 다를 수 있으니 반드시 명시해줘야 한다. 203 | - IE에서의 default type은 'button', 다른 브라우저에서는 'submit'이다. 204 | - 따라서 submit 타입으로 사용할 목적이 아니라면, 다른 타입으로 명시해줘야 한다.(그냥 버튼타입을 사용하려면 `type="button"`) 205 | 206 | ## 4. 이벤트 207 | 208 | ### 4-1. 이벤트 위임 209 | 210 | - [#111] 아이템 생성시마다 매번 listener를 등록해주는 것보다는, 211 | 최초 한 번 상위 element에 등록해두고 event delegation을 활용하는 편이 훨씬 좋다. - [바로가기](https://github.com/woowacourse/javascript-youtube-classroom/pull/111/files/23d6739edc86e14986bd35b4bf2b276833475326#r825247408) 212 | 213 | - 예제코드 해석 214 | - $videoList에 videoListTemplate을 만들어서 넣을 것이다. 215 | - videoListTemplate에 있는 각각의 요소들마다 이벤트리스너를 등록하는 것이 아니라, 최초 한 번만 상위 element인 $videoList에 이벤트를 등록함으로써 $videoList 하위로 들어가는 요소들에도 이벤트가 위임되도록 하였다. 216 | 217 | ```js 218 | // 예제코드 219 | class SearchModal { 220 | init() { 221 | this.$videoList = $(".video-list", this.$searchResult); 222 | this.addEvent(); 223 | } 224 | 225 | // 상위 요소인 $videoList에 이벤트리스너 한 번 등록 226 | addEvent() { 227 | this.$videoList.addEventListener("click", event => { 228 | this.handleClickVideoList(event); 229 | }); 230 | } 231 | 232 | // $videoList에 videoListTemplate을 만들어서 넣을 것이다. 233 | renderVideoItems(videos) { 234 | const videoListTemplate = videos 235 | .map(video => { 236 | return `
  • 237 | // 생략 238 |
  • `; 239 | }) 240 | .join(""); 241 | 242 | this.$videoList.insertAdjacentHTML("beforeend", videoListTemplate); 243 | } 244 | 245 | // 인수로 event를 받아 조건을 분기하여 위임을 처리 246 | handleClickVideoList(event) { 247 | const { target } = event; 248 | if (target.classList.contains("video-item__save-button")) { 249 | this.handleClickSaveButton(event); 250 | } 251 | } 252 | } 253 | ``` 254 | 255 | ### 4-2. bindEvent() 같은 메서드로 분리하는 경우 256 | 257 | - [#105] bindEvent를 따로 빼게 되는 경우 - [바로가기](https://github.com/woowacourse/javascript-youtube-classroom/pull/105#discussion_r826553859) 258 | - (1) 클래스 인스턴스를 만들게 될때 멤버변수와 멤버함수를 가지고 메모리에 올라가게 됩니다. 최초 실행 이후 더이상 실행되지 않는 멤버함수를 계속 들고 있을 필요가 없다고 판단됩니다. 259 | - (2) 생성자 호출이후 bindEvent를 stack에 할당하고 호출하는데 이 또한 1번처럼 굳이 한번 더 할 필요없느 불필요한 과정이라고 생각합니다. 260 | - (3) bindEvent를 private으로 은닉하지 않음으로서 메소드가 노출되고 외부에서 잘못 사용할 여지가 있습니다.(1번과 2번은 지나친 성능 고려이긴 합니다) 261 | - 한편 여러 줄이여서 길어진다면 주석을 활용하여 구분, 줄바꿈을 통해서 구분, 새로운 메소드를 활용하여 구분할 수 있습니다. 이부분은 개발자마다 다르겠지만 이때는 bindEvent를 메소드를 따로 빼도 괜찮다고 생각합니다. 262 | 263 | ## 5. 클린코드 264 | 265 | ## 6. CSS, HTML 266 | 267 | ### 6-1. BEM 268 | 269 | - [#100] BEM은 궁극적으로 유니크한 클래스를 보장하기 때문에, BEM을 class name으로 사용한다면 굳이 id를 설정할 필요 없이, 유니크함이 보장되는 class name을 사용하는 것이 낫다. 또한, BEM을 class name으로 제공하셨기에 id과 혼동할 여지를 주지 않는게 중요하다. 270 | 271 | ## 7. 테스트 272 | 273 | ### 7-1. 유닛테스트와 통합(E2E)테스트의 차이를 생각하자 274 | 275 | - [#100] 보통 더미 데이터로 많이 사용하지만 실제 api를 테스트해서 하는 것만큼 통합테스트에 적합한 것이 없다. 여기서 중요한 점은 "유닛 테스트" 와 "통합 테스트"의 차이를 집중해야 한다는 점이다. 276 | 277 | - 비디오의 데이터를 활용해 다양한 메소드나 함수들이 정상동작하는지 여부를 파악하고 싶다면 더미데이터를 이용해서 유닛테스트를 진행하면 될 것 같다. 278 | - 반면, api가 정상적으로 동작되는가에 대한 통합 테스트는 jest의 mock 기능 등을 이용해 진행하는 것이 적합할 것 같다. 279 | 280 | - [#103] `API를 통한 테스트`는 `통합테스트`을 할 때 진행하면 될 것 같고, `유닛테스트`에서는 `더미데이터`를 사용하는 것이 적절할 것 같다. 281 | 282 | - [#112] 더미데이터를 활용하여 checkSearchResult를 테스트하는 것은 의미있는 테스트라 판단합니다. checkSearchResult는 데이터를 가공하는 일을 하고 '단위 테스트'는 그 가공하는 일이 제대로 이루어지는지 테스트하면 됩니다. 유투브같은 외부 API의 데이터 형식이 바뀌는경우는 적으며, API를 업데이트하더라도 하위호환을 고려해서 대응합니다. 한편 실제 전체 프로그램의 작동하는것까지 고려하는것은 E2E테스트를 통해서 확인하는게 맞다고 생각합니다. 283 | 284 | ### 7-2. AAA 테스트 285 | 286 | - [#100] GWT(given, when, then) 및 AAA 테스팅 287 | - Arrange : 테스트에 필요한 데이터를 생성한다. 288 | - Act : 테스트하려는 코드를 실행한다. 289 | - Assert : 예상하는 결과를 확인한다. 290 | 291 | ### 7-3. 오작동하는 케이스를 중심을 테스트하자 292 | 293 | - [#100] 로직의 정상 작동 결과는 하나뿐이지만, 오작동하는 케이스는 수십가지가 넘는다. 294 | - 따라서 유저가 어떤 행동을 했을 때 오작동을 하면 유저에게 그에 대한 피드백이 필요하다. 이러한 오작동 케이스에 대한 수많은 문제를 정상적으로 사용할 수 있도록 유도하는 코드가 필요하다. 295 | - 테스트코드는 오작동 문제 상황에 대한 코드 보완 여부를 협업하는 사람들과 공유하는 성격도 갖고 있다. 296 | - 따라서 에러 상황에 대한 테스트코드를 많이 고민하고 작성수록, 이 테스트코드를 코드단위로 이행시켜서 효율성을 증대시킬 수 있다. 297 | - 결론: 유저 관점에서 유저가 일으킬 수 있는 수많은 오작동 케이스를 커버할 수 있는 테스트코드를 작성하자. 그리고 테스트코드는 에러 상황 커버리지를 코드단위로 이행시키며 다양한 사람들에게 선한 영향력을 줄 수 있음을 염두하고 정성껏 작성하자! 298 | > 문제가 있는 경우에 대한 테스트 작성 예시 : "입력된 검색어가 없거나, 공백으로 입력된 경우 검색이 안되게한다." 299 | 300 | ## 8. 설계 301 | 302 | ## 9. 기타 303 | 304 | ### 9-1. eslint 설정 305 | 306 | - [#100] 불필요한 eslint 설정 제거 307 | 308 | - globals 프로퍼티는 선언되지 않은 전역변수를 사용하는 경우 ESLint 경고가 발생하지 않도록 사용자 전역 변수를 추가하는 곳이다. ESLint는 V8 엔진에 있는 Atomics와 SharedArrayBuffer를 인식하지 못하기 때문에, 이러한 전역 객체를 사용하는 경우 이와 관련한 ESLint 설정을 추가해주는 것 같다. 이와 관련된 기능을 사용하지 않는다면, eslint 설정에 둘 이유가 없다. 309 | 310 | > 참고 311 | > [Eslint what is meaning of Atomics Read only and Shared Array Buffer Readonly](https://www.onooks.com/tag-globals-atomics-readonly/) > [MDN-Atomics](https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Atomics) 312 | 313 | ```js 314 | globals: { 315 | Atomics: 'readonly', 316 | SharedArrayBuffer: 'readonly', 317 | }, 318 | ``` 319 | 320 | ### 9-2. prettier 설정 321 | 322 | - [#84] `trailingComma: all` 의 장점은? 323 | - trailingComma를 하게되면 코드리뷰 시점에 이전 코드 부분에 `, `가 추가가 안되므로 더 편하게 수정된 부분만 확인할수도 있는 등, 다양한 이점이 있어요! 324 | -------------------------------------------------------------------------------- /youtube/step1/movie.md: -------------------------------------------------------------------------------- 1 | # LEVEL 1 나만의 유튜브 강의실 STEP 1 - 무비😎 2 | 3 | ## 분석 담당 코드 4 | 5 | - 무비 [PR 104](https://github.com/woowacourse/javascript-youtube-classroom/pull/104) X 티거 [PR 101](https://github.com/woowacourse/javascript-youtube-classroom/pull/101) 6 | - 우디 [PR 107](https://github.com/woowacourse/javascript-youtube-classroom/pull/107) X 시지프 [PR 113](https://github.com/woowacourse/javascript-youtube-classroom/pull/113) 7 | - 하리 [PR 79](https://github.com/woowacourse/javascript-youtube-classroom/pull/79) X 민초 [PR 94](https://github.com/woowacourse/javascript-youtube-classroom/pull/94) 8 | - 비녀 [PR 108](https://github.com/woowacourse/javascript-youtube-classroom/pull/108) X 코이 [PR 92](https://github.com/woowacourse/javascript-youtube-classroom/pull/92) 9 | 10 |
    11 |
    12 | 13 | ## 아키텍처 분석 14 | 15 | ### [[#104](https://github.com/woowacourse/javascript-youtube-classroom/pull/104) X [#101](https://github.com/woowacourse/javascript-youtube-classroom/pull/101)] 무비 X 티거 16 | 17 | - UI를 위해 custom element 사용 18 | - HTMLElement -> 상속 -> CustomElement -> 상속 19 | - UI에서 발생한 이벤트 처리를 위해 custom event사용 20 | - 데이터 관리를 위한 store가 존재하는데, store는 데이터 변경이 있으면 element들에게 알림을 준다. 21 | - 로직에 대한 코드는 `domains`에 존재한다. 22 | 23 |
    24 | 25 | - **store** 26 | - 비디오에 변경사항이 있으면 구독자들(elements)에게 알린다. 27 | - 비디오에 대한 정보를 저장하고 관리한다. 28 | - **domain** 29 | - element에게 발생한 이벤트를 구독한다. 30 | - 이벤트 발생을 처리하고 store에게 dispatch 요청을 보낸다. 31 | - **element** 32 | - 이벤트 발생을 domain에게 알린다. 33 | - store에서 notify를 주면 UI를 렌더링한다. 34 | 35 |
    36 | 37 | #### store 38 | 39 | - 어디서나 접근할 수 있는 싱글톤 패턴 사용 40 | 41 | ```js 42 | static _instance = null; 43 | 44 | static get instance() { 45 | if (!VideoStore._instance) { 46 | VideoStore._instance = new VideoStore(); 47 | } 48 | return VideoStore._instance; 49 | } 50 | ``` 51 | 52 | - store를 element들이 구독하게 하여 데이터 변경이 일어나면 구독자들에게 알림을 준다. 53 | 54 | ```js 55 | #videos = []; 56 | 57 | #subscribers = []; // elements 58 | 59 | subscribe(element) { 60 | this.#subscribers.push(element); 61 | // element들이 store를 구독한다. 62 | } 63 | 64 | dispatch(action, data) { 65 | const newVideos = [...this.getVideos(), ...data]; 66 | 67 | this.#setVideos(newVideos); 68 | // 데이터 변경이 있으면 69 | 70 | this.#subscribers.forEach((subscriber) => { 71 | subscriber.notify(action, data); 72 | // 구독자들에게 알린다. 73 | }); 74 | } 75 | ``` 76 | 77 | #### custom element 78 | 79 | - custom element 활용을 위한 추상 클래스인 `CustomElement`가 존재하고 이를 상속 받아 여러 element들을 만들었다. 80 | 81 | ```js 82 | // abstract class 83 | // parent class 84 | class CustomElement extends HTMLElement {} 85 | 86 | // child class 87 | class MyClassroom extends CustomElement {} 88 | class SearchResult extends CustomElement {} 89 | ``` 90 | 91 |
    92 | 93 | ### [[#107](https://github.com/woowacourse/javascript-youtube-classroom/pull/107) X [PR 113](https://github.com/woowacourse/javascript-youtube-classroom/pull/113)] 우디 X 시지프 94 | 95 | > 기존에 MVC 패턴으로 코드를 짜왔는데, 이벤트가 발생할 때 데이터를 바꾸어주고, UI를 바꾸어 주는 작업이 조금 번거롭다고 생각했습니다. 그래서 React처럼 상태 정의하고, 상태가 변하면 UI가 바뀌게 짜기로 했습니다 96 | 97 | - 리액트처럼 짜려고 한 코드! 98 | - UI를 컴포넌트화 99 | - `component`라는 부모 클래스(core)가 존재하고 만들어지는 component들이 이를 상속받는 구조 100 | - component의 생성자에는 target(component의 element)와 props를 넘겨준다. 101 | - component에서 써야하는 함수 등을 `props`로 넘겨준다. 102 | - component는 리액트와 비슷하게 `beforeMounted()`, `afterMounted()` 등의 함수가 존재한다. 103 | - state 104 | - component만의 지역 상태 존재 105 | - 전역 state는 rootStore에서 관리하고 state가 변경되었을 때 구독한 컴포넌트를 재 렌더링한다. 106 | 107 |
    108 |
    109 | 110 | ## 피드백 정리 111 | 112 | 😛 링크를 누르면 해당 discussion으로 이동! 😛 113 | 114 |
    115 | 116 | ### 구조 117 | 118 | [[#94](https://github.com/woowacourse/javascript-youtube-classroom/pull/94#discussion_r827920206)] 모듈 간의 연결 방식은 통일 해주자! (어떤 모듈은 클래스의 멤버변수로 지정해주고, 어떤 모듈은 import해주는 방식이 혼용되면 일관성이 떨어진다.) \* 119 | 120 | ### 함수 121 | 122 | [[#101](https://github.com/woowacourse/javascript-youtube-classroom/pull/101#discussion_r825252118)] 생성자에서만 호출하는 함수는 분리해주지 않아도 된다. 123 | 124 | [[#101](https://github.com/woowacourse/javascript-youtube-classroom/pull/101#discussion_r825260434)] 함수의 인자는 [2개 이하](https://github.com/qkraudghgh/clean-code-javascript-ko#%ED%95%A8%EC%88%98-%EC%9D%B8%EC%9E%90%EB%8A%94-2%EA%B0%9C-%EC%9D%B4%ED%95%98%EA%B0%80-%EC%9D%B4%EC%83%81%EC%A0%81%EC%9E%85%EB%8B%88%EB%8B%A4)가 적절하다. 125 | 126 |
    127 | 128 | ### 클래스 129 | 130 | [[#101](https://github.com/woowacourse/javascript-youtube-classroom/pull/101#discussion_r827513622)] 클래스에서 불필요한 getter와 setter는 지양하자. \* 131 | 132 | > 객체의 상태가 변경되는 것은 객체 스스로의 행동에 의해서야 한다. => 느슨한 결합과 유연한 협력 - "객체지향의 사실과 오해" 133 | 134 |
    135 | 136 | ### 상수화 137 | 138 | [[#94](https://github.com/woowacourse/javascript-youtube-classroom/pull/94#pullrequestreview-911446808)] threshold나 snackbar의 노출 시간 등 이후에 충분히 옵션값으로 자주 수정할 수도 있을 부분들, 의미있게 사용되는 부분들에 대해서는 중복으로 사용되지 않더라도 의미 있는 이름을 지어준다는 취지에서 상수화를 고려해보자. 139 | 140 | [[#108](https://github.com/woowacourse/javascript-youtube-classroom/pull/108#discussion_r825376456)] LocalStorage에 사용하는 Key는 상수로 관리하자. 물론 쓰이는 Key가 하나라면 과도하게 분리하진 않아도 된다. 관리가 힘들어질때 분리해보자. 141 | 142 |
    143 | 144 | ### OOP 145 | 146 | [[#101](https://github.com/woowacourse/javascript-youtube-classroom/pull/101#discussion_r825257497)] 도메인에 한정되어 사용되는 메서드라면 `private`를 고려해보자. 147 | 148 |
    149 | 150 | ### HTML 151 | 152 | [[#94](https://github.com/woowacourse/javascript-youtube-classroom/pull/94#discussion_r827932240)] `input`태그의 값이 필수적이라면 [required](http://www.tcpschool.com/html-tag-attrs/input-required)를 지정해주는 것을 고려해보자. \* 153 | 154 | ```html 155 | 156 | 157 | ``` 158 | 159 | [[#108](https://github.com/woowacourse/javascript-youtube-classroom/pull/108#discussion_r825376270)] 태그의 속성 중 [`role`](https://happycording.tistory.com/entry/HTML-Role-%EC%99%9C-%EC%82%AC%EC%9A%A9%ED%95%B4%EC%95%BC%EB%A7%8C-%ED%95%98%EB%8A%94%EA%B0%80)은 element에게 명확한 의미를 부여해준다. (웹 접근성을 위한 속성) \* 160 | 161 |
    162 | 163 | ### 이벤트 164 | 165 | [[#79](https://github.com/woowacourse/javascript-youtube-classroom/pull/79#discussion_r825301847)] window에 eventListener를 붙일 수가 있는데 window는 browser 내에서 모두가 공유하는 최상단 객체인데 이곳에 eventListener를 붙이는건 성능적인 면에서, 또 안정성 면에서 좋지 않다. 본인이 의도한 영역으로 최소화 하자. 166 | 167 | [[#94](https://github.com/woowacourse/javascript-youtube-classroom/pull/94#discussion_r827895129)] keyCode는 현재 [deprecated](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/keyCode)! 대신 [key](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key)를 사용해보자. \* 168 | 169 | [[#94](https://github.com/woowacourse/javascript-youtube-classroom/pull/94#discussion_r827900878)] 이벤트 위임을 할 때 이벤트 타겟이 특정 class를 가지고 있냐에 따라 이벤트를 적용해줄 수 있다. \* 170 | 171 | ```js 172 | if (e.target.classList.contains('video-item__save-button')) { 173 | video.save(e); 174 | } 175 | ``` 176 | 177 | [[#94](https://github.com/woowacourse/javascript-youtube-classroom/pull/94#discussion_r827904507)] 도메인에 event객체를 모두 넘겨주지 말고 필요한 데이터만 넘겨주자. event 객체라는 건 UI 영역에서만 알면 되는 정보이기 때문. \* 178 | 179 |
    180 | 181 | ### 유용한 정보 182 | 183 | [[#101](https://github.com/woowacourse/javascript-youtube-classroom/pull/101#discussion_r825256475)] 삼항 연산자의 조건과 리턴값이 동일하다면 병합 연산자를 사용해보자 \* 184 | 185 | ```js 186 | // 삼항 연산자 187 | return localStorage.getItem('videos') 188 | ? JSON.parse(localStorage.getItem('videos')) 189 | : []; 190 | 191 | // 병합 연산자 192 | return JSON.parse(localStorage.getItem('videos') ?? '[]'); 193 | ``` 194 | 195 | [[#113](https://github.com/woowacourse/javascript-youtube-classroom/pull/113#discussion_r826976214)] 테스트를 진행할 떄 CLI만 사용해서는 코드레벨에서의 에러를 확인하기 어려운 경우가 많다. jest plug-in과 같은 도구를 사용해보자. \* 196 | 197 | [[#94](https://github.com/woowacourse/javascript-youtube-classroom/pull/94#discussion_r825287822)] export를 묶어서 해줄 수도 있다. \* 198 | 199 | ```js 200 | const MAX_SAVE_COUNT = 100; 201 | 202 | const STORAGE_KEY = 'videoId'; 203 | 204 | export { MAX_SAVE_COUNT, STORAGE_KEY }; 205 | ``` 206 | 207 |
    208 | 209 | ### API 통신 210 | 211 | [[#104](https://github.com/woowacourse/javascript-youtube-classroom/pull/104#pullrequestreview-908015752)] 너무 잦은 호출에 대한 문제 212 | 213 | 1. 무한 스크롤로 구현했을 시 스크롤 이벤트가 발생했을 경우 바로 API가 호출된다. -> 유저가 스크롤을 한번에 훅 넘겼을 때, 그만큼의 요청이 서버에 전달된다. 214 | 2. 스크롤의 접점에서 여러 번의 스크롤 이벤트가 발생할 수 있다. (바운싱 현상) 215 | 216 | 이 모든 문제는 [debounce와 throttle](https://techcourse.woowahan.com/s/dSWvXWYI/ls/Is8GTQl7)를 통해 해결가능하다. 217 | 218 | [[#107](https://github.com/woowacourse/javascript-youtube-classroom/pull/107#discussion_r825312544)] 웹 성능 최적화를 위해 현재 화면에 보여지지 않는 이미지를 추후에 로딩하여 초기 로딩시간을 줄이는 [lazy loading기법을 간단히 태그의 loading 속성을 통해 적용해줄 수 있다.](https://web.dev/i18n/ko/browser-level-image-lazy-loading/) \* 219 | 220 | ```html 221 | 222 | ``` 223 | 224 | [[#94](https://github.com/woowacourse/javascript-youtube-classroom/pull/94#discussion_r827880419)] API요청 URL을 작성할 때 파라미터가 많다면 URL이 복잡해지고, 파라미터를 변경해야할 때 어려움이 있다. 이럴 때 [URLSearchParams](https://developer.mozilla.org/ko/docs/Web/API/URLSearchParams)와 같은 API를 적용해보자. \* 225 | 226 | [[#94](https://github.com/woowacourse/javascript-youtube-classroom/pull/94#discussion_r827909437)] Promise체인 내에 async/await가 혼용되면 어색할 수 있다. 227 | 228 |
    229 | 230 | ### 테스트 231 | 232 | [[#79](https://github.com/woowacourse/javascript-youtube-classroom/pull/79#discussion_r825298920)] 테스트를 위해 직접 mock객체를 만드는 경우가 있다. 하지만 이 경우에 테스트하는 객체와 mock객체의 규격이 동일하지 않다면 실제 코드에서는 테스트가 실패할 수 있어 테스트 코드가 무의미해진다. \* 233 | 234 |
    235 | 236 | ### Custom Element 237 | 238 | [[#104](https://github.com/woowacourse/javascript-youtube-classroom/pull/104#pullrequestreview-908015752)] custom element를 사용했을 경우 모든 element에 대한 `import`를 `index.js`에서 해주는 것은 구조가 커져 custom element가 많아졌을 경우 적절하지 못하다. 사용하는 곳에서 `import`를 해주자. 239 | 240 |
    241 | 242 | ### Prettier 243 | 244 | [[#113](https://github.com/woowacourse/javascript-youtube-classroom/pull/113#discussion_r825396404)] prettier에서는 기본 설정이 존재하고, 이 기본 설정을 그대로 따르고 싶을 수 있다. 하지만 기본 설정을 모두 외우고 있는 개발자는 드물고 기본 설정값을 바뀔 수 있기 때문에, 기본 설정이라도 명시해주는 것이 좋다. \* 245 | -------------------------------------------------------------------------------- /youtube/step2/hope.md: -------------------------------------------------------------------------------- 1 | # Level1 Youtube Step2(담당 PR 번호들) - 호프 2 | 3 | - 분석 담당 코드 4 | 5 | - 호프 [#121](https://github.com/woowacourse/javascript-youtube-classroom/pull/121) 6 | - 콤피 [#141](https://github.com/woowacourse/javascript-youtube-classroom/pull/141) 7 | - 돔하디 [#149](https://github.com/woowacourse/javascript-youtube-classroom/pull/149) 8 | - 블링 [#126](https://github.com/woowacourse/javascript-youtube-classroom/pull/126) 9 | - 밧드 [#125](https://github.com/woowacourse/javascript-youtube-classroom/pull/125) 10 | - 나인 [#132](https://github.com/woowacourse/javascript-youtube-classroom/pull/132) 11 | - 안 [#152](https://github.com/woowacourse/javascript-youtube-classroom/pull/152) 12 | 13 |
    14 | 15 | # 피드백 정리 16 | ## 1. 구조 17 | - [#149] storage 위에도 대략 설명을 드렸지만 해당 유튜브 앱의 영속성을 담당하는 중요한 부분입니다. 하지만 네이밍과는 다르게 도메인에 종속성이 있어서 재활용이 어려울 것 같네요 18 | - [#126] view 들을 관장하는 view 클래스가 있는 경우- 보통 각 뷰는 독립적이어야 하는데 뷰들을 관장하는 뷰가 있다는건 뷰의 역할은 아닌 것 같다. - [바로가기](https://github.com/woowacourse/javascript-youtube-classroom/pull/126#discussion_r831918465) 19 | 20 |
    21 | 22 | 23 | ## 2. DOM & HTML 24 | - [#149] event.target.parentNode.parentNode.remove(); 25 | - 할아버지까지 찾고 있군요. 만약 기획이나 디자인 수정이 생기면 해당 로직은 의미가 있을까요? - [바로가기](https://github.com/woowacourse/javascript-youtube-classroom/pull/149#discussion_r830591414) 26 | 27 | - [#149] **dataset 속성 남용** - DOM에 꽤나 많은 데이터를 때려넣는 느낌이다. 좋은 시도이지만 의존도가 점점 심해지고 있다. 브라우저 검사도구로 사용자가 무난하게 조작도 가능한 부분이다. -[바로가기](https://github.com/woowacourse/javascript-youtube-classroom/pull/149#pullrequestreview-915080811) 28 | 29 | - [#152] 본 영상 & 볼 영상 체크 할 때, 클래스 토글이 아니라 radio checked 속성 이용 가능 - [바로가기](https://github.com/woowacourse/javascript-youtube-classroom/pull/152#discussion_r832467573) 30 | ```javascript 31 | 32 | 33 | 34 | 35 | 36 | ``` 37 | 38 |
    39 | 40 | 41 | ## 3. 네이밍 42 | - [#141] 간결한 용어사용보다 훨씬 중요한 것이 ‘문맥에 따라 충분히 유추가능한 어휘 사용’이다. 단어가 길어지더라도 누구든 그 의미를 파악할 수 있도록 보다 친절하게 작성해주시길 - [바로가기](https://github.com/woowacourse/javascript-youtube-classroom/pull/141#pullrequestreview-915072062) 43 | 44 | - [#149] 관계에 집중하는 방법 vs 행동과 명확함에 집중하는 방법 - [바로가기](https://github.com/woowacourse/javascript-youtube-classroom/pull/149#discussion_r830591040) 45 | - 행동과 명확함에 집중하기 : `savedVideos & videoToSave` 46 | - 관계에 집중하기 : `savedVideos & savedVideo` 47 | 48 | - [#132] 목데이터에는 가공 후 값이 들어야가야 할까, 가공 전 값이 들어가야 할까? -[바로가기](https://github.com/woowacourse/javascript-youtube-classroom/pull/132#discussion_r831025225) 49 | - 가공 후 값이 들어간다면 기획자가 가공 방법을 변경 할 때마다 목데이터를 한땀 한땀 바꿔줘야 한다. 50 | - 따라서, 가공 전 값을 목데이터로 넣고, 테스트에서 가공해주는게 좋다. 51 | 52 | 53 |
    54 | 55 | ## 4. 테스트 56 | - [#149] cypress viewport 수정 - [바로가기](https://github.com/woowacourse/javascript-youtube-classroom/pull/149#discussion_r830591830) 57 | - `cy.viewport(1536, 960);` 58 | - 주어진 검색 모달창의 크기가 width: 1080px; height: 727px; 인데 cypress 디폴트 뷰포트 크기가 조금 작아서 모달창의 바깥 영역을 클릭하는 것에 실패함. 그래서 cypress 테스트를 진행할 때 검색모달의 바깥부분을 클릭할 수 있도록 수정 59 | 60 |
    61 | 62 | ## 5. 함수 63 | - [#149] renderNoSavedVideo (저장된 영상 없음 상태 렌더링) & renderSavedVideo(저장된 영상 있을시 상태 렌더링)로 가져가기 보다는 renderVideo & renderSavedVideo 로 가져가는게 더 효율적이지 않은가? 렌더하는 비디오의 종류가 3가지 이상이 아닌데 왜 No flag 를 써야하나 - [바로가기](https://github.com/woowacourse/javascript-youtube-classroom/pull/149#discussion_r830593341) 64 | - 크루: ‘저장된 비디오가 없습니다’를 보여주는 기능이 renderNoSavedVideo인데, renderSavedVideo와 정반대 의미를 가지고 있다고 생각했다. 제안해주신 renderVideo와 renderSavedVideo로 가져가나면 renderVideo가 하는 일은 무엇? 65 | - 리뷰어 : 그럼 앞으로 기능이 확장되고, 비슷한 리스트가 늘어날 때 마다 모든 리스트에 no-상태를 추가할것인가? 66 | - 즉, 기능 확장에 의해서 어떤 비디오 리스트가 다양해질 수 있는데 그럴 때마다 noSomeVideos 라고 플래그를 계속 만드는 것이 문제가 될 수가 있다. 67 | - [#132] 데이터를 forEach 로 돌면서 여러번 insertAdjacentHTML을 해주는 상황, 아래와 같이 map 으로 한번만 insertAdjacentHTML 실행하도록 리팩토링 가능 - [바로가기](https://github.com/woowacourse/javascript-youtube-classroom/pull/132#discussion_r831009399) 68 | ```javascript 69 | const template = videoStorage.getVideoDataList().map(videoData => { 70 | return `템플릿만들기` 71 | }) 72 | this.$storedVideoList.insertAdjacentHTML('beforeend', template.join('')); 73 | 74 | ``` 75 | - [#132] forEach 내부의 return 문은 continue 같은 역할을 한다. - [바로가기](https://github.com/woowacourse/javascript-youtube-classroom/pull/132#discussion_r831011837) 76 | - [#132] 구조분해할당 활용하기 77 | ```javascript 78 | 79 | // before 80 | videoId: clickedVideo.children[4].dataset.videoid, 81 | publishedAt: clickedVideo.children[3].textContent, 82 | title: clickedVideo.children[1].textContent, 83 | url: clickedVideo.children[0].src, 84 | channelTitle: clickedVideo.children[2].textContent, 85 | 86 | // after 87 | const [thumbnailImg, title, channelTitle, publishedAt, videoId] = videoInfo.children; 88 | return { 89 | videoId: videoId.dataset.videoId, 90 | publishedAt: publishedAt.textContent, 91 | title: title.textContent, 92 | url: thumbnailImg.src, 93 | channelTitle: channelTitle.textContent, 94 | type: VIDEO_TYPE.WATCH_LATER, 95 | }; 96 | } 97 | 98 | ``` 99 | 100 | 101 |
    102 | 103 | 104 | ## 6. CSS 105 | - [#126] css 로 화면에 보이는 최대 글자수 지정하기 - [바로가기](https://github.com/woowacourse/javascript-youtube-classroom/pull/126#discussion_r830735124) 106 | ```css 107 | .search-result-keyword { 108 | width: 300px; 109 | text-overflow: ellipsis; 110 | overflow: hidden; 111 | white-space: nowrap; 112 | } 113 | ``` 114 | 115 |
    116 | 117 | ## 7. 성능 118 | - [#152] *N회 렌더링하는 문제* 백엔드 개발자는 서버의 리소스를 먹고 프론트엔드 개발자는 사용자의 리소스를 먹습니다. 그런데 유튜브 미션 요구사항을 수행하며 (스크롤 + Data Fetch + DOM Append)을 일으키고 있는데, 여기서 가장 심각한 실수가 N회 렌더링하는 것입니다.나중에 분명 Reflow & Repaint를 공부하실텐데 꼭 이 리뷰를 기억해주세요. - [바로가기](https://github.com/woowacourse/javascript-youtube-classroom/pull/152#pullrequestreview-916826903) 119 | 120 | ### reflow / repaint는? 121 | ![](https://developers.google.com/web/fundamentals/performance/rendering/images/intro/frame-full.jpg) 122 | - reflow : 생성된 DOM 노드의 레이아웃 수치(너비,높이,위치) 변경 시 영향받은 모든 노드의 수치를 다시 계산하여, 렌더트리를 재생성하는 과정 (Layout) 123 | - repaint : reflow 과정이 끝난 후 재생성된 렌더트리를 다시 그리는 과정 (Paint) 124 | 125 | ### reflow 는 언제 발생하나? 126 | - 노드 추가 제거 127 | - 요소 위치 변경 128 | - 요소 크기 변경 (margin,padding,border,width,height 등) 129 | - 페이지 초기 랜더링 130 | - 윈도우 리사이징 131 | 132 | ### repaint 만 일어나는 경우 133 | - 화면의 구조가 변경되지 않은, 화면 변화의 경우 134 | - css의 opacity, background-color, visibility, outline 속성들 135 | 136 | ### reflow 최적화 방법 몇가지 (DOM관련) 137 | - 최대한 가장 하위노드만 변경하기 138 | - 상위노드가 변경되면, 상위노드부터 -> 자식노드까지 모두 변경되므로, 가장 하위 노드만 변경한다면 리플로우 범위를 줄일 수 있다. 139 | - DOM 사용 최소화 하기 140 | - DOM Fragment를 사용하여 DOM 을 추가 할 때, DOM 접근 최소화 하기 141 | ```javascript 142 | const frag = document.createDocumentFragment(); 143 | const ul = frag.appendChild(document.createElement('ul')); 144 | 145 | for (let i = 1; i <= 3; i++) { 146 | li = ul.appendChild(document.createElement('li')); 147 | li.textContent = `item ${ i }`; 148 | } 149 | 150 | document.body.appendChild(frag); 151 | ``` 152 | 153 |
    154 | 155 | ## 8. 그 외 156 | - [#129] localStorage 에서 배열인 json 데이터를 꺼내오는건 비용이 크다. 프로젝트에서 storage 와 같은 데이터를 관리해보는게 좋다. 157 | 실제 localStorage 데이터를 수정해야할 경우에만 localStorage 에 접근하고, 데이터 가져오는 일은 서비스 처음 시작할 때 한번만 해보도록 한다. - [바로가기](https://github.com/woowacourse/javascript-youtube-classroom/pull/126#discussion_r831002052) 158 | 159 | - [#129] 가독성을 위해 선언부가 끝나고 나면 한 번 개행해주시면 어떨까요? - [바로가기](https://github.com/woowacourse/javascript-youtube-classroom/pull/126#discussion_r831034576) 160 | 161 |
    162 | 163 | ## 좋은 말 164 | - [#121] 개인적으로는 ‘효율’은 ‘완성’이후에 고려해도 늦지 않다고 생각해요! -------------------------------------------------------------------------------- /youtube/step2/kkojae.md: -------------------------------------------------------------------------------- 1 | # STEP2. 나만의 유튜브 강의실 정리 2 | 3 | --- 4 | 5 | ## 담당 크루 6 | 7 | --- 8 | 9 | - 준찌 [#117](https://github.com/woowacourse/javascript-youtube-classroom/pull/83) 10 | - 빅터 [#128](https://github.com/woowacourse/javascript-youtube-classroom/pull/90) 11 | - 꼬재 [#122](https://github.com/woowacourse/javascript-youtube-classroom/pull/98) 12 | - 우연 [#133](https://github.com/woowacourse/javascript-youtube-classroom/pull/109) 13 | - 유세지 [#140](https://github.com/woowacourse/javascript-youtube-classroom/pull/99) 14 | - 온스타 [#120](https://github.com/woowacourse/javascript-youtube-classroom/pull/87) 15 | - 아놀드 [#154](https://github.com/woowacourse/javascript-youtube-classroom/pull/106) 16 | 17 | ## 피드백 정리 18 | 19 | --- 20 | 21 | ### JS 22 | 23 | #### [#122](https://github.com/woowacourse/javascript-youtube-classroom/pull/122#discussion_r831122491) 외부와 강하게 결합되어있을 수록 변경에 취약해 보통 레이어 하나 정도를 두어 유연하게 작성하는게 좋다. 24 | 25 | 변경 전 26 | 27 | ```js 28 | const videoStorage = { 29 | getVideo() { 30 | return JSON.parse(localStorage.getItem("saveVideoData")) || []; 31 | }, 32 | }; 33 | ``` 34 | 35 | 좋은 예가 아니다...!! 36 | 변경 후 37 | 38 | ```js 39 | export default class VideoStorage { 40 | #videos = JSON.parse(localStorage.getItem("saveVideoData")) || []; 41 | 42 | getVideo() { 43 | return this.#videos; 44 | } 45 | 46 | setVideo() { 47 | localStorage.setItem("saveVideoData", JSON.stringify(this.#videos)); 48 | } 49 | } 50 | ``` 51 | 52 | 네이밍도 동일하다. 53 | Storage라는 이름일 경우 localStorage가 API로 변경되었을 경우를 생각한다면, Videos 라는 이름을 갖는게 좋다! 54 | 55 | #### [#133](https://github.com/woowacourse/javascript-youtube-classroom/pull/133#discussion_r831685384) 단방향 / 양방향 바인딩 56 | 57 | 단방향 데이터 바인딩 58 | 59 | - 데이터와 템플릿을 결합하여 화면을 생성한다. 60 | - UI에서 받아온 데이터를 멤버 변수 혹은 도메인에서 상태를 관리하지 않고 곧 바로 render 해준다. 61 | - react는 단방향 데이터 바인딩을 한다. 62 | 63 | 양방향 데이터 바인딩 64 | 65 | - 데이터의 변화를 감지해 템플릿과 결합하여 화면을 갱신하고 화면에서의 입력에 따라 데이터를 갱신한다. 66 | - 즉, 데이터와 화면 사이의 데이터가 계속해서 일치하게 되는 것이다. (UI와 domain의 데이터가 일치) 67 | - Vue는 양방향 데이터 바인딩을 한다. 68 | 69 | > [facebook article](https://facebook.github.io/flux/docs/in-depth-overview/) 70 | 71 | #### [#133](https://github.com/woowacourse/javascript-youtube-classroom/pull/133#discussion_r831692497) 인스턴스를 여러군데에서 사용할 경우 인스턴스를 생성해서 export 한다. 72 | 73 | 클래스 맴버 변수로 가지고 있을 이유가 없을 경우 아래와 같이 인스턴스를 export 해주면 필요한 곳에서 사용해 줄 수 있다. 74 | 75 | ```js 76 | class YoutubeAPI {} 77 | const youtubeAPI = new YoutubeAPI(); 78 | export default youtubeAPI; 79 | ``` 80 | 81 | #### [#120](https://github.com/woowacourse/javascript-youtube-classroom/pull/120#issuecomment-1073419431) localStorage 접근 또한 비용이 많이 든다. 82 | 83 | localStorage에 불필요하게 접근하고 있는건 아닌지?! 확인해 볼 필요가 있다. 84 | 85 | #### [#128](https://github.com/woowacourse/javascript-youtube-classroom/pull/128#discussion_r830633687) 저장된 video들이 많아 질 경우 배열로 관리하는 것이 좋을까? 86 | 87 | 하나의 video를 고치기 위해 모든 배열을 순회할 필요가 있을까?! 88 | 89 | - 배열이 아닌 객체로 변경 90 | 91 | #### [#117](https://github.com/woowacourse/javascript-youtube-classroom/pull/117#discussion_r830470065) Promise.all 사용 92 | 93 | localStorage에 id값만 저장하고 이후 id 값들로 API 호출을 다시 한다. 94 | 95 | ```js 96 | const savedVideoIdList = 97 | localStorageUtil.getArrayData(LOCAL_STORAGE_UTIL_KEYS.SAVED_VIDEO_LIST_KEY) ?? 98 | []; 99 | setState(STATE_STORE_KEY.IS_SAVED_VIDEO_WAITING, true); 100 | 101 | const savedVideoList = await Promise.all( 102 | savedVideoIdList.map((savedVideoId) => this.requestVideoById(savedVideoId)) 103 | ); 104 | 105 | async requestVideoById(id) { 106 | const videoResult = await youtubeAPIFetcher({ 107 | path: API_PATHS.GET_VIDEO, 108 | params: { 109 | id, 110 | part: 'snippet', 111 | }, 112 | }); 113 | 114 | const { 115 | items: [videoInfos], 116 | } = parserVideos(videoResult); 117 | 118 | return Video.create({ ...videoInfos, videoId: id }); 119 | } 120 | ``` 121 | -------------------------------------------------------------------------------- /youtube/step2/lokba.md: -------------------------------------------------------------------------------- 1 | # Level1 Youtube Step2(담당 PR 번호들) - 록바 2 | 3 | - 분석 담당 코드 4 | 5 | - 록바 [#138](https://github.com/woowacourse/javascript-youtube-classroom/pull/138) 6 | - 앨버 [#124](https://github.com/woowacourse/javascript-youtube-classroom/pull/124) 7 | - 태태 [#118](https://github.com/woowacourse/javascript-youtube-classroom/pull/118) 8 | - 결 [#115](https://github.com/woowacourse/javascript-youtube-classroom/pull/115) 9 | - 자스민 [#116](https://github.com/woowacourse/javascript-youtube-classroom/pull/116) 10 | - 동키콩 [#135](https://github.com/woowacourse/javascript-youtube-classroom/pull/135) 11 | - 도리 [#127](https://github.com/woowacourse/javascript-youtube-classroom/pull/127) 12 | - 소피아 [#136](https://github.com/woowacourse/javascript-youtube-classroom/pull/136) 13 | 14 |
    15 | 16 | ## 피드백 정리 17 | 18 | ## 대분류(ex: 아키텍처, 함수/클래스, 컨벤션, DOM, 테스트 등) 19 | 20 | ### 1. 아키텍처 21 | 22 | - [#135] [크루원 질문] 구조를 만들 때 어떻게 진행하는게 올바른 방법이라 생각하시는지 구조를 만들 때 중점 사항을 두시는 부분이 있으신지 궁금합니다. - [바로가기](https://github.com/woowacourse/javascript-youtube-classroom/pull/135#pullrequestreview-915496528) 23 | 24 | - [리뷰어님 답변] 25 | 26 | ``` 27 | 세상에 완벽한 아키텍처는 존재하지 않습니다. 28 | 또, 처음부터 좋은 아키텍처를 만들수도 없죠. 29 | 첫 시작은 하신 것 처럼 필요로 인해 이렇게 나눠보시는 훈련을 하는거라 생각하시면 되겠습니다. 30 | 31 | 그렇기에 "구조를 막 짜보고 부숴보라"고 권해드리는 것이고, 다른 코치님들도 동일한 답변을 32 | 하실 것 같아요~ 33 | 34 | 그리고 가장 중요한 것은, 35 | 아키텍처는 프로그래밍 개발 지식만으론 만들어지지 않는다는걸 이해하셔야합니다. 36 | 오늘 만드셨던 "나만의 유튜브 강의실"의 경우 지금 만드신 아키텍처가 적합한지 다시 생각해볼 필요가 있어요. 37 | 38 | "나만의 유튜브 강의실"은 어떤 도메인이고, 특성을 갖고있을까요? 39 | 그리고 함께 개발하는 인원은 몇명인가요? 개발 기간은 언제까지일까요? 40 | 이런 것처럼 다양한 환경적 요인과 비즈니스를 성사시키기 위해 아키텍처라는 틀을 만들고 41 | 협업/목표를 이루기 위한 수단을 만드는 거라 이해하시면 되겠습니다. 42 | ``` 43 | 44 | --- 45 | 46 | ### 2. 클래스 47 | 48 | #### 2.1 내장 메소드 49 | 50 | - [#115] classList toggle을 적극 활용해라 - [바로가기](https://github.com/woowacourse/javascript-youtube-classroom/pull/115#discussion_r830433395) 51 | 52 | ```jsx 53 | //수정 전 코드 54 | if (this.willWatchVideoList.children.length !== 0) { 55 | this.element.classList.add("hide-element"); 56 | } else if (this.willWatchVideoList.children.length === 0) { 57 | this.element.classList.remove("hide-element"); 58 | } 59 | 60 | //수정 후 코드 61 | this.element.classList.toggle( 62 | "hide-element", 63 | this.willWatchVideoList.children.length !== 0 64 | ); 65 | ``` 66 | 67 |
    68 | 69 | - [#115] 배열을 spread하는 작업은 매우 비싼 작업이다. - [바로가기](https://github.com/woowacourse/javascript-youtube-classroom/pull/115#discussion_r830436124) 70 | ```jsx 71 | //해당 코드 72 | [ 73 | ...ControlVideo.getStorageWillWatchVideoList(), 74 | ...ControlVideo.getStorageWatchedVideoList(), 75 | ].length > MAX_SAVE_VIDEO_COUNT; 76 | ``` 77 | - 배열을 spread하는 것은 배열의 각 요소를 한 번씩 순회하는 비싼 작업입니다. 78 | 여기서는 두 배열의 length를 합하는 것으로 충분해 보이네요. 79 | 80 |
    81 | 82 | #### 2.2 static 83 | 84 | - [#115] static 멤버만 존재하는 클래스가 클래스로서의 가치가 있을까요? - [바로가기](https://github.com/woowacourse/javascript-youtube-classroom/pull/115#discussion_r830430735) 85 | - 해당 클래스를 객체로 전환하는 것으로 충분하다. 86 | 87 | --- 88 | 89 | ### 3. 클린코드 90 | 91 | #### 3-1. 네이밍 92 | 93 | - [#116] show vs move 접두사 의미 - [바로가기](https://github.com/woowacourse/javascript-youtube-classroom/pull/116#discussion_r830470580) 94 | - css를 통해서 view를 제어하고 있기 때문에 show란 이름이 더 적절하다. 95 | - move란 이름은 해당페이지로 history.push되거나 link로 이동하는 느낌이다. 96 | 97 |
    98 | 99 | --- 100 | 101 | ### 4. HTML 102 | 103 | - [#127] input type="search" 속성 - [바로가기](https://github.com/woowacourse/javascript-youtube-classroom/pull/127#discussion_r830486205) 104 | 105 | - [[MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/search)] search 속성은 검색어를 입력할 수 있는 텍스트 필드를 정의한다. 검색 필드는 텍스트 필드와 기능적으로 완전히 똑같지만, 브라우저에 의해 다르게 표현될 수 있다. 106 | - search 필드에서는 반드시 name 속성을 설정해야 하며, name 속성이 설정되어 있지 않으면 서버로 제출되지 않는다. 가장 자주 사용되는 대표적인 name 속성값은 'q'이다. 107 | 108 | ```html 109 |
    110 | 검색 111 | 112 |
    113 | ``` 114 | 115 |
    116 | 117 | --- 118 | 119 | ### 5. 테스트 120 | 121 | - [#118] cypress config로 baseUrl를 관리할 수 있다. -[바로가기](https://github.com/woowacourse/javascript-youtube-classroom/pull/118#discussion_r830497307) 122 | ```jsx 123 | //cypress.json 124 | { 125 | "supportFile": false, 126 | "baseUrl": "https://nan-noo.github.io/javascript-youtube-classroom" 127 | } 128 | ``` 129 | - cypress config 관련 링크 - [여기](https://docs.cypress.io/guides/references/configuration#cypress-json) 130 | 131 | --- 132 | 133 | ### 6. 기타 134 | 135 | #### 6-1. 나는 생각하지 못한 생각 136 | 137 | - [#118] 저장한 비디오를 localStorage에 담고 있는데, 이 경우 사용자에게 오래된 데이터를 보여줄 수 있다. 크루가 생각한 방식은, timestamp를 같이 저장해서 데이터를 가져올 때 일정 기간이 지난 데이터를 삭제하는 방식을 생각했다. - [바로가기](https://github.com/woowacourse/javascript-youtube-classroom/pull/118#issue-1173685271) 138 | 139 | - 좋은 생각이다. 다른 프로그램과 서비스에서는 어떻게 캐시데이터를 관리하는지 찾아봐라. 140 | - 관련 키워드 - Cache Invalidation 141 | 142 |
    143 | 144 | - [#127] 아이템을 나열하는 리스트 UI에서 자주 사용하는 방식 - [바로가기](https://github.com/woowacourse/javascript-youtube-classroom/pull/127#discussion_r830480679) 145 | - 원래는 내부 스크롤 없이 리스트가 페이지에 쭉 나열되고, **스크롤로 내리면 메뉴 바가 상단에 붙는 UI를 많이 쓴다.** 146 | - 관련 키워드 : sticky, fixed 147 | - 스티키라는 것은 css 속성값이라고도 하지만, UI 적으로도 스티키 헤더라고 부른다. 148 | 149 | 150 |
    151 | 152 | #### 6-2. 좋은 말 153 | 154 | - [#118] 주니어 때 리팩토링 관련 도서 한 권은 필수로 읽자. - [바로가기](https://github.com/woowacourse/javascript-youtube-classroom/pull/118#pullrequestreview-915012855) 155 | 156 |
    157 | 158 | - [#136] 단순 구조 vs 확장성 구조 - [바로가기](https://github.com/woowacourse/javascript-youtube-classroom/pull/136#pullrequestreview-915020902) 159 | - 프로토타입이거나 프로덕션이라도 **변경 요청이 별로 없는 부분이라면 확장성을 포기하고 최대한 단순한 구조를 유지**할 것입니다. 160 | - 프로덕션 레벨이고 **변경이 잦은 부분이라면 확장성을 고려해서 복잡해지더라도 유연한 설계를 취할 것**입니다. 161 | 162 |
    163 | 164 | ### 7. 내가 정리한 키워드 165 | 166 | #### 7-1. Cache Invalidation 167 | 168 | 캐시 무효화(Invalidation)는 요청에 대해 캐시 데이터가 사용되어야 할 지 아닐지를 결정하고 항상 최신의 데이터를 반환하도록 하는 일련의 과정입니다. 169 | 170 | 일반적인 캐시 무효화 방법은 다음과 같습니다. 171 | 172 | - **Expiration Time** 173 | 캐시가 생성될 때 지정한 TTL에 의해 캐시의 수명이 결정되고 수명이 다한 캐시는 사용되지 않습니다. 가장 일반적인 캐시 무효화 전략입니다. 174 | TTL을 보수적으로 정했을 때, 해당 시간이 지나면 최신의 데이터가 반환되기 때문에 안전한 방법입니다. 캐시를 잘 관리할 자신이 없다면 TTL을 짧게 정하는 것이 좋습니다. 175 | 176 |
    177 | 178 | - **Freshness Caching Verification** 179 | 별도의 검증 절차를 통해 캐시가 유효한지 확인하는 방법입니다. 180 | 예를 들어 원본 데이터가 업데이트 된 시간과 캐시가 생성된 시간을 비교하여 캐시의 유효성을 판별합니다. 단점은 캐시가 사용될 때마다 추가적인 확인 절차를 수행하기 때문에 이에 따른 오버헤드가 발생합니다. 181 | 182 |
    183 | 184 | - **Active Application Invalidation** 185 | 데이터가 수정되는 코드가 수행될 때마다 백엔드에서 연관된 캐시를 무력화하는 방법입니다. 캐시를 세밀하게 관리할 수 있기 때문에 이상적으로 설계되었을 때 가장 좋은 효율을 발휘합니다. 1번 방식이 수동적인 방법인데 비해 아주 능동적인 방법입니다. 별도의 코드를 통해 캐시를 컨트롤 하기 때문에 가장 에러가 발생하기 쉽고 관리가 까다롭습니다. 186 | 187 |
    188 | 189 | - 더 자세하게 알고 싶다면 [여기](https://toss.tech/article/smart-web-service-cache)를 참고하세요. - toss tech 190 | -------------------------------------------------------------------------------- /youtube/step2/marco.md: -------------------------------------------------------------------------------- 1 | # Level1 Youtube Step2(담당 PR 번호들) - 마르코 2 | 3 | - 분석 담당 코드 4 | - 마르코 [#147](https://github.com/woowacourse/javascript-youtube-classroom/pull/147) 5 | - 위니 [#150](https://github.com/woowacourse/javascript-youtube-classroom/pull/150) 6 | - 병민 [#146](https://github.com/woowacourse/javascript-youtube-classroom/pull/146) 7 | - 해리 [#142](https://github.com/woowacourse/javascript-youtube-classroom/pull/142) 8 | - 코카콜라 [#134](https://github.com/woowacourse/javascript-youtube-classroom/pull/134) 9 | - 후이 [#148](https://github.com/woowacourse/javascript-youtube-classroom/pull/148) 10 | - 샐리 [#131](https://github.com/woowacourse/javascript-youtube-classroom/pull/131) 11 | 12 | # 2. 피드백 정리 13 | 14 | ## 1. 함수 15 | 16 | - [#150] for문을 some 메서드를 활용하여 리팩토링 - [바로가기](https://github.com/woowacourse/javascript-youtube-classroom/pull/150#discussion_r830994669) 17 | - [MDN - some](https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Array/some) 18 | ```js 19 | // [before] for문 20 | for (let idx = 0; idx < this.#savedVideoItems.length; idx += 1) { 21 | if (this.#savedVideoItems[idx].videoId === deleteVideoId) { 22 | this.#savedVideoItems.splice(idx, 1); 23 | return; 24 | } 25 | } 26 | ``` 27 | 28 | ```js 29 | // [after] some 메서드 30 | this.#savedVideoItems.some((item, idx) => { 31 | if (item.videoId === deleteVideoId) { 32 | this.#savedVideoItems.splice(idx, 1); 33 | } 34 | return item.videoId === savedId; 35 | }); 36 | ``` 37 | 38 | ## 2. 클래스와 메서드 39 | 40 | - [#148] 클래스에 연관된 헬퍼함수, 템플릿, 상수 파일들의 분리 방법 - [바로가기](https://github.com/woowacourse/javascript-youtube-classroom/pull/148/files/1ba87826bed272037aac5446acbadcd4565ad3a3#r830620883) 41 | - 예들 들어, SearchModal 폴더를 만들고 하위에 SearchModal.js, SearchModal.helper.js, SearchModal.template.js, SearchModal.constants.js 이렇게 나누는것도 한 방법이다. 42 | 43 | ## 3. DOM 44 | 45 | - [#146] attribute는 '고유속성'에 가까운 정적인 개념에 가깝다. 동적으로 변하는 '상태'에는 적합하지 않다. 46 | - https://ko.javascript.info/dom-attributes-and-properties#ref-1789 47 | - [#178] 데이터 바인딩 - [바로가기](https://github.com/woowacourse/javascript-youtube-classroom/pull/148#discussion_r833264831) 48 | - [준찌 추천 링크](https://facebook.github.io/flux/docs/in-depth-overview/) 49 | - 데이터 바인딩은 데이터와 뷰를 묶는다는 뜻이다. 웹어플리케이셥의 복잡도가 증가하면 뷰와 데이터를 일치시키기 어려워진다. 그래서 데이터와 뷰가 자동으로 일치하도록 묶어 두는 것이 데이터 바인딩이다. 50 | - 데이터 바인딩은 다시 `단방향`과 `양방향`으로 나뉜다. 51 | - 단반향 데이터 바인딩은 데이터가 변경되면 템플릿과 데이터를 합쳐 뷰를 만든다. 52 | - 대표적으로 React가 단방향 데이터 바인딩을 한다. 단방향 데이터 바인딩은 데이터와 템플릿을 결합해 화면을 생성하는 것이다. (JS -> HTML만 가능) 사용자의 입력에 따라 데이터를 갱신하고 화면을 업데이트 해야 하므로 단방향 데이터 바인딩으로 구성하면, 데이터의 변화를 감지하고 화면을 업데이트 하는 코드를 매번 작성해주어야 한다. 리액트는 자바스크립트 기반으로, 부모 뷰->자식 뷰 바뀐 내용을 직접 전달한다. 53 | - 장점 : 데이터 변화에 따른 성능 저하 없이 DOM 객체 갱신 가능, 데이터 흐름이 단방향(부모->하위 컴포넌트)이라, 코드를 이해하기 쉽고 데이터 추적과 디버깅이 쉬움 54 | - 단점: 변화를 감지하고 화면을 업데이트 하는 코드를 매번 작성해야 함 55 | - 양방향 데이터바인딩은 뷰의 변경을 감지하여 데이터에 반영해주는 동시에, 데이터의 변경이 일어나면 템플릿과 데이터를 합쳐 뷰에 반영해준다. 56 | - 양방향 데이터 바인딩 57 | - 두 데이터 혹은 정보의 소스를 일치시키는 기법으로, 화면에 보이는 데이터와 브라우저 메모리에 있는 데이터(여러개의 자바스크립트 객체)를 일치시키는 것을 말한다. 예를 들어, MVC 모델에서 model과 view를 서로 묶어 model과 view의 "자동 동기화" 시키기 라고 이해할 수 있다. 58 | - 컨트롤러에서 model이 변경됨 -> view변경됨, view에서 scope model이 변경됨 -> 컨트롤러에서 model이 변경됨 59 | - 장점 : 코드의 사용면에서 코드량을 크게 줄여줌 60 | - 단점 : 변화에 따라 DOM 객체 전체를 렌더링해주거나 데이터를 바꿔주므로, 성능이 감소되는 경우가 있음 61 | - [편집하면서 참고한 블로그](https://blog.hyunmin.dev/15) 62 | 63 | ## 4. 이벤트 64 | 65 | ## 5. 클린코드 66 | 67 | ## 6. CSS, HTML 68 | 69 | - [#147] css 네 방향 속성 관련 70 | - 예를 들어, `margin: 0 0 30px 0;` 과 `margin: 0 0 30px;`은 같다. 71 | - "위 오른쪽 아래 왼쪽" 에서 오른쪽과 왼쪽이 같으면 "위 오른쪽=왼쪽 아래 "처럼 마지막 왼쪽을 생략해도 된다. 72 | - 그리고 "위와 아래끼리", "오른쪽과 왼쪽끼리" 같은 경우 "위=아래 오른쪽=왼쪽" 처럼 두 개로 축약해도 된다. 73 | - [#147] `CSS 의사 클래스`와 `CSS 의사요소`를 구분하자. 74 | - 브라우저에서 콜론 하나(:)는 `CSS 의사 클래스`에서 사용하고, 콜론 두 개(::)는 `CSS 의사 요소`에서 사용한다. 75 | - CSS3에 들어서면서, `CSS 의사 요소`와 `CSS 의사 클래스`를 구분하기 위해, 콜론 두 개인 `::before`과 같은 구문을 도입했다. 76 | - CSS3에서는 `::before`이고 CSS2에서는 원래 `:before`로 사용되었다. 77 | - 아직 브라우저에서는 CSS2 구문인 `:before`를 아직 허용한다고 하지만, before는 `의사 요소`이므로 `::before` 구문으로 사용하는 것이 권장된다. 78 | - 즉, CSS3 도입 취지에 부합하도록, `의사요소(:active, :hover 등)`와 `의사클래스(::before, ::after 등)`를 구분하여 콜론 개수를 달리 표시해야 한다. 79 | - [MDN- CSS 의사 클래스](https://developer.mozilla.org/ko/docs/Web/CSS/Pseudo-classes) 80 | - [MDN- CSS 의사 요소](https://developer.mozilla.org/ko/docs/Web/CSS/Pseudo-elements) 81 | 82 | ## 7. 테스트 83 | 84 | - [#147] 웹스토리지나 API 자체를 Mocking하여 테스트하는 것에 의미가 없지 않다. 85 | - [질문] 제가 로컬스토리지를 mocking해서 테스트하려는 것은 "저장한 영상을 로컬스토리지에서 불러올 수 있어야 한다."인데, 이처럼 유닛테스트에서 웹스토리지나 API 자체를 Mocking해서 테스트하는 것에 어떤 의미가 있는 것인지 고민이 떠올랐어요. 결국 테스트를 위한 테스트가 될 것 같다는 생각이 들기도 해서요. 해당 테스트가 유닛테스트에서 불필요할 것 같아, 지울까 하는 고민이 드는데, 어떻게 생각하시나요? 86 | - [답변] 의미 없지 않습니다! 실제 메소드가 동작하는지 로컬스토리지에 데이터가 있는 상황, 없는 상황을 가정해서 테스트를 할 수 있거든요! 추후엔 오히려 더 테스트 케이스를 늘려서 보강하셔야합니다! 87 | 88 | ## 8. 설계 89 | 90 | - [#147] 객체지향에서는 setter는 사용하지 않는 것이 권장된다. 91 | 92 | - 객체지향에서 말하길, setter와 getter에서 setter는 사용하지 않는걸 권장하고 있다. 그 이유는 객체지향은 객체간 협력을 나타내는 것인데, setter는 객체간 협력보다는 "의존" 혹은 "명령"이기 때문이다. 93 | - 예를 들어, A와 B가 모래성을 쌓고 있는데, 이들은 멋진 모래성을 쌓는게 목표이다. 그렇게 열심히 둘이 모래성을 쌓고 있는데, B가 실수로 A에게 "파괴하라!" 라고 `명령(set)`을 하게 된다면, A는 모래성을 파괴하게 된다. 94 | - 만약 setter를 안쓰고 `행동 단위`로 했다면 어땠을까? A와 B는 C라는 목표를 달성하기 위해 행동하는 객체이게 된다. C라는 목표를 모래성으로 설정하고. A와 B는 메소드로 PileUp() 이라는 메소드로 쌓을 수 있다. 즉, PileUp(C)를 통해 C를 서로가 쌓을 수 있게 된다. 여기서 C가 B로 바뀌던 A로 바뀌던, 결국엔 쌓기만 한다. `쌓는 행위` 외에 다른 것들은 할 수 없으니 비교적 명령보다 `안전`하다. 95 | - 따라서, setter와 같은 명령 대신에 행동 단위로 하는 것이 좋다. 96 | 97 | - [#142] 객체지향적 관점에서 `has-a`와 `is-a` 차이점 - [바로가기](https://github.com/woowacourse/javascript-youtube-classroom/pull/142#discussion_r830583944) 98 | 99 | - is-a 100 | - is-a는 추상화(형식이나 클래스와 같은)들 사이의 포함 관계를 의미하며, 한 클래스 A가 다른 클래스 B의 서브클래스(파생클래스)임을 이야기합니다. 다른 말로, 타입 A는 타입 B의 명세(specification)를 암시한다는 점에서 타입 B의 서브타입이라고도 할 수 있습니다. is-a 관계는 타입 또는 클래스 간의 has-a 관계와는 대조됩니다. has-a 및 is-a 관계들 간의 혼동은 실세계 관계 모델에 대한 설계에 있어 자주 발견되는 에러입니다. is-a 관계는 또한 객체 또는 타입 간의 instance-of 관계와도 대조됩니다. 101 | - 예) 학생은 사람이다(o) 102 | - 코드를 공통적으로 관리하기 때문에 코드의 추가 및 변경이 매우 용이하다. 103 | - 조상클래스의 변경이 있으면 자손클래스는 영향을 주지만, 자손클래스가 변경되는 것은 조상클래스에 아무런 영향을 주지 않는다 104 | - 자손클래스의 인스턴스를 생성하면 조상클래스의 멤버와 자손 클래스의 멤버가 합쳐진 하나의 인스턴스로 생성된다 105 | ![image](https://user-images.githubusercontent.com/59413128/160842017-4e59add0-2e4f-4d03-8d1e-c4a567e8bf03.png) 106 | - has-a 107 | - has-a는 구성 관계를 의미하며 한 오브젝트(구성된 객체, 또는 부분/멤버 객체라고도 부릅니다)가 다른 오브젝트(composite type이라고 부릅니다)에 "속한다(belongs to)"를 말합니다. 단순히 말해, has-a 관계는 객체의 멤버 필드라고 불리는 객체를 말하며, Multiple has-a 관계는 소유 계층구조를 형성하기 위해 결합하는 경우를 말합니다. 108 | - 예) 학생은 책을 가지고있다(o) 109 | - 한 클래스의 멤버변수로 다른 클래스를 선언하는 것 110 | - 작은 단위의 클래스를 먼저 만들고, 이 들을 조합해서 하나의 커다란 클래스를 만든다. 111 | ![image](https://user-images.githubusercontent.com/59413128/160842074-1885d659-f21b-441a-abc5-ed2e20c0dcb2.png) 112 | 113 | - [#131] 아키텍처 맵(구조도) - [바로가기](https://github.com/woowacourse/javascript-youtube-classroom/pull/131#issuecomment-1073797128) 114 | - 아키텍처 맵은 다음과 같이 두 가지로 방법으로 그려볼 수 있다. 115 | - 폴더 스트럭쳐: 폴더 스트럭쳐단위로, 서로의 폴더가 어떤걸 참조하고 있는지 그리는 방법 116 | - SoC: 관심사 단위로 어떤 것들이 어떻게 분리되고 어떻게 참조하고 있는지 그리는 방법 117 | -------------------------------------------------------------------------------- /youtube/step2/movie.md: -------------------------------------------------------------------------------- 1 | # LEVEL 1 나만의 유튜브 강의실 STEP 2 - 무비😎 2 | 3 | ## 분석 담당 코드 4 | 5 | - 무비 [PR 123](https://github.com/woowacourse/javascript-youtube-classroom/pull/123) X 티거 [PR 129](https://github.com/woowacourse/javascript-youtube-classroom/pull/129) 6 | - 우디 [PR 151](https://github.com/woowacourse/javascript-youtube-classroom/pull/151) X 시지프 [PR 143](https://github.com/woowacourse/javascript-youtube-classroom/pull/143) 7 | - 하리 [PR 119](https://github.com/woowacourse/javascript-youtube-classroom/pull/119) X 민초 [PR 130](https://github.com/woowacourse/javascript-youtube-classroom/pull/130) 8 | - 비녀 [PR 139](https://github.com/woowacourse/javascript-youtube-classroom/pull/139) X 코이 [PR 137](https://github.com/woowacourse/javascript-youtube-classroom/pull/137) 9 | 10 |
    11 |
    12 | 13 | ## 피드백 정리 14 | 15 | 😛 링크를 누르면 해당 discussion으로 이동! 😛 16 | 17 |
    18 | 19 | ### 구조 20 | 21 | - [#129] Custom Element를 활용했을 때 재사용성에 대한 생각 - [바로가기](https://github.com/woowacourse/javascript-youtube-classroom/pull/129#pullrequestreview-914990036) 22 | - **Q** : Custom Element를 사용하는 데 저장된 비디오 리스트, 검색된 비디오 리스트가 비슷한 element라 재사용을 하고 싶습니다. 그런데 둘의 로직이 다른 부분이 존재하기 때문에 합치는 것이 복잡할 것 같아요. 이럴때는 어떻게 하는 것이 좋을까요? 23 | - **A** : 분리하는게 맞다고 생각합니다. 24 | 1. Element들이 세부적으로 이벤트 바인딩도 제각기 다른 상태 25 | 2. (합치게 되었을 때) 한 파일에 너무 역할이 크다면 말씀하신대로 복잡도가 올라가 유지보수에 더 힘들다. 26 | 3. 중복이 있는 코드는 상속을 통해 해결해보자. 27 | 4. 항상 재사용하는게 정답이라고 생각하지 않는다. 재사용빈도가 낮고 재사용했을 때 **복잡도를 증가시킬 수 있다고 생각하면 있는 그대로 사용해도 좋다.** 28 | - [#143] 자기가 짠 코드가 자주 헷갈린다면, 단순한 다른 구조를 생각해보자. - [바로가기](https://github.com/woowacourse/javascript-youtube-classroom/pull/143#discussion_r832735972) 29 | - [#137] 상속은 강한 결합도를 갖게 하므로 여러가지를 고려해서 결정해주자. (상속은 확실한 `is - a` 관계에서 사용해보자) - [바로가기](https://github.com/woowacourse/javascript-youtube-classroom/pull/137#discussion_r830519847) 30 | ```js 31 | export default class VideoManager extends Observer {} 32 | // 과연 "VideoManager is an Observer" 일까? 33 | ``` 34 | 35 |
    36 | 37 | ### 테스트 38 | 39 | - [#123] (cypress) 특정 아이템이 삭제되었는지 확인하는 테스트에서는 그 아이템이 존재하는 지에 대한 테스트를 진행하자. - [바로가기](https://github.com/woowacourse/javascript-youtube-classroom/pull/123#discussion_r830628283) 40 | - [#129] (cypress) API 요청을 테스트할 때 `cy.wait()`을 써서 API 응답을 기다릴 수는 있다. 그런데 구체적인 시간을 지정해주는 것이기 때문에, 그 전에 API 응답이 도착했으면 비효율적일 수 있다. - [바로가기](https://github.com/woowacourse/javascript-youtube-classroom/pull/129#discussion_r830480992) 41 | - cypress의 intercept를 통해 API 응답을 invoke해서 해당 API Fetch를 wait해줄 수 있다. 42 | - 그런데 해당 크루분이 `cy.wait()`를 지우고 `cy.get()`만 사용해봤더니, 정상적으로 테스트가 성공했다. 이유는 `cy.get()`은 기본적으로 기다리는 시간이 [4초](https://docs.cypress.io/guides/references/configuration#Timeouts)이기 때문이다! (`option: defaultCommandTimeout, default: 4000`) - **지정된 시간 내에 elmeent가 생성되면 이후에 체이닝된 should를 검증** 43 | - 그러므로 cypress가 추천하는 방법인 `cy.intercept()`를 사용해보자! 44 | ```js 45 | cy.intercept('GET', '/users', [{ name: 'Maggy' }, { name: 'Joan' }]); 46 | cy.get('#fetch').click(); 47 | cy.wait(4000); // <--- this is unnecessary 48 | cy.get('table tr').should('have.length', 2); 49 | // 테이블의 길이가 2가 될 때까지 기다린다. 50 | ``` 51 | - 만약 `cy.wait()`를 쓰고 싶다면 `aliased route`를 이용해서 특정한 작업을 기다리도록 해주자. 52 | ```js 53 | cy.intercept('GET', '/users', [{ name: 'Maggy' }, { name: 'Joan' }]).as( 54 | 'getUsers' 55 | ); 56 | cy.get('#fetch').click(); 57 | cy.wait('@getUsers'); // <--- wait explicitly for this route to finish 58 | cy.get('table tr').should('have.length', 2); 59 | ``` 60 | - [#129] (cypress) 중복이 있는 테스트 코드를 분리해주고 싶다면 [command](https://docs.cypress.io/api/cypress-api/custom-commands#Syntax)를 사용해보자. - [바로가기](https://github.com/woowacourse/javascript-youtube-classroom/pull/129#discussion_r830481260) 61 | 62 | ```js 63 | // command 64 | Cypress.Commands.add('hasCountVideoItems', ($container, count) => { 65 | cy.get($container).find('.video-item').should('have.length', count); 66 | // 영상 목록에 video-item이 몇개 있을지 67 | }); 68 | 69 | // test case 70 | it('저장한 영상을 볼 영상 목록에서 확인할 수 있어야 한다.', () => { 71 | // ..add 72 | 73 | cy.hasCountVideoItems('.playlist-videos-container', 1); // command 이용 74 | }); 75 | 76 | it('영상을 삭제할 수 있어야 한다.', () => { 77 | // ..delete 78 | 79 | cy.hasCountVideoItems('.watched-videos-container', 0); 80 | }); 81 | ``` 82 | 83 | - [#151] (cypress) 테스트에서 반복적으로 같은 버튼을 클릭한다면? [`as`](https://docs.cypress.io/guides/core-concepts/variables-and-aliases)를 사용해보자. - [바로가기](https://github.com/woowacourse/javascript-youtube-classroom/pull/151#discussion_r830634780) 84 | 85 | ```js 86 | beforeEach(() => { 87 | cy.get('saved-list') 88 | .find('.video-item__remove-button') 89 | .first() 90 | .as('first-video-button'); // as 91 | }); 92 | 93 | it('삭제 버튼을 클릭하면 사용자에게 삭제할 것인지 물어야 한다.', () => { 94 | cy.get('@first-video-button') // as 95 | ... 96 | }); 97 | ``` 98 | 99 | - [#139, #151] (jest) package.json에 [`testPathIgnorePatterns`](https://runebook.dev/ko/docs/jest/configuration#testpathignorepatterns-arraystring)를 활용하면, 특정 경로를 테스트에서 제외시킬 수 있다. - [바로가기](https://github.com/woowacourse/javascript-youtube-classroom/pull/139#discussion_r830580284) 100 | - jest에서 cypress테스트가 진행되지 않게 해주려면 해주자. 101 | ```json 102 | "jest": { 103 | "testPathIgnorePatterns": [ 104 | "cypress" 105 | ] 106 | }, 107 | ``` 108 | - [#137] (cypress) 반복적으로 `localhost:----`에 cy.visit()해줘야 한다면, cypress config에서 [`baseUrl`설정](https://docs.cypress.io/guides/references/configuration#Options)을 해주자. - [바로가기](https://github.com/woowacourse/javascript-youtube-classroom/pull/137#discussion_r830517643) 109 | 110 |
    111 | 112 | ### 데이터 113 | 114 | - [#123] 같은 아이템 (id가 같은 아이템)을 저장하는 경우에는 오류를 발생시킬 수 있으므로 중복 id를 저장하는 일을 피하자. - [바로가기](https://github.com/woowacourse/javascript-youtube-classroom/pull/123#discussion_r830629853) 115 | 116 |
    117 | 118 | ### HTML 119 | 120 | - [#143] [`br`](https://developer.mozilla.org/ko/docs/Web/HTML/Element/br)태그 같은 경우 공식 문서에서도, `
    `보다는 `
    `을 사용하고 있다.! 이번 기회에 `/`를 제거해보자. - [바로가기](https://github.com/woowacourse/javascript-youtube-classroom/pull/143#discussion_r831049297) 121 | 122 |
    123 | 124 | ### CSS 125 | 126 | - [#129] 색상 팔레트를 나눠줄 때 100단위로 나눠주는 게 좋다. - [바로가기](https://github.com/woowacourse/javascript-youtube-classroom/pull/129#discussion_r830478991) 127 | - [#151] `grid`를 사용해줬을 때 스크린 사이즈에 따라 보여지는 cell의 개수를 조정하고 싶다면, [`grid-template-columns`](https://studiomeal.com/archives/533)를 사용해보자.! - [바로가기](https://github.com/woowacourse/javascript-youtube-classroom/pull/151#discussion_r830633346) 128 | ```css 129 | @media only screen and (min-width: 600px) { 130 | .video-list { 131 | grid-template-columns: repeat(2, minmax(0, 1fr)); 132 | } 133 | /* repeat(반복횟수, 반복값) */ 134 | /* minmax(최소 길이, 최대 길이) */ 135 | } 136 | @media only screen and (min-width: 960px) { 137 | .video-list { 138 | grid-template-columns: repeat(3, minmax(0, 1fr)); 139 | } 140 | } 141 | @media only screen and (min-width: 1280px) { 142 | .video-list { 143 | grid-template-columns: repeat(4, minmax(0, 1fr)); 144 | } 145 | } 146 | ``` 147 | 148 |
    149 | 150 | ### 함수 151 | 152 | - [#151] deep clone해주는 작업을 특정 메서드에 그대로 넣어준다면, 해당 메서드의 역할을 파악하는 것에 어려움이 있을 수 있다. deep clone해주는 작업은 따로 메서드 분리해주자. - [바로가기](https://github.com/woowacourse/javascript-youtube-classroom/pull/151#discussion_r830630646) 153 | - [#143] return에서 또 return으로 이어지는 구조는 가독성이 떨어진다. 변수에 한번 저장해주고 return해주는 구조를 생각해보자. - [바로가기](https://github.com/woowacourse/javascript-youtube-classroom/pull/143#discussion_r831059726) 154 | ```diff 155 | - return rawVideos.map(({ id, snippet }) => { 156 | - return { 157 | - videoId: id.videoId, 158 | - thumbnailUrl: snippet.thumbnails.default.url, 159 | - title: snippet.title, 160 | - channelTitle: snippet.channelTitle, 161 | - publishTime: snippet.publishTime, 162 | - }; 163 | - }); 164 | + const videos = rawVideos.map(...) 165 | + return videos 166 | ``` 167 | 168 |
    169 | 170 | ### 객체 171 | 172 | - [#151] `JSON.stringify()`를 사용하여 두 객체가 같은 지 비교할 때, 키의 순서가 바뀌면 제대로 된 [검증을 할 수 없다](https://betterprogramming.pub/why-you-shouldnt-use-json-stringify-to-compare-objects-in-javascript-c9a16b7331e).! - [바로가기](https://github.com/woowacourse/javascript-youtube-classroom/pull/151#discussion_r830633031) 173 | 174 | ```js 175 | JSON.stringify({a: 'a', b: 'b'}) === JSON.stringify({{a: 'a', b: 'b'}} //true 176 | 177 | JSON.stringify({b: 'b', a: 'a'}) === JSON.stringify({{a: 'a', b: 'b'}} //false 178 | ``` 179 | 180 |
    181 | 182 | ### 유용한 구문 183 | 184 | - [#151] 오류처리에 집중하느라 이후에 작업을 놓칠 수 있다. `try-catch`를 사용했을 때 `finally`를 한번씩 생각해주자. - [바로가기](https://github.com/woowacourse/javascript-youtube-classroom/pull/151#discussion_r830629741) 185 | - [#151] log를 보기 쉽게 들여쓰기 하고 싶다면, [`console.group()`](https://developer.mozilla.org/ko/docs/Web/API/Console/group)을 사용해보자. - [바로가기](https://github.com/woowacourse/javascript-youtube-classroom/pull/151#discussion_r830642019) 186 | ![mdn](https://developer.mozilla.org/en-US/docs/Web/API/console/group/nesting.png) 187 | 188 | ```js 189 | console.log('This is the outer level'); 190 | 191 | console.group(); // lv2 192 | console.log('Level 2'); 193 | 194 | console.group(); // lv3 195 | console.log('Level 3'); 196 | console.warn('More of level 3'); 197 | console.groupEnd(); // lv3 198 | 199 | console.log('Back to level 2'); 200 | console.groupEnd(); // lv2 201 | 202 | console.log('Back to the outer level'); 203 | ``` 204 | 205 | - [#151] [log의 스타일도 커스텀](https://www.samanthaming.com/tidbits/40-colorful-console-message/)이 가능하다. - [바로가기](https://github.com/woowacourse/javascript-youtube-classroom/pull/151#discussion_r830642019) 206 | 207 | ```js 208 | console.log('%cHello', 'color: green; background: yellow; font-size: 30px'); // [1] 209 | 210 | console.log('Nothing here %cHi Cat %cHey Bear', 'color: blue', 'color: red'); // [2] 211 | ``` 212 | 213 | 스크린샷 2022-03-29 오후 9 21 47 214 | 215 | - [#130] 이벤트 위임을 해줄 때 early return을 활용해보자! - [바로가기](https://github.com/woowacourse/javascript-youtube-classroom/pull/130#discussion_r831294765) 216 | 217 | ```js 218 | // before 219 | if (e.target.classList.contains('video-item__check-button')) { 220 | // do something 221 | } 222 | 223 | // after 224 | if (!e.target.classList.contains('video-item__check-button')) { 225 | return; 226 | } 227 | // do something 228 | ``` 229 | 230 |
    231 | 232 | ### 네이밍 233 | 234 | - [#143] 메서드 이름을 보고 어떤 return값이 나올지 추측해보는 것이 네이밍 작성하는데 좋은 접근방법 - [바로가기](https://github.com/woowacourse/javascript-youtube-classroom/pull/143#discussion_r832737482) 235 | --------------------------------------------------------------------------------