티스토리 뷰

1) 객체 지향 프로그래밍에 대해 배웠다.

그 동안 단순히 함수로 문제를 해결해왔다. 하지만 class의 constructor, 메서드 등을 정의하고 사용해보며 객체 지향 프로그래밍에 대해 배울 수 있었고 장점을 느낄 수 있었다. 특히 하나의 class 내에 비슷한 역할을 하는 메서드들을 한 곳에 모을 수 있기 때문에 구조화된 것을 볼 수 있어서 좋았다. 그러나 class의 장점 중 하나인 재사용성을 제대로 활용해보지 못한 부분이 아쉬웠다.

 

2) MVC패턴을 적용함으로써 구조화된 프로젝트를 경험했다.

파일의 구조를 model, view, controller로 분리했다. model은 상태를 관리, view는 렌더링(출력)을 관리, controller는 사용자의 동작을 처리하고 view와 model 사이에서 중재하는 역할을 했다. 

 

아래의 Model 클래스에서는 컴퓨터의 값, 사용자의 값을 관리하는 곳이며 setter메서드에 의해 값들을 업데이트 한다.

/* BaseballGameModel.js */
class BaseballGameModel {
  constructor(){
    this.computerValue = "";
    this.userValue = "";
  }

  setComputerValue(data){
    this.computerValue = data;
  }

  setUserValue(data){
    this.userValue = data;
  }
}

module.exports = BaseballGameModel;

아래의 View 클래스에서는 게임의 결과를 출력한다.

/* BaseballGameView.js */
class BaseballGameView {
  print(message){
    printConsole(message);
  }

  printResultGame(strike, ball){
    if (ball && !strike){
      this.print(ball);
    } else if (!ball && strike){
      this.print(strike);
    } else if (ball && strike){
      this.print(`${ball} ${strike}`);
    } else if (!ball && !strike){
      this.print(GAME_MESSAGE.NOTHING);
    }
  }
}

module.exports = BaseballGameView;

 

아래의 Controller 클래스에서는 게임 시작, 게임 재시작, 게임 실행, 게임 성공 시 등 사용자의 동작에 대해 처리하며 view와 model 사이에서 중재하는 역할을 한다.

/* BaseballGameController.js */
class BaseballGameController {
  constructor(baseballGameModel, baseballGameView) {
    this.baseballGameModel = baseballGameModel;
    this.baseballGameView = baseballGameView;
  }

  // 게임 시작 메서드
  startGame() {
    this.baseballGameView.print(GAME_MESSAGE.START);
    this.baseballGameModel.setComputerValue(getRandomNumbers());
    this.triggerGame();
  }

  // 게임을 시작하는 메서드
  triggerGame() {
    triggerConsole(GAME_MESSAGE.INPUT_NUMBERS, (number) => {
      if (isValidateNumbers(number)) {
        this.baseballGameModel.setUserValue(number);
        this.resultGame();
      }
    });
  }

  // 게임 결과를 확인하는 메서드
  resultGame() {
    const strike = getStrike(
      this.baseballGameModel.computerValue,
      this.baseballGameModel.userValue,
    );
    const ball = getBall(this.baseballGameModel.computerValue, this.baseballGameModel.userValue);
    if (strike !== `${GAME_RULE.STRIKE}${GAME_MESSAGE.STRIKE}`) {
      this.baseballGameView.printResultGame(strike, ball);
      this.triggerGame();
    } else if (strike === `${GAME_RULE.STRIKE}${GAME_MESSAGE.STRIKE}`) {
      this.baseballGameView.print(GAME_MESSAGE.SUCCESS);
      this.successGame();
    }
  }

  // 게임 성공 시 호출되는 메서드
  successGame() {
    triggerConsole(GAME_MESSAGE.INPUT_NUMBER, (number) => {
      if (isValidateNumber(number)) {
        if (number === GAME_RULE.RESTART_NUMBER) {
          this.restartGame();
        } else if (number === GAME_RULE.FINISH_NUMBER) {
          closeConsole();
        }
      }
    });
  }

