[우아한테크코스 FE 5기] 레벨1 로또 게임 미션 회고

Chex·2023년 2월 27일
1

우아한테크코스

목록 보기
7/19
post-thumbnail

🎱 로또 게임

📍 학습 목표

  • UI와 도메인 영역을 분리할 수 있는 설계를 고민해보고, 목적에 맞게 객체와 함수를 활용
  • 단위 테스트 기반으로 점진적인 리팩터링
  • 기본적인 웹 표준을 준수하는 웹 UI 개발

🚀 기능 요구 사항

  • step1-콘솔 기반 로또 게임
    • 로또 구입 금액을 입력하면 구입 금액에 해당하는 만큼 로또를 발행해야 한다.
    • 로또 번호는 오름차순으로 정렬하여 보여준다.
    • 로또 1장의 가격은 1,000원이다.
    • 당첨 번호와 보너스 번호를 입력받는다.
    • 사용자가 구매한 로또 번호와 당첨 번호를 비교하여 당첨 내역 및 수익률을 출력한다.
    • 당첨은 1등부터 5등까지 있다. 당첨 기준과 금액은 아래와 같다.
      • 1등: 6개 번호 일치 / 2,000,000,000원
      • 2등: 5개 번호 + 보너스 번호 일치 / 30,000,000원
      • 3등: 5개 번호 일치 / 1,500,000원
      • 4등: 4개 번호 일치 / 50,000원
      • 5등: 3개 번호 일치 / 5,000원
    • 당첨 통계를 출력한 뒤에는 재시작/종료 여부를 입력받는다.
      • 재시작할 경우 구입 금액 입력부터 게임을 다시 시작하고, 종료하는 경우 그대로 프로그램을 종료시킨다.
    • 사용자가 잘못된 값을 입력한 경우 throw문을 사용해 예외를 발생시키고, 에러 메시지를 출력 후 그 부분부터 입력을 다시 받는다.
  • step2-웹 기반 로또 게임
    • 웹 UI를 사용해 로또 게임의 주요 기능을 사용할 수 있어야 한다.
      • 로또 구매
      • 구매한 로또 목록 확인
      • 당첨 번호 및 보너스 번호 지정
      • 당첨 통계 확인
      • 게임 재시작

✅ 프로그래밍 요구사항

예측 가능하고, 실수를 방지할 수 있는 코드를 작성하기 위해 노력한다.

  • 변수 선언시 const 만 사용한다.
  • 함수(또는 메서드)의 들여쓰기 depth는 1단계까지만 허용한다.
  • 함수의 매개변수는 2개 이하여야 한다.
  • 함수에서 부수 효과를 분리하고, 가능한 순수 함수를 많이 활용한다.

테스트하기 쉬운 코드에 대해 고민하고, 문제를 작은 단위로 쪼개서 접근하는 방식을 연습한다.

  • 모든 기능을 TDD로 구현하는 것을 시도하여, 테스트 할 수 있는 도메인 로직에 대해서는 모두 단위 테스트가 존재해야 한다. (단, UI 로직은 제외)

모듈화에 대해 고민한다.

  • 클래스(또는 객체)를 사용하는 경우, 프로퍼티를 외부에서 직접 꺼내지 않는다. 객체에 메시지를 보내도록 한다.
  • 클래스를 사용하는 경우, 3개 이상의 인스턴스 변수를 가진 클래스를 쓰지 않는다.

1단계

모듈화와 객체 간에 로직을 재사용하는 방법에 대해 고민한다.

  • 로또 번호와 당첨 로또 번호의 유효성 검사시 발생하는 중복 코드를 제거해야 한다.
  • 클래스(또는 객체)를 사용하는 경우, 프로퍼티를 외부에서 직접 꺼내지 않는다. 객체에 메시지를 보내도록 한다.
    • getter를 금지하는 것이 아니라 말 그대로 프로퍼티 자체를 그대로 꺼내서 객체 바깥에서 직접 조작하는 등의 작업을 지양하자는 의미입니다 :) 객체 내부에서 알아서 할 수 있는 일은 객체가 스스로 할 수 있게 맡겨주세요.
  • 클래스를 사용하는 경우, 3개 이상의 인스턴스 변수를 가진 클래스를 쓰지 않는다.

