예측 가능하고, 실수를 방지할 수 있는 코드를 작성하기 위해 노력한다.
- 변수 선언시 const 만 사용한다.
- 함수(또는 메서드)의 들여쓰기 depth는 1단계까지만 허용한다.
- 함수의 매개변수는 2개 이하여야 한다.
- 함수에서 부수 효과를 분리하고, 가능한 순수 함수를 많이 활용한다.
테스트하기 쉬운 코드에 대해 고민하고, 문제를 작은 단위로 쪼개서 접근하는 방식을 연습한다.
- 모든 기능을 TDD로 구현하는 것을 시도하여, 테스트 할 수 있는 도메인 로직에 대해서는 모두 단위 테스트가 존재해야 한다. (단, UI 로직은 제외)
모듈화에 대해 고민한다.
- 클래스(또는 객체)를 사용하는 경우, 프로퍼티를 외부에서 직접 꺼내지 않는다. 객체에 메시지를 보내도록 한다.
- 클래스를 사용하는 경우, 3개 이상의 인스턴스 변수를 가진 클래스를 쓰지 않는다.
모듈화와 객체 간에 로직을 재사용하는 방법에 대해 고민한다.
모듈화에 대해 고민한다. - 도메인과 UI 관심사의 분리
일관성 있고 의도가 드러나는 마크업을 작성하기 위해 노력한다.
CSS 문법 사용에 익숙해진다.
textContent, innerText, innerHTML, insertAdjacentHTML 등의 차이를 정리한 노션 링크
document.querySelector('.search-input-style')
document.getElementById('search')
DOM을 변경하는 것뿐만 아니라 셀렉팅(ex.
document.getElementById
)만 해오더라도 추가적인 연산이 필요하여 불필요하게 매번 새롭게 셀렉팅하는 것은 안 좋다고 들었는데요. 그렇다고view
에 프로퍼티로 등록하여 사용하면 프로퍼티 개수가 많아져서 별로 안좋을 것 같은데.. 헤인티의 생각은 어떤지 궁금합니다!
장단점이 있는 것 같아요. 셀렉팅을 매번 하면 매번 그 element 가 유효한지를 알 수 있으니 좋지만 매번 연산을 하게 되고, 초기에 한 번만 하게 되면 매번 연산을 하지는 않지만 따지고 보면 매번 그 element 가 유효한지 체크하는 코드가 있어야겠죠? 어떤게 더 유리한지를 생각해보면 좋겠네요. (그리고 사실 이런 작은 프로젝트에서 셀렉팅을 해봤자 얼마나 느려지겠습니까..! ㅋㅋㅋㅋㅋ 1초마다 50개 정도는 셀렉팅해야 고민해볼만한 성능 이슈일 것 같습니다.)
개발자가 보는 에러 메시지와 사용자가 보는 에러 메시지가 항상 같을까요?
로또 2단계 피드백 시간에 가장 와닿았던 문장이었다.
강의 전날, “유효성 검사를 validator
객체의 isValid
~메서드로 해주었는데 try
/ catch
로 또 에러를 잡기 위한 처리를 해줘야하나?”라고 생각했었다.
try/catch는 프로그래머가 예상하지 못한 상황에 대한 에러를 처리할 때 사용하는 것이라는 말을 보았고 실제로도 2단계 리팩터링을 하면서 예상하지 못한 에러를 catch문이 잡아주는 경험을 하면서 그 필요성을 다시금 인식했지만, 위 문장도 에러처리가 필요한 또다른 이유로서 나에게 확 와닿았던 것 같다.
1단계 콘솔기반 로또 게임을 → 2단계 웹 기반 로또 게임으로 변경하면서 거의 처음으로 JavaScript 코드에 HTML과 CSS를 붙이는 작업을 해보았다.
그리고 도메인로직과 UI로직을 잘~분리해야하는 이유를 직접 경험하며 깨달을 수 있었다. 각자의 책임에 대하여 분리가 잘 되어있어야 요구사항이 수정되었을 때(콘솔기반→웹기반) 좀더 수월하게 코드를 짤 수 있기 때문이다.
다행히 1단계에서 도메인로직과 UI로직의 분리가 어느정도 괜찮게 됐었는지 lottoGame
객체에 대한 약간의 수정과 유효성 검사 로직을 제외하곤 도메인 로직 수정은 거의 없었다.
하지만 어려움은 여기서 끝이 아니었고, 난 HTML 기본구조 작성부터 어려웠다.
기본구조를 어떻게 배치해야하는지도 몰랐기 때문이다. 그래서 단순히 묶어주는 역할을 하는 div
id로 lucky-numbers-input
이라는 id를 주기도 했고 bonus-number
라는 클래스명을 주기도 했다. div
는 input
도 아니고 보너스번호를 감싸는 태그였을 뿐이었는데 지금보니 왜 저렇게 했지?싶다. 물론 이 부분은 리뷰어인 헤인티에게 피드백을 받아서 input
이란 클래스명은 input
태그인 경우에만 부여하는 방식으로 수정했고 보너스번호를 감싸는 div
의 클래스명도 control-bonus-number
등으로 수정했다.
지금은 약간의 감을 잡은 상태인데 좀더 수련이 필요해보인다. → MDN의 input문서
bindBuyButtonEvent() {
this.view.buyButton.addEventListener('click', this.onClickBuyButton);
}
// LottoGameControllerStep2.js
2단계 로또미션을 처음 구현했을 때에는 위 코드처럼 view
를 컨트롤러의 프로퍼티로 선언하여 view
의 프로퍼티(buyButton
)를 외부(컨트롤러)에서 꺼내서 addEventListener
를 등록해주었다. 왜 이렇게 했냐면 클릭 이벤트가 발생했을 때 컨트롤러의 메서드인 onClickBuyButton
을 실행시켜 buyButton에 대한 유효성 검사와 컨트롤러에서 수행해야하는 다음 로직을 진행하기 위함이었다.
하지만 저렇게 구현하면 프로퍼티를 외부에서 직접 꺼내서 쓰는 것이기 때문에 아~주 옳지 않아보인다. 그래서 찾은 해결방법은 onClickBuyButton
메서드를 bindBuyButtonEvent
의 파라미터로 넘겨서 view
에서 로직을 수행하는 것이었다.
bindBuyButtonEventHandler(onClickBuyButton) {
const buyButton = document.querySelector('#buy-button');
buyButton.addEventListener('click', event => {
event.preventDefault();
// 여기서의 this는? lottoView
const buyMoneyInput = document.querySelector('#buy-money-input');
const buyMoney = Number(buyMoneyInput.value);
try {
lottoGameValidatorStep2.throwErrorIfInvalidBuyMoney(buyMoney);
onClickBuyButton(buyMoney);
buyMoneyInput.value = null;
} catch (error) {
alert(error.message);
buyMoneyInput.focus();
}
});
},
// lottoView.js
이렇게 되면 lottoView
의 프로퍼티를 외부(컨트롤러)에서 꺼낼필요가 없어진다.
class LottoGameControllerStep2 {
lottoGame;
constructor() {
this.bindLottoButtonEventHandlers();
lottoView.bindModalCloseEventHandler(this.onClickModalCloseButton);
}
startGame() {
this.lottoGame = new LottoGame();
}
bindLottoButtonEventHandlers() {
lottoView.bindBuyButtonEventHandler(this.onClickBuyButton);
lottoView.bindShowResultButtonEventHandler(this.onClickShowResultButton);
lottoView.bindModalCloseButtonEventHandler(this.onClickModalCloseButton);
lottoView.bindRestartButtonEventHandler(this.onClickRestartButton);
}
onClickBuyButton = buyMoney => {
this.lottoGame.buyLottos(buyMoney);
const lottoNumbersList = this.lottoGame.getLottoNumbersList();
lottoView.printPurchasedLottos(lottoNumbersList);
};
onClickShowResultButton = (bonusNumber, luckyNumbers) => {
this.lottoGame.initWinningNumbers(luckyNumbers, bonusNumber);
const amountOfRanks = this.lottoGame.getAmountOfRanks();
const profit = this.lottoGame.calculateProfit();
lottoView.printResult(amountOfRanks, profit);
};
onClickModalCloseButton = () => {
this.lottoGame.resetWinningNumbers();
this.lottoGame.resetAmountOfRanks();
};
onClickRestartButton = () => {
this.startGame();
};
}
// lottoGameControllerStep2.js
여기서 한가지 또 생각해봐야할 것은 이벤트리스너의 파라미터로 넘겨주는 콜백함수를 화살표함수로 정의했다는 점이다.
콜백함수 내부에서 this
를 사용하는 경우, 콜백함수는 호출의 주체를 명시할 수 없기 때문에 this
가 전역객체를 가리키므로 this를 바인딩해줘야하는데, 화살표함수 내부에서의 this
는 항상 상위 스코프의 this
를 가리키기 때문에 bind, call, apply
를 통해 화살표함수의 this
를 변경할 필요가 없기 때문이다.(변경하고 싶어도 변경할 수도 없다고 함)
addEventListener 함수의 콜백 함수 → 이 링크에서는 이벤트리스너의 콜백 함수를 화살표 함수로 정의하면 this
가 상위 컨택스트인 전역 객체 window
를 가리키기 때문에 콜백 함수 내에서 this
를 사용하는 경우 일반 함수를 사용해야 한다고 되어있는데, 이 말을 다시 정리하면 아래와 같다.
즉, addEventListener
의 콜백함수에서는 this에 해당 이벤트리스너가 호출된 엘리먼트가 바인딩되도록 정의되어 있는데, 이처럼 이미 this의 값이 정해져있는 콜백함수의 경우, 화살표 함수를 사용하면 기존 바인딩 값이 사라지고 상위 스코프(이 경우엔 전역 객체)가 바인딩되고 의도했던대로 동작하지 않을 수 있기 때문에 “콜백함수 내에서 this를 사용하는 경우 일반 함수를 사용해야한다”는 말이었다.
그렇다면, 왜 나의 경우엔 이벤트리스너의 콜백 함수를 화살표 함수로 정의했는데 this가 전역객체가 아니라 lottoView 객체를 참조할까?
나의 경우엔 lottoView
객체의 메서드로 bindBuyButtonEventHandler
를 정의했는데 이 메서드가 이벤트리스너의 콜백에 쓰인 화살표함수의 상위스코프가 되었고 bindBuyButtonEventHandler
메서드의 this
는 lottoView
이기 때문에 화살표함수의 this
도 lottoView
가 되었던 것 같다.
상위 스코프의 속성들을 사용하기 위해 의도한 경우라면 이벤트리스너의 콜백함수를 화살표함수로 정의하여 사용해도 되지 않을까?가 나의 결론이다.
bindBuyButtonEventHandler(onClickBuyButton) {
const buyButton = document.querySelector('#buy-button');
buyButton.addEventListener('click', event => { // 이벤트리스너의 콜백함수
event.preventDefault();
// 여기서의 this는? lottoView
const buyMoneyInput = document.querySelector('#buy-money-input');
const buyMoney = Number(buyMoneyInput.value);
try {
lottoGameValidatorStep2.throwErrorIfInvalidBuyMoney(buyMoney);
onClickBuyButton(buyMoney);
buyMoneyInput.value = null;
} catch (error) {
alert(error.message);
buyMoneyInput.focus();
}
});
},
// lottoView.js
어려운 this
의 세계.. 언젠가 다 이해하는 날이 올까?
Be the best version of you!