  // 게임 재시작 메서드
  restartGame() {
    this.baseballGameModel.setComputerValue(getRandomNumbers());
    this.triggerGame();
  }
}

module.exports = BaseballGameController;

상위 클래스인 App 클래스에서는 Model과 View 클래스는 독립적으로, Controller 클래스에서는 의존성을 주입하도록 아래와 같이 정의했다.

/* App.js */
class App {
  play() {}
  constructor() {
    this.baseballGameModel = new BaseballGameModel();
    this.baseballGameView = new BaseballGameView();
    this.baseballGameController = new BaseballGameController(this.baseballGameModel,this.baseballGameView);
  }

  play() {
    this.baseballGameController.startGame();
  }
}

module.exports = App;

각각 역할이 체계적으로 나누어져 있어서 관리하기가 편했다. 하지만 아직 MVC들의 역할을 조금 더 명확히 공부해야될 필요를 느꼈다. 컴퓨터의 랜덤값을 구하는 로직을 model에서 처리해야할지 controller에서 처리해야할 지, 스트라이크와 볼의 개수를 구하는 로직을 controller에서 처리해야할 지 따로 utils에서 처리해야할 지에 대해 명확한 답을 얻지 못한 부분이 있어서 앞으로 개선해야될 부분인 것 같다.

 

3) 클린 코드를 배웠다.

클린 코드를 위해 주어진 제약 사항들이 많았다. 프로젝트를 진행하며 기능 구현보다 주어진 제한사항들을 지키려는 것이 더 힘들었지만, 덕분에 그 동안 기능 구현에만 신경을 썼던 모습을 반성할 수 있었다. 그래서 개발자로서 올바른 자세로 교정을 한다는 생각으로 즐겁게 진행했다. 첫 번째로 메서드(함수)와 변수 네이밍은 절대 축약하지 않고 길어지더라도 무슨 역할을 하는 지에 맞춰서 작성했다. 또한 너무 네이밍이 길어진다는 뜻은 많은 일을 하고 있다는 뜻이기 때문에 메서드를 더 분리하려고 노력했다. 두 번째로는 자바스크립트 코드 컨벤션을 지키는 것이였다. Airbnb 스타일 가이드를 정독하며 코드 스타일에 대해서 많이 배웠다. 하지만 정해진 코드 스타일을 모두 외워서 적용해야 되는지 고민이 있었지만, eslint의 extends에 Airbnb를 적용하여 해결 할 수 있었으며 추가적으로 rules에 인덴트가 3이 넘지 않도록 설정하여 클린 코드를 위해 더 제한을 두었다.

이렇듯 클린 코드를 위한 노력을 통해 주석 없이도 함수 이름 자체에 역할을 알 수 있었고, 함수들이 간결해질 수 있었고, eslint를 통해 코드 스타일 교정하는데 도움이 많이 되었다.

 

4) 상수화를 하며 유지보수를 위해 신경을 썼다.

상수화를 통해 리팩토링 작업을 했다. 고정된 값인 문자열, 게임의 규칙(숫자) 등을 상수화 하며 constant파일에 정의하였다. 이를 통해 고정된 값을 여러 곳에서 정의하는 문제를 피할 수 있었고, 한 곳의 파일에 모아두니 가독성과 수정하는 면에서 편리함을 느낄 수 있었다.

/* constant.js */
const GAME_RULE = {
  RESTART_NUMBER: '1',
  FINISH_NUMBER: '2',
  NUMBERS_LENGTH: 3,
  STRIKE: 3,
  MIN_NUMBER: 1,
  MAX_NUMBER: 9,
};