2단계

모듈화에 대해 고민한다. - 도메인과 UI 관심사의 분리

  • 1단계에서 구현한 도메인 로직을 (최대한) 수정하지 않고, UI만 변경한다.

일관성 있고 의도가 드러나는 마크업을 작성하기 위해 노력한다.

  • 목적에 맞는 HTML 태그를 사용한다.
  • CSS 속성 선언 순서의 일관성을 고려한다.

CSS 문법 사용에 익숙해진다.

  • CSS 속성은 가능하면 축약형(shorthand)을 사용한다.
  • flexbox를 활용해 레이아웃을 구성한다.

🔗 관련 링크


🐾 기능목록

View

  • 입력받는 객체 생성하기
    • 메서드) 구입 금액 입력받기
    • 예외) 0으로 시작하는 경우
    • 예외) 문자열이 숫자가 아닌 경우
    • 예외) 1000원 단위가 아닌 경우(몫, 나머지 체크)
    • 메서드) 당첨 번호 입력받기
      • 예외) 숫자,숫자 형식이 아닌 경우
      • 예외) 1 ~ 45 사이의 숫자가 아닌 경우
      • 예외) 6개의 숫자가 아닌 경우
      • 예외) 중복인 경우
    • 메서드) 보너스 번호 입력받기
      • 예외) 1 ~ 45 사이의 숫자가 아닌 경우
      • 예외) 당첨 번호와 중복된 숫자를 입력 받는 경우
    • 메서드) 재시작/종료 여부를 입력받기
      • 예외) y 혹은 n 외의 입력을 받은 경우
  • 출력하는 객체 생성하기
    • 메서드) 로또 구입 결과 출력하기
    • 메서드) 당첨 통계 출력하기

Domain

  • 게임 시작 객체 생성하기
  • 로또 게임 객체 생성
    • 프로퍼티) 구입한 금액
    • 프로퍼티) 생성한 로또들
    • 프로퍼티) 당첨된 등수별 갯수들
    • 메서드) 로또 추첨하기
      • 메서드) 당첨된 등수별 갯수 배열로 당첨금액 계산하기
    • 메서드) 구입금액, 당첨금액으로 수익률 계산하기
  • 로또 클래스 생성하기
    • 프로퍼티) 구입한 로또의 번호
    • 메서드) 등수 구하기
      • 메서드) 보너스 숫자가 있는지 판별하기
  • 로또 머신 생성하기
    • 메서드) 구입한 로또 갯수만큼 로또 발행하기
    • 메서드) 로또 한개 발행하기
      • 로또 번호를 오름차순으로 정렬하기

Util

  • 숫자 핸들러 객체 생성하기
    • 메서드) 천의 자리마다 콤마 찍기
    • 메서드) 소수점 둘째자리에서 반올림하기
    • 메서드) 범위 내의 랜덤 숫자값 반환하기
      • 메서드) 몫을 정수형으로 반환하기
  • 배열 핸들러 객체 생성하기
    • 메서드) 두 배열의 동일한 숫자의 개수 반환하기
    • 메서드) 배열을 오름차순으로 정렬하기

📖 배운점

1. HTMLElement 요소에 접근하는 함수들에 관하여(innerText, innerHTML 등)

textContent, innerText, innerHTML, insertAdjacentHTML 등의 차이를 정리한 노션 링크

2. querySelector와 getElementById의 차이에 관하여

  • 공통점?
    • DOM 객체를 찾는 키워드이다.
  • querySelector ?
    • cssSelector로 DOM 객체를 찾는다.
    • document.querySelector('.search-input-style')
  • getElementById ?
    • id로 DOM 객체를 찾는다.
    • document.getElementById('search')

3. Event.preventDefault()에 관하여

  • 기능: DOM요소의 기본 동작을 중단시킨다. 주로 사용되는 경우?
    • a요소를 클릭하면 href 어트리뷰트에 지정된 링크로 이동하지 않도록 한다.
    • form요소 안에 submit 버튼을 눌러도 새로고침되지 않도록 한다.(submit은 O)

