March 8, 2021 • ☕️☕️☕️☕️ 19 min read
우테코 Lv1 Lotto PR로그
ES6이후로는 함수 Object Constructor 문법은 잘 사용하지 않는다 → function 대신 class로 작성하기
Controller가 Model과 View를 멤버로 가지는 동시에, 역할도 알맞게 위임하기
직접 css style을 건드리는 대신, class 이용하기
DOM이 load됐을 때 App 실행하기
const App = () => {
init();
};
window.addEventListener("DOMContentLoaded", App);
습관적인 (필요 없는) 리턴 주의하기
// Bad
export const hideElement = (element) => {
return element.classList.add("d-none");
};
App에서 controller, view를 연결하는 새로운 방법
export default class App {
constructor() {
this.$target = $("#app");
this.setup();
}
execute() {
this.mountComponent();
}
setup() {
this.gameManager = new GameManager([]);
}
mountComponent() {
this.gameInput = new GameInput({
gameManager: this.gameManager,
});
this.gameDisplay = new gameDisplay({ gameManager: this.gameManager });
}
}
class에서 dom과 event 메소드들 묶기
class GameClass {
constructor(props) {
this.props = props;
this.selectDOM();
this.bindEvent();
}
}
🤔 id
vs data-*
?
id
, 비슷한 성격의 것들이라면 data-*
html에 간략한 섹션 설명 달기
<!-- purchase amount input form -->
<form class="mt-5" id="purchase-amount-form" novalidate></form>
novalidate
form
태그의 novalidate
속성은 폼 데이터(form data)를 서버로 제출할 때 해당 데이터의 유효성을 검사하지 않음을 명시한다.쉴드패턴: 값의 유효성 검사 후 값이 유효하지 않다면 함수의 진행을 중단시켜서(즉시 return) 유효하지 않은 데이터로 로직이 도는것을 막는 코딩 패턴
throw
로 에러를 낸 상태를 가리킨다. 이 때 이 에러를 잡아서(catch) 시스템이 죽지 않도록 처리 하는 것이 익셉션 핸들러의 역할이다.)class private property 사용
export class Lotto {
#numbers;
constructor(numbers) {
this.#numbers = numbers;
}
get numbers() {
return [...this.#numbers];
}
}
#
은 물론이고 실무에서 ES6 이상의 스팩을 사용하려면 Babel을 써야 한다. 이는 #
도 마찬가지.private vs protected
_
컨벤션)는 JS 에서는 기능적인 지원은 없지만 내부 클래스 + 유기적으로 상속된 클래스 간에서는 접근이 가능하게끔 약속된 컨벤션의 성격이 강하다.private 크로스 브라우징 이슈
의존성 주입
extra-mile UX - 거스름돈 반환하기
boolean flag
// Boolean 타입이 아닌 truthy, falsy 조건식
if (change) {
}
// Boolean 타입인 조건식
if (!!change) {
}
if (change > 0) {
}
if (change !== 0) {
}
if (Boolean(change)) {
}
대부분의 자바스크립트 스타일가이드와 lint에서 camelCase를 사용을 권장한다.
$(document.querySelector)
는 DOM tree를 탐색하는 비용이 든다. 한번만 쓰고 말 DOM reference라면 이렇게 inline으로 작성해도 상관없지만, 계속 사용할 DOM reference라면 한번 select 후 저장해두는 것이 좋다.
react component형 App
class App extends Component {
initStates() {
this.lottos = new State([]);
}
mountTemplate() {
this.$target.innerHTML = `
...
`;
}
mountChildComponents() {
new PurchaseInput($("#purchase-input-wrapper"), { lottos: this.lottos });
new LottList($("#lotto-view-wrapper"), { lottos: this.lottos });
}
}
Component를 추상 클래스로 구현하기
export default class Component {
$target;
props;
constructor($target, props = {}) {
this.$target = $target;
this.props = props;
this.initStates();
this.render();
this.initEvent();
}
render() {
this.mountTemplate();
this.mountChildComponents();
}
initStates() {}
initEvent() {}
mountTemplate() {}
mountChildComponents() {}
}
래퍼 객체
테스트 코드 분리하기
기능에 대한 테스트
레이아웃 상황에 대한 테스트
array.fill()
에 객체가 들어갈 경우 참조 복사가 발생하여, 값의 변경에 취약하다.
label
과 input
<label for="lotto">
은 <input id="lotto">
과 짝을 이룬다.<input>
태그를 <label>
태그에 자식으로 넣으면 id
, for
를 사용하지 않아도 연결된다.<label>
과 <input>
을 연결하면 <label>
영역을 선택하여 <input>
에 초점을 맞추거나 활성화시킬 수 있음.<label>
안에 <a>
, <button>
같은 인터랙티브 요소를 배치해선 안 된다.addEventListener
의 DOM
addEventListener
의 익명함수 내 this
는 이벤트를 추가한 DOM 요소를 가리킨다.addEventListener
의 화살표함수 내 this
는 상위 스코프의 this
를 가리킨다.classList.replace
메서드의 이름은 내부에 존재하는 로직을 알려주는 방식으로 짓는 것이 아닌 이 메서드를 사용 하는 측(메서드를 호출하는 측)을 위해 오히려 내부 로직을 드러내지 않는식으로 지어야 한다.
input의 value를 destructuring으로 가져오기
const { value } = event.target.elements["purchase"];
new App()
을 생성하자마자 앱이 실행되는 것은 어색하다. 항상 동적할당은 메모리에 대한 객체를 갖고 있는게 안전하다. (언제 쓸 지 모르기 때문)
// 생성과 실행 분리
const app = new App();
app.execute();
visibility
display: none
: 영역 차지 X / 이벤트 동작 Xvisibility: hidden
: 영역 차지 O / 이벤트 동작 Xopacity: 0
: 영역 차지 O / 이벤트 동작 ODocumentFragment
활용하기
const fragment = document.createDocumentFragment();
const childrenFragment = document.createDocumentFragment();
input의 valueAsNumber
css BEM
처음부터 불필요하게 구조나 디자인 패턴을 적용하지 말고, 앱의 규모에 따라서 적절한 구조를 적용해보자
innerHTML, innerText 대신 createElement
model은 데이터만 관리하는게 아닌, 데이터를 정제하고 행동을 만들고, 실제 구현체 용도로 사용할 수 있다.
array의 reduce
함수는 map
과 join
으로 대체하기
두 가지 방식
const mapJoinHTML = array
.map((element) => `${element.number}번 요소`)
.join("");
const reduceHTML = array.reduce(
(prev, cur) => prev + `${element.number}번 요소`,
""
);
처음에 보이면 안 되는 요소는 html에 display: 'none'
으로 심어두기
없는 클래스를 삭제하려고 할 때 에러가 발생하지 않는다.
elementClasses
는 elementNodeReference
의 클래스 속성을 나타내는 DOMTokenList
이다. 만약 클래스 속성이 설정되어 있지 않거나 비어있다면 elementClasses.length
는 0을 반환한다.closest
의 브라우저 지원 이슈
새로운 것을 많이 시도한, 조금 어려운 어느 크루의 코드
자식 컴포넌트에 props
로 변수 뿐 아니라 메서드도 넘겨줄 수 있다.
new WinningNumberForm($("#winning-number-form-wrapper"), {
open: this.open,
winningNumber: this.winningNumber,
tickets: this.tickets,
});
new ResultModal($(".modal"), {
open: this.open,
result: this.result,
reset: this.reset.bind(this),
});
hasOwnProperty
로 컴포넌트의 유효성을 체크한다. (abstract 클래스의 메소드를 모두 구현했는지 여부)
if (!this.isAllMethodsImplemented()) this.throwErrorByCase();
isAllMethodsImplemented() {
const prototype = this.__proto__;
return (
Object.hasOwnProperty.call(prototype, 'initStates') &&
Object.hasOwnProperty.call(prototype, 'subscribeStates') &&
...
);
}
throwErrorByCase() {
const prototype = this.__proto__;
if (!Object.hasOwnProperty.call(prototype, 'initStates'))
throw new Error('initStates is not implemented');
if (!Object.hasOwnProperty.call(prototype, 'subscribeStates'))
throw new Error('subscribeStates is not implemented');
...
}
if절 또는 switch case 대신 object literal을 사용하는 방법
const winnerIndex = {
[SCORE.FIRST]: 0,
[SCORE.SECOND]: ticket.includes(winningNumber.bonus) ? 1 : 2,
[SCORE.THIRD]: 2,
[SCORE.FOURTH]: 3,
};
테스트는 “주어진 상황에서 예측가능한 상태”를 검증할때 적절한 방식이다.
사용성 - esc 키를 눌렀을 때 모달 닫기
if (key === "Escape") {
closeModal();
return;
}
Map
const rankCountMap = new Map([
[VALUE.WINNING_RANK.FIRST, 0],
[VALUE.WINNING_RANK.SECOND, 0],
[VALUE.WINNING_RANK.THIRD, 0],
[VALUE.WINNING_RANK.FOURTH, 0],
[VALUE.WINNING_RANK.FIFTH, 0],
[VALUE.WINNING_RANK.NONE, 0],
]);
lotto.tickets.forEach(({ winningRank }) => {
rankCountMap.set(winningRank, rankCountMap.get(winningRank) + 1);
});
🤔 왜 Object
놔두고 Map
을 쓸까?
Object
로 쓰는 게 나은 경우도 있다!Ref https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Map
UML - 구조를 다이어그램 등으로 시각화하는 방법은 여러가지 표준들이 존재한다. 각 네모나 화살표, 점선, 화살표 방향(의존) 등이 의미하는 바가 있다.
dom selector의 단수/복수를 한번에 처리하는 방법
const $ = (selector) => {
const selected = document.querySelectorAll(selector);
return selected.length === 1 ? selected[0] : selected;
};
error 여부 판단 (하루)
const { isError, message, change } = this.validatePurchaseAmount(purchaseAmount);
if (isError) {
alert(message);
clearInputValue(this.$purchaseAmountInput);
this.$purchaseAmountInput.focus();
return;
}
validatePurchaseAmount(purchaseAmount) {
if (purchaseAmount % MONETARY_UNIT) {
return {
isError: true,
message: ...,
};
}
controlled, uncontrolled 컴포넌트
state
에 값을 담아 form에서 submit해주는 형식이다. setState
를 사용하는 것이 일반적이다. 반면, uncontrolled 컴포넌트에서 form data는 DOM 그 자체에 의해 처리된다. react에서는 ref
를 사용하여, 지금 이순간 input에 담긴 값을 form에서 submit해준다.Ref
html 전체를 아우르는 main
태그 만들기
DOMContentLoaded
를 사용하여 초기 HTML 문서를 완전히 불러오고 분석했을 때 앱을 실행하자.
document.addEventListener("DOMContentLoaded", () => {
const app = new App($("#app"));
app.execute();
});
redux 따라하기
action
export const updatePayment = (value) => {
"use strict";
return {
type: UPDATE_PAYMENT,
payload: { payment: value },
};
};
reducer
export const payment = (state = 0, { type, payload = {} }) => {
switch (type) {
case UPDATE_PAYMENT:
if (payload.payment) {
return payload.payment;
}
return state;
case RESTART:
return 0;
default:
return state;
}
};
리듀서는 순수함수여야 한다. 사이드 이팩트가 없고 넣은게 같다면 나오는 것도 같아야 한다. 다시 말하면 내부에서 random을 호출하는 코드가 존재 하면 안 된다. (직접 호출이 아니라고 하더라도)
Object.seal()
Object.freeze()
와의 차이라고 할 수 있다.도메인에 의존적인 상태들(payment, lottos… 등등)과 라이브러리(리덕스 역할의)가 되는 Store를 분리하는 것이 좋다. 그래야 재사용이 가능해진다.
singleton pattern
prettier의 printWidth
는 80 → 120로 변경되는 게 대세!
‘식’ 대신 조건’문’ 사용하기
!numbers.includes(randomNumber) && numbers.push(randomNumber);
개발자 각자의 취향 차이라고 볼 수도 있지만, ‘문’이 아닌 ‘식’으로만 써야하는 제약이 있는 상황이 아니라면
if (!numbers.includes(randomNumber)) {
numbers.push(randomNumber);
}
이렇게 조건’문’으로 작성된 코드의 가독성이 일반적으로 더 잘 읽힌다.
각 validator에 이름 붙여 return하기
export const validator = {
purchaseAmount: money => {
if (!(money / UNIT_AMOUNT > 0 && money % UNIT_AMOUNT === 0)) {
return MSG_INVALID_PURCHASE_AMOUNT;
}
return '';
},
lottoNumbers: numbers => {
if (numbers.length < WINNING_NUMBER_COUNT) {
return MSG_BLANK_INPUT;
}
if (!isRangeOf(MIN_LOTTO_NUMBER, MAX_LOTTO_NUMBER, numbers)) {
return MSG_OUT_RANGED_LOTTO_NUMBERS;
}
...
return '';
},
};
원하는 form을 선택해서 reset시키기
document.querySelector("price-form").reset();
Object.hasOwnProperty.call
// Object의 hasOwnProperty를 사용하고 'this'를 foo로 명시적 바인딩한 후 호출할 수 있다.
({}.hasOwnProperty.call(foo, "bar")); // true
// 같은 목적으로 Object prototype의 `hasOwnProperty`를 사용할 수도 있다.
Object.prototype.hasOwnProperty.call(foo, "bar"); // true
Ref https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Object/hasOwnProperty
private hash(#) prefix 사용하기
export class Lotto {
#numbers;
constructor(numbers) {
this.#numbers = numbers;
}
get numbers() {
return this.#numbers;
}
}
memoize
구현하여 cache
기능 따라해보기 😮
export function memoize(callback) {
const cache = new Map();
return function (arg) {
if (!cache.get(arg)) {
cache.set(arg, new callback(arg));
}
return cache.get(arg);
};
}
🙄 이 캐시, 어디서 썼나? 바로 아래에서 👇
DOM을 계속해서 select하는 문제
depth-first pre-order traversal
(깊이 우선 preorder 탐색) 한다고 되어 있다. 즉 querySelector의 성능은 DOM tree의 크기와 어느정도 비례한다는 것이다.라이브러리에서 throw error하기
undefined
를 리턴하는것 보다는, 명시적으로 자바스크립트가 에러를 뱉고 동작을 정지할 수 있도록 throw new Error
를 하는 것이 좋다.undefined
를 리턴할 경우 사용부에서 에러가 나지않으면 이 라이브러리를 잘못 사용했는데도 그 원인이 어디에 있는지 파악하기 어려울 수 있다.this
binding이 많이 사용되는 class에서는 bind를 미리 선언하기
this.onRestart = this.onRestart.bind(this);
this.$restartButton.addEventListener("click", this.onRestart);
toFixed(Int)
로 소수점 자리수 나타내기
단방향 데이터 플로우 (flux 패턴)
dom selector에 기능 붙이기
export function $(selector) {
const target = document.querySelector(selector);
const $customElement = Object.assign(target, {
clearChildren: function () {
while (this.hasChildNodes()) {
this.removeChild(this.firstChild);
}
},
disable: function () {
this.disabled = true;
},
enable: function () {
this.disabled = false;
},
});
return $customElement;
}
브라우저 서포트 검색하기
Object deepfreeze
export const deepFreeze = (target) => {
if (target && typeof target === "object" && !Object.isFrozen(target)) {
Object.freeze(target);
Object.keys(target).forEach((key) => deepFreeze(target[key]));
}
return target;
};
WeakMap
사용하여 cache
구현하기
const cache = new WeakMap();
WeakMap
객체는 키가 약하게 참조되는 키/값 쌍의 컬렉션이다. 키는 객체여야만 하나 값은 임의 값이 될 수 있다.WeakMap
내 키는 약하게 유지된다. 다른 강한 키 참조가 없는 경우, 모든 항목은 가비지 컬렉터에 의해 WeakMap
에서 제거된다.Ref https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/WeakMap
class나 id보다 dataset이 성능은 느리다?
Proxy handler 다루기
덕 타이핑 사용해보기
CypressWrapper
사용
class CypressWrapper {
_getCy(selector) {
return cy.get(selector)
}
type(selector, value) {
try {
this._getCy(selector).type(value)
} catch (err) {
new Error(err)
}
return this
}
click(selector, params) {
try {
this._getCy(selector).click(params)
} catch (err) {
new Error(err)
}
return this
}
...
}
const cw = new CypressWrapper()
reduce
로 총합 구하기
profitRate() {
const income = Object.values(this.#lottoResult).reduce((acc, cur) => {
return acc + cur.price * cur.count
}, 0)
return getProfitRate(income, this.#tickets.length * 1000)
}
cypress should
& and
cy.wrap(winningNumberInput)
.should("have.value", "")
.and("be.focused")
.type(winningNumbers[index]);
class의 메소드들
innerHTML
vs innerText
vs textContent
textContent
는 노드의 모든 요소를 반환한다. 그에 비해 innerText
는 스타일링을 고려하며, “숨겨진” 요소의 텍스트는 반환하지 않는다.innerText
의 CSS 고려로 인해, innerText 값을 읽으면 최신 계산값을 반영하기 위해 리플로우가 발생한다. (리플로우 계산은 비싸므로 가능하면 피해야 한다.)innerText
를 수정하면 요소의 모든 자식 노드를 제거하고, 모든 자손 텍스트 노드를 영구히 파괴한다. 이로 인해 해당 텍스트 노드를 이후에 다른 노드는 물론 같은 노드에 삽입하는 것도 불가능하다.Element.innerHTML
는 이름 그대로 HTML을 반환한다. 간혹 innerHTML
을 사용해 요소의 텍스트를 가져오거나 쓰는 경우가 있지만, HTML로 분석할 필요가 없다는 점에서 textContent
의 성능이 더 좋다.textContent
는 XSS 공격 (en-US)의 위험이 없다.textContent
를 사용하는 것이 좋다. 성능과 보안에 강점이 있고, 결과적으로 해당 노드의 raw text를 얻게 됨으로써 이후 의도한 대로 가공할 수 있기 때문이다.convention 정의
## 📓 Convention
- 클래스에서 속성명 앞에 _ 가 붙여진 것은 해당 속성이 protected 속성임을 뜻하며 인스턴스 외부에서 해당 속성을 수정해서는 안됩니다.
- 클래스에서 속성명 앞에 # 가 붙여진 것은 해당 속성이 private 속성임을 뜻하며 상속이 수행된 클래스 내부에서도 해당 속성을 사용하려 해서는 안됩니다.
- 어떤 인스턴스에 pascal case 표기된 속성이 있다면 해당 속성이 사실 getter 임을 뜻합니다. 이를 조작하려는 시도를 하지 않도록 유의해주십시오.
Node.nodeType
Ref https://developer.mozilla.org/en-US/docs/Web/API/Node/nodeType
javascript는 타입이 느슨하여, parameter가 이상하게 들어올 수 있는 확률이 굉장히 높다. 이에 대비를 잘 해주자
Service layer pattern
클래스의 메소드보다 변수를 아래로? 비유를 하면, 자전거가 굴러가는지, 안 굴러가는지는 내부 상태를 정의한 값으로만은 알 수 없다. 상태만 정의되어있고 move라는 행동이 없다면 굴러가지 않는다. 대규모 프로그래밍을 하게되면, 사실 상태가 의미가 없는 경우가 많다. 해당 클래스를 빠르게 보면서 이 친구는 어떤 역할을 하는 친구인지, 어떤 행동을 하는지만 알면 대략적으로 어느 부분을 수정해야 하는지 확인할 수 있고, 수정해야 하는 메서드를 잘 나눠놓았다면 메서드 단위로 수정을 해주면 된다. 해당 메서드를 수정할 때에는 내부에서 사용되는 상태를 파악하면 되는 것. 결국 상태는 많이 확인을 안하게 된다. 다만 이 전제는 객체지향적으로 메소드나 함수, 클래스를 잘 짜놓았을 경우의 상황이다.
함수 작성 순서
Ref https://github.com/woowacourse/javascript-racingcar/pull/25#discussion_r576201158