const GAME_MESSAGE = {
  START: '숫자 야구 게임을 시작합니다.',
  INPUT_NUMBERS: '숫자를 입력해주세요 : ',
  SUCCESS: `${GAME_RULE.STRIKE}스트라이크\n${GAME_RULE.STRIKE}개의 숫자를 모두 맞히셨습니다! 게임 종료`,
  INPUT_NUMBER: `게임을 새로 시작하려면 ${GAME_RULE.RESTART_NUMBER}, 종료하려면 ${GAME_RULE.FINISH_NUMBER}를 입력하세요.\n`,
  STRIKE: '스트라이크',
  BALL: '볼',
  NOTHING: '낫싱',
};

const GAME_ERROR_MESSAGE = {
  NOT_VALID_VALUE: '숫자가 아닙니다.',
  NOT_VALID_LENGTH: `길이가 ${GAME_RULE.NUMBERS_LENGTH}이 아닙니다.`,
  NOT_VALID_NUMBER: `${GAME_RULE.RESTART_NUMBER} 또는 ${GAME_RULE.FINISH_NUMBER}의 값이 아닙니다.`,
  INCLUDE_ZERO: '0이 포함되어 있습니다.',
  DUPLICATE_NUMBER: '중복된 숫자가 존재합니다.',
};

module.exports = {
  GAME_RULE,
  GAME_MESSAGE,
  GAME_ERROR_MESSAGE,
};

 

5) node.js 환경에서 입출력 문제를 해결할 수 있는 자바스크립트 파일 실행 방법을 알게 되었다.

그 동안 node.js 환경에서 자바스크립트 파일 실행을 할 때 Code Runner를 이용해서 실행했다. 하지만 이번 미션에서 이렇게 실행시키면 입력을 받을 수가 없었다. 그래서 node.js환경에서 다른 방법으로 자바스크립트 파일을 실행할 수 있는 방법을 알게 되었다. 바로 아래의 명령어를 통해 실행할 수 있었다.

$ node src/App.js

 

6) 기능 목록을 작성함으로써 프로젝트를 체계적으로 진행할 수 있었다.

1주차에서 배웠던대로 이번에도 기능 목록을 작성한 후 프로젝트를 개발함으로써 체계적으로 나아갈 수 있었다. 하지만 1주차보다 더 많은 요구사항들을 통해 기능 목록을 작성하려니 초기에 시간이 많이 소요되는 문제가 있었다. 그래서 요구사항들의 더 큰 틀만 잡고 개발하면서 점차 세부 내용을 작성하는 방법을 선택해야 할 지 조금 더 생각해봐야 할 것 같다. 

## 📌 구현할 기능 목록

- [x] 1. 게임을 시작하는 기능
  - [x] MissonUtils.Console.print를 이용하여 게임 시작 문구를 출력한다.
    ```
      숫자 야구 게임을 시작합니다.
    ```
- [x] 2. 컴퓨터의 임의의 수를 생성하는 기능
  - [x] 각 숫자는 1~9까지이여야 한다.
  - [x] 각 숫자는 서로 달라야 한다.
  - [x] 3자리 수이어야 한다.
  - [x] MissionUtils.Random.pickNumberInRange를 이용하여 랜덤값을 생성한다.
- [x] 3. 사용자의 입력을 받는 기능
  - [x] 각 숫자는 1~9까지이여야 한다.
  - [x] 각 숫자는 서로 달라야 한다.
  - [x] 3자리 수이어야 한다.
  - [x] MissionUtils.Console.readLine를 이용하여 입력 받는다.
    ```
    숫자를 입력해주세요 :
    ```
- [x] 4. 볼과 스트라이크의 개수를 구하는 기능 
  - [x] 컴퓨터의 수와 사용자의 수를 비교하여 같은 자리에 동일한 숫자가 몇 개 있는지 스트라이크 개수를 구한다.
  - [x] 컴퓨터의 수와 사용자의 수를 비교하여 다른 자리에 동일한 숫자가 몇 개 있는지 볼 개수를 구한다.