4. Form-Submit vs SubmitButton-Click 이벤트 사용 기준에 관하여

  • 여러 input이 들어있는 경우엔 form으로 감싸고 form에 submit이벤트리스너를 추가하여 한번에 form 데이터를 얻자!

5. DOM객체 셀렉팅 연산에 관하여

  • 리뷰어(헤인티)에게 질문 했던 내용

    DOM을 변경하는 것뿐만 아니라 셀렉팅(ex. document.getElementById)만 해오더라도 추가적인 연산이 필요하여 불필요하게 매번 새롭게 셀렉팅하는 것은 안 좋다고 들었는데요. 그렇다고 view에 프로퍼티로 등록하여 사용하면 프로퍼티 개수가 많아져서 별로 안좋을 것 같은데.. 헤인티의 생각은 어떤지 궁금합니다!

  • 리뷰어(헤인티)에게 답변 받은 내용

    장단점이 있는 것 같아요. 셀렉팅을 매번 하면 매번 그 element 가 유효한지를 알 수 있으니 좋지만 매번 연산을 하게 되고, 초기에 한 번만 하게 되면 매번 연산을 하지는 않지만 따지고 보면 매번 그 element 가 유효한지 체크하는 코드가 있어야겠죠? 어떤게 더 유리한지를 생각해보면 좋겠네요. (그리고 사실 이런 작은 프로젝트에서 셀렉팅을 해봤자 얼마나 느려지겠습니까..! ㅋㅋㅋㅋㅋ 1초마다 50개 정도는 셀렉팅해야 고민해볼만한 성능 이슈일 것 같습니다.)

✏️ 느낀점

1. 사용자에게 좀더 친절하기

개발자가 보는 에러 메시지와 사용자가 보는 에러 메시지가 항상 같을까요?

로또 2단계 피드백 시간에 가장 와닿았던 문장이었다.

강의 전날, “유효성 검사를 validator객체의 isValid~메서드로 해주었는데 try / catch 로 또 에러를 잡기 위한 처리를 해줘야하나?”라고 생각했었다.

try/catch는 프로그래머가 예상하지 못한 상황에 대한 에러를 처리할 때 사용하는 것이라는 말을 보았고 실제로도 2단계 리팩터링을 하면서 예상하지 못한 에러를 catch문이 잡아주는 경험을 하면서 그 필요성을 다시금 인식했지만, 위 문장도 에러처리가 필요한 또다른 이유로서 나에게 확 와닿았던 것 같다.

2. 의미에 맞는 HTML 태그 사용하기

1단계 콘솔기반 로또 게임을 → 2단계 웹 기반 로또 게임으로 변경하면서 거의 처음으로 JavaScript 코드에 HTML과 CSS를 붙이는 작업을 해보았다.

그리고 도메인로직과 UI로직을 잘~분리해야하는 이유를 직접 경험하며 깨달을 수 있었다. 각자의 책임에 대하여 분리가 잘 되어있어야 요구사항이 수정되었을 때(콘솔기반→웹기반) 좀더 수월하게 코드를 짤 수 있기 때문이다.

다행히 1단계에서 도메인로직과 UI로직의 분리가 어느정도 괜찮게 됐었는지 lottoGame 객체에 대한 약간의 수정과 유효성 검사 로직을 제외하곤 도메인 로직 수정은 거의 없었다.

하지만 어려움은 여기서 끝이 아니었고, 난 HTML 기본구조 작성부터 어려웠다.

스크린샷 2023-02-27 오전 1 24 51

기본구조를 어떻게 배치해야하는지도 몰랐기 때문이다. 그래서 단순히 묶어주는 역할을 하는 div id로 lucky-numbers-input이라는 id를 주기도 했고 bonus-number라는 클래스명을 주기도 했다. divinput도 아니고 보너스번호를 감싸는 태그였을 뿐이었는데 지금보니 왜 저렇게 했지?싶다. 물론 이 부분은 리뷰어인 헤인티에게 피드백을 받아서 input이란 클래스명은 input태그인 경우에만 부여하는 방식으로 수정했고 보너스번호를 감싸는 div의 클래스명도 control-bonus-number등으로 수정했다.