- [x] 5. 게임의 결과를 출력하는 기능
  - [x] 볼 또는 스트라이크 개수 중 1개 이상인 것만 출력한다. `1볼` 또는 `1스트라이크`
  - [x] 볼과 스트라이크 둘 다 1개 이상일 때 볼을 먼저 출력한다. `1볼 1스트라이크`
  - [x] 볼과 스트라이크 둘 다 0개 일 때 낫싱을 출력한다. `낫싱`
  - [x] 3개의 숫자를 모두 맞힐 경우 3스트라이크를 출력한다. `3스트라이크` 
  - [x] MissonUtils.Console.print를 이용하여 출력한다.
- [x] 6. 게임을 종료하는 기능
  - [x] 3스트라이크일 때 게임을 종료한다.
  - [x] 게임 종료 후 사용자의 입력 1 또는 2를 입력 받아야 한다.
    ```
      3개의 숫자를 모두 맞히셨습니다! 게임 종료
      게임을 새로 시작하려면 1, 종료하려면 2를 입력하세요.
    ```
  - [x] 사용자가 1을 입력하면 게임을 재시작할 수 있다.
  - [x] 사용자가 2를 입력하면 완전히 종료할 수 있다.
- [x] 7. 게임을 재시작하는 기능
  - [x] 컴퓨터의 임의의 수를 재성성한 후 게임을 진행한다.
- [x] 8. throw을 사용해 예외 발생시키는 기능
  - [x] 게임 진행 시 사용자의 입력 형식이 기능 목록 3번의 형식과 다를 경우
  - [x] 게임 종료 시 사용자의 입력이 1 또는 2 이외의 값일 경우
## 📌 테스트 목록
- [x] 1. [기능 목록 1번] 게임을 시작하는 기능 테스트 코드
- [x] 2. [기능 목록 2번] 컴퓨터의 임의의 수를 생성하는 기능 테스트 코드
- [x] 3. [기능 목록 3번] 사용자 입력값의 형식이 잘못되었을 때 예외 발생 테스트 코드
- [x] 4. [기능 목록 4번] 스트라이크. 볼의 개수를 구하는 기능 테스트 코드
- [x] 5. [기능 목록 5,6,7번] 게임 재시작, 종료 기능 테스트 코드
- [x] 6. [기능 목록 8번] 게임 종료 시 사용자 입력 1 또는 2 이외의 값일 경우 예외 발생 테스트 코드

 

7) 각각의 기능들의 테스트 코드를 작성해보며 배웠다.

이번 미션에서 가장 어렵고 시간을 많이 투자한 부분이였다. 무슨 테스트를 어떻게 하는거지? Jest의 메서드들은 어떻게 사용하는거지? 등 많은 고민과 여러움을 겪었다. 그래서 먼저 ApplicationTest.js와 StringTest.js에서 작성되어 있는 테스트 코드와 Jest 공식문서를 보며 최대한 이해하려고 노력했다. 이 때 정말 신기했던 것은 가짜 함수를 만들어 임의로 반환값을 설정해줄 수도 있고, spyOn을 사용하여 해당 메서드가 어떻게 호출됐는지, 반환값 등 정보를 빼내올 수 있는 것이 신기했다. 이후 점차 구현했던 기능 목록에 맞춰 테스트 코드를 작성하였더니 Jest의 메서드와 유닛 테스트에 조금 더 익숙해졌으며 각각의 함수가 구현한 기능에 맞는 역할을 하는지 테스트를 하였더니 프로젝트에 안정감이 생길 수 있었다.

/*  BaseballGameTest.js */
const MissionUtils = require('@woowacourse/mission-utils');
const App = require('../src/App');
const { getRandomNumbers, getStrike, getBall } = require('../src/utils/core');

const getSpy = (object, methodName) => {
  const spy = jest.spyOn(object, methodName);
  spy.mockClear();
  return spy;
};

const mockUserValue = (numbers) => {
  MissionUtils.Console.readLine = jest.fn();
  numbers.reduce(
    (acc, input) =>
      acc.mockImplementationOnce((question, callback) => {
        callback(input);
      }),
    MissionUtils.Console.readLine,
  );
};

const mockRandoms = (numbers) => {
  MissionUtils.Random.pickNumberInRange = jest.fn();
  numbers.reduce(
    (acc, number) => acc.mockReturnValueOnce(number),
    MissionUtils.Random.pickNumberInRange,
  );
};

describe('숫자 야구 게임', () => {
  test('게임 시작 문구 출력', () => {
    const logSpy = getSpy(MissionUtils.Console, 'print');
    const message = '숫자 야구 게임을 시작합니다.';

    const app = new App();
    app.play();

    expect(logSpy).toHaveBeenCalledWith(message);
  });

  test('1에서 9까지의 서로 다른 3자리 수 생성', () => {
    const pickNumberSpy = getSpy(MissionUtils.Random, 'pickNumberInRange');
    const result = getRandomNumbers();
    const removeDuplicatedNumber = new Set(result);

    expect(pickNumberSpy).toHaveBeenCalledWith(1, 9);
    expect(result).toHaveLength(3);
    expect(result).toEqual(expect.not.stringContaining('0'));
    expect(result.length).toEqual(removeDuplicatedNumber.size);
  });

  test('사용자의 입력값이 1에서 9까지의 서로 다른 3자리 수가 아닐 때 예외 발생', () => {
    const userValue = ['000', '120', '102', '0', '12', '3', '121', 'a', 'AAA'];

    mockUserValue(userValue);

    for (let index = 0; index < userValue.length; index += 1) {
      expect(() => {
        const app = new App();
        app.play();
      }).toThrow();
    }
  });

  test('스트라이크, 볼 개수 계산', () => {
    const computerValue = ['132', '283', '632', '192', '527'];
    const userValue = ['123', '139', '632', '132', '752'];
    const strike = ['1스트라이크', '', '3스트라이크', '2스트라이크', ''];
    const ball = ['2볼', '1볼', '', '', '3볼'];

    for (let index = 0; index < computerValue.length; index += 1) {
      const resultStrike = getStrike(computerValue[index], userValue[index]);
      const resultBall = getBall(computerValue[index], userValue[index]);

      expect(resultStrike).toEqual(strike[index]);
      expect(resultBall).toEqual(ball[index]);
    }
  });

  test('게임 재시작, 종료 기능', () => {
    const randoms = [1, 9, 2, 4, 6, 1];
    const userValue = ['129', '192', '1', '123', '461', '2'];
    const logSpy = getSpy(MissionUtils.Console, 'print');
    const result = [
      '2볼 1스트라이크',
      '3스트라이크',
      '1볼',
      '3스트라이크',
      '3개의 숫자를 모두 맞히셨습니다! 게임 종료',
    ];

    mockRandoms(randoms);
    mockUserValue(userValue);

    const app = new App();
    app.play();

    result.forEach((output) => {
      expect(logSpy).toHaveBeenCalledWith(expect.stringContaining(output));
    });
  });

  test('게임 종료 후 사용자의 입력값이 1 또는 2 이외의 값일 때 예외 발생', () => {
    const randoms = [7, 1, 5];
    const userValue = ['715', '3'];

    mockRandoms(randoms);
    mockUserValue(userValue);

    expect(() => {
      const app = new App();
      app.play();
    }).toThrow();
  });
});

2주차 숫자 야구 게임 Pull Request

 

[숫자 야구 게임] 박근우 미션 제출합니다. by geunu97 · Pull Request #198 · woowacourse-precourse/javascript-bas

 

github.com

 

'회고' 카테고리의 다른 글

[우아한테크코스 5기 FE] 4주차 프리코스 회고  (0) 2022.11.22
댓글
Total
Today
Yesterday