지금은 약간의 감을 잡은 상태인데 좀더 수련이 필요해보인다. → MDN의 input문서

3. “클래스(또는 객체)를 사용하는 경우, 프로퍼티를 외부에서 직접 꺼내지 않는다”라는 요구사항에 관하여

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

4. 이벤트리스너의 콜백 함수를 화살표 함수로 정의하는 것에 관하여

여기서 한가지 또 생각해봐야할 것은 이벤트리스너의 파라미터로 넘겨주는 콜백함수를 화살표함수로 정의했다는 점이다.

콜백함수를 화살표함수로 작성한 이유는?

콜백함수 내부에서 this를 사용하는 경우, 콜백함수는 호출의 주체를 명시할 수 없기 때문에 this가 전역객체를 가리키므로 this를 바인딩해줘야하는데, 화살표함수 내부에서의 this는 항상 상위 스코프의 this를 가리키기 때문에 bind, call, apply를 통해 화살표함수의 this를 변경할 필요가 없기 때문이다.(변경하고 싶어도 변경할 수도 없다고 함)

  • 화살표함수는 실행 컨텍스트를 생성할 때 this바인딩 과정 자체가 빠지게 되어, 함수 내부에는 this가 아예 없으며, 접근하고자 하면 스코프체인상 가장 가까운 상위 스코프의 this에 접근한다.

궁금한 점?

addEventListener 함수의 콜백 함수 → 이 링크에서는 이벤트리스너의 콜백 함수를 화살표 함수로 정의하면 this가 상위 컨택스트인 전역 객체 window를 가리키기 때문에 콜백 함수 내에서 this를 사용하는 경우 일반 함수를 사용해야 한다고 되어있는데, 이 말을 다시 정리하면 아래와 같다.

addEventListner 메서드는 콜백함수를 호출할 때 자신의 this를 상속하도록 정의되어 있다.

즉, addEventListener의 콜백함수에서는 this에 해당 이벤트리스너가 호출된 엘리먼트가 바인딩되도록 정의되어 있는데, 이처럼 이미 this의 값이 정해져있는 콜백함수의 경우, 화살표 함수를 사용하면 기존 바인딩 값이 사라지고 상위 스코프(이 경우엔 전역 객체)가 바인딩되고 의도했던대로 동작하지 않을 수 있기 때문에 “콜백함수 내에서 this를 사용하는 경우 일반 함수를 사용해야한다”는 말이었다.

this가lottoView

그렇다면, 왜 나의 경우엔 이벤트리스너의 콜백 함수를 화살표 함수로 정의했는데 this가 전역객체가 아니라 lottoView 객체를 참조할까?

나의 생각

나의 경우엔 lottoView객체의 메서드로 bindBuyButtonEventHandler를 정의했는데 이 메서드가 이벤트리스너의 콜백에 쓰인 화살표함수의 상위스코프가 되었고 bindBuyButtonEventHandler메서드의 thislottoView이기 때문에 화살표함수의 thislottoView가 되었던 것 같다.

상위 스코프의 속성들을 사용하기 위해 의도한 경우라면 이벤트리스너의 콜백함수를 화살표함수로 정의하여 사용해도 되지 않을까?가 나의 결론이다.

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의 세계.. 언젠가 다 이해하는 날이 올까?

this 바인딩 관련 참고사항(출처: 코어자바스크립트)

  • 전역공간에서의 this는 전역객체(브라우저에서는 window, Node.js에서는 global)를 참조
  • 함수를 메서드로서 호출한 경우 this는 메서드 호출 주체(메서드명 앞의 객체)를 참조
  • 함수를 함수로서 호출한 경우 this는 전역객체를 참조(메서드의 내부 함수에서도 같음)
  • 콜백 함수 내부에서의 this는 해당 콜백 함수의 제어권을 넘겨받은 함수가 정의한 바에 따르며, 정의하지 않은 경우에는 전역객체를 참조
  • 생성자 함수에서의 this는 생성될 인스턴스를 참조

Be the best version of you!

profile
Fake It till you make It!

0개의 댓글

관련 채용 정보