[회고] 우테코 6기 프리코스 1주차 회고

Ninto·2023년 10월 25일
0

회고

목록 보기
1/1
post-thumbnail

🧐 작년 숫자 야구 게임과의 비교

이번 온보딩이자 1주차 과제였던 숫자야구게임은 제가 작년 우테코 5기에 지원하면서 경험해본적 있던 과제였습니다.

작년 프리코스 5th 숫자 야구 게임 PR

전반적으로 숫자 야구 게임에 필요한 핵심 기능들은 작년과 모두 같았지만,
약간의 미세한 차이점들이 있었어요.



1. Node.js의 실행버전 업그레이드

(Node.js 14 -> Node.js 18.17.1)


작년의 경우, Node.js 14버전에서 실행이 가능해야 했지만

올해 미션에서는 아래와 같이 Node.js 18.17.1버전으로 업데이트 되었습니다.


이번 과제를 진행할 당시엔, 제 Node.js가 16버전이였기 때문에 버전 업그레이드가 필요했습니다.

하지만 보다시피 Node.js의 공식홈페이지에 올라오는 버전들은 최신 업데이트가 진행되기 때문에 작년 기준으로 LTS였던 18.17.1버전을 찾기가 어려웠습니다.
(Window 환경에선 해당 버전에 맞는 Node.js 파일이 필요했어요.)

그래서 Node.js dist 목록에 들어가 해당하는 버전의 win-x64 실행파일을 재설치했습니다.


Node.js 18.17.1버전을 사용하면서 가장 크게 느꼈던건 모듈을 불러오고, 내보내는 방식이 기존에 사용했던 Node.js 14버전의 방식과 달라졌다는 점이였어요.

// 14버전을 사용할땐 require만 가능했음
const { Random, Console } = require("@woowacourse/mission-utils");
...
module.exports = App;


// 18.17.1버전에선 import 사용가능
import BullsAndCowsGameController from "./controller/BullsAndCowsGameController.js";
...
export default App;


2. 제공된 라이브러리 변경


▼ 작년 미션때 제공된 라이브러리

▼ 올해 미션때 제공된 라이브러리

언뜻 보기엔 비슷해 보이지만, 사용자의 값을 입력받는 메서드가 달라졌습니다.
Console.readLineConsole.readLineAsync의 차이가 있었죠.


제공된 미션 유틸 라이브러리에서 찾아보면 해당 메서드에 대한 정의와 예시가 나와있습니다.

  • readLine(query, callback)

    • 주어진 질문을 화면에 출력하고, 사용자가 답변을 입력할 때까지 기다린 다음 입력된 답변을 인수로 전달하는 콜백 함수를 호출한다.
// readLine(query, callback) 사용예시
Console.readLine("닉네임을 입력해주세요.", (answer) => {
  console.log(`닉네임: ${answer}`);
});
  • readLineAsync(query)

    • 주어진 질문을 화면에 출력하고, 사용자가 입력한 답변을 Promise를 통해 반환한다.
// readLineAsync(query) 사용예시
async function getUsername() {
  try {
    const username = await Console.readLineAsync("닉네임을 입력해주세요.");
  } catch (error) {
    // reject 되는 경우
  }
}

사용자가 입력한 답변을 비동기적으로 수행하기 때문에 애플리케이션의 시작점인 App.jsplay메서드 앞에 async가 붙게 되었습니다.

class App {
  async play() {}
}

export default App;

전보다 편해진 점은 Console.readLineAsync은 Console 메서드 외부에서 해당 입력값에 대한 처리가 가능해졌다는 점이였습니다.



👊 이번 과제를 진행하며 목표한 점

저는 이번 과제를 진행하면서 개인적으로 중요하다고 느꼈던 3가지 목표사항들을 세웠어요.

  1. 기능과 역할을 알맞게 분리하고 설계하기
  2. 테스트 코드 작성하기
  3. 코드 컨벤션 지키기

작년 과제를 진행하면서 어려움을 많이 느꼈던 부분들이였기 때문에,
이번에는 위 사항들을 최대한 반영해보고 싶었습니다.


1. MVC패턴으로 구조 재설계

// 작년 숫자야구게임 구조설계
src
|-- App.js

// 올해 숫자야구게임 구조설계
src
|-- constants
|   |-- conditions.js
|   |-- messages.js
|
|-- controller
|   |-- BullsAndCowsGameController.js
|
|-- models
|   |-- ComputerNumberGenerator.js
|   |-- GameAnalyzer.js
|   |-- InputValidator.js
|
|-- views
|   |-- InputView.js
|   |-- OutputView.js
|
|-- App.js

작년의 경우, App.js 파일에서 애플리케이션의 모든 기능을 담당하도록 했었어요.

하지만 하나의 파일에서 애플리케이션에 필요한 입출력, 제어, 도메인 등을 전부 포함시키니 코드의 길이가 비대해지고, 한 눈에 파악하기에도 쉽지 않았습니다.


그래서 애플리케이션의 기능들을 아래와 같이 분리했습니다.

필요한 위 기능들을 MVC 디자인 패턴을 적용해서 역할분담을 손쉽게 할 수 있었습니다.

▼ MVC 디자인 패턴 적용

처음 애플리케이션의 시작점인 App.js는 고정되어있기 때문에, 게임의 컨트롤을 담당하는 컨트롤러를 만들어서 연결시켰어요.

사용자의 입력과 출력에 해당하는 부분들은 뷰로 분리시키고 게임에 필요한 메인 로직들을 모델에 두었습니다.


  1. 첫번째 고민: 컴퓨터 번호를 생성시키는 시점

이때, 개인적으로 고민을 했던 점은 컴퓨터 번호를 생성시키는 시점이였습니다.

숫자 야구 게임이 정상적으로 작동하기 위해선, 컴퓨터 번호가 먼저 정상적으로 생겨야한다고 생각했습니다.

class App {
  #gameController;

  constructor() {
    this.#gameController = new BullsAndCowsGameController(
      new ComputerNumberGenerator()
    );
  }

  async play() {
    await this.#gameController.startGame();
  }
}

그래서 최상단 App에서 컨트롤러를 만들때 의존성 주입을 통해 컴퓨터 모델을 넣어주었어요.


  1. 두번째 고민: 출력과 관련된 로직의 위치

두번째로 고민했던 점은, 출력 부분에 해당했던 기능로직의 위치였어요.

// 출력되야하는 분기들
- n볼
- n스트라이크
- n볼 n스트라이크
- 낫싱

컴퓨터와 사용자의 숫자를 비교하는 모델에서 출력에 해당하는 분기까지 담당을 시키는게 맞을까 고민했습니다.

결론적으로, 위 기능은 출력뷰에서 담당하도록 코드를 수정했습니다.

// OutputView
  printGameProgress(ball, strike) {
    let message = '';

    if (ball === 0 && strike === 0) message = PROGRESS.nothing;
    if (ball > 0) message += `${ball}${PROGRESS.ball}`;
    if (strike > 0) {
      if (message.length > 0) message += PROGRESS.and;
      message += `${strike}${PROGRESS.strike}`;
    }

    return this.printStaticMessage(message);
  },

모델에서 얻은 데이터값을 출력뷰에서 최종적으로 코팅해서 처리하는게 각각의 역할에 맞다고 생각했어요.



2. TDD로 구현하기

이번 과제를 진행하면서, 위에서 설계했던 주요 모델 3가지와 출력값 테스트까지 총 4개의 테스트 코드를 추가했어요.

TDD에서 말하는 원칙먼저 실패하는 테스트를 만들고 프로덕션을 진행하는 것이였는데
이 점이 처음에는 굉장히 어색했습니다.

아무것도 구현되있지 않은 상태에서 테스트 코드부터 먼저 만들기 위해선

  • 어떤 테스트를 할 것 인지
  • 해당 테스트가 어떤 인풋과 아웃풋이 필요한지

위 두 가지 고려사항이 필요했어요.

그래서 1차적으로 기능 구현 목록에 작성해두었던 독립적인 도메인 위주의 작은 테스트부터 추가했어요.

이렇게 하니, 해당 테스트가 성공하기 위해서는 구현해야 되는 프로덕션 코드의 방향성을 잡아나갈 수 있었던 것 같아요.

이번 프리코스를 진행하면서 TDD는 지속적인 연습이 필요할 것 같습니다..!



3. 자바스크립트 코드 컨벤션 최대한 지키기

"클린한 코드는 나뿐만 아니라, 함께 일하는 동료들에게도 도움을 준다."

이번 과제는 순수 Vanilla JS로만 구현한다는 조건이 포함되어 있었기 때문에 JavaScript 코드 컨벤션을 지키는 사항이 요구조건에 포함되어 있었어요.

함께 링크된 Airbnb 자바스크립트 스타일 가이드를 쭉 읽으면서 생각보다 많이 제가 놓치고 있었던 컨벤션들을 찾아낼 수 있었습니다.


1. 네이밍 컨벤션

  • 소스의 변수명, 클래스명 등에는 영문 이외의 언어를 사용하지 않습니다.
  • 클래스, 메서드 등의 이름에는 특수 문자를 사용하지 않습니다.
// bad
funtion $some() {

}
  • 상수명은 SNAKE_CASE로 작성합니다.
// bad
const firefox = 1;
const is_left = true;

// good
const FIREFOX = 1;
const IS_LEFT = true;

// bad - unnecessarily uppercases key while adding no semantic value
export const MAPPING = {
  KEY: "value",
};

// good
export const MAPPING = {
  key: "value",
};

위 네이밍 컨벤션을 지키기 위해, 따로 상수들을 관리하는 constants 폴더를 만들었어요.
객체로 상수를 관리할 경우 key는 카멜케이스를 사용해야 했습니다.

// constants/conditions.js

const USER_COMMAND = Object.freeze({
  replay: 1,
  end: 2,
});

const GAME_CONDITION = Object.freeze({
  maxLength: 3,
  limitNumber: 0,
  startScope: 1,
  endScope: 9,
});

export { USER_COMMAND, GAME_CONDITION };

// models/InputValidator.js
const isValidLength = input.length === GAME_CONDITION.maxLength;

위와 같이 작성하니 나중에 해당 상수를 사용할때, 조금 더 읽기 편해졌습니다.


2. 명명 규칙

  • If the property/method is a boolean, use isVal() or hasVal().
// bad
if (!dragon.age()) {
  return false;
}

// good
if (!dragon.hasAge()) {
  return false;
}

boolean타입을 return하는 메서드들에는 앞에 ishas를 붙이는 것 만으로도 가독성 향상에 도움을 받았어요.

// models/InputValidator.js
  hasValidCommand(input) {
    const commandInput = parseInt(input, 10);

    return (commandInput === USER_COMMAND.replay) || (commandInput === USER_COMMAND.end);
  },

// views/InputView.js
 if (!InputValidator.hasValidCommand(userCommand)) throw new Error(MESSAGES.inputError);

  • It’s okay to create get() and set() functions, but be consistent.
// Airbnb 컨벤션 예시
class Jedi {
  constructor(options = {}) {
    const lightsaber = options.lightsaber || "blue";
    this.set("lightsaber", lightsaber);
  }

  set(key, val) {
    this[key] = val;
  }

  get(key) {
    return this[key];
  }
}

특정 값을 얻을 수 있는 메서드명에는 앞에 get을 붙여서 변수명을 확실하게 잡았어요.

  getComputerNumber() {
    return this.#computerNumber;
  }

3. 적절한 주석달기

위에 네이밍 컨벤션과 명명규칙들만 잘 적용해도 그 코드는 굳이 주석을 달지 않아도 이해하기 쉽다고 생각해요.

저는 최대한 주석 사용을 지양하는 편이지만, 이번 자바스크립트 컨벤션을 공부하면서
적절한 주석을 사용했을때의 이점을 알게 되었어요.

  • 문제에 대한 해결책을 주석으로 달기 위해 사용합니다 // TODO:.

개발을 진행하면서 추후 구현해야되는 부분의 주석에는 아래의 예시와 같이 TODO를 붙이는 것 만으로도 해당 주석이 무엇을 말하는지 한눈에 파악하기 쉬웠습니다.

class Calculator extends Abacus {
  constructor() {
    super();

    // TODO: total should be configurable by an options param
    this.total = 0;
  }
}

그리고 핵심 로직의 경우, 한눈에 의도를 파악하기 어려운 부분들에 return과 param의 타입을 추가하거나 요약하는 주석을 처리해주었어요.

  /**
   * 컴퓨터의 중복되지 않는 랜덤한 세자리 숫자를 만들어낸다.
   * @returns {number} 중복되지 않는 랜덤한 세자리 숫자
   */
  generateRandomNumbers() {
    const digitsArray = new Set();

    while (digitsArray.size < GAME_CONDITION.maxLength) {
      const randomNumber = Random.pickNumberInRange(GAME_CONDITION.startScope, GAME_CONDITION.endScope);
      digitsArray.add(randomNumber);
    }

    return parseInt([...digitsArray].join(''), 10);
  }

자바스크립트 코드 컨벤션은 이 외에도 많지만, 이번 과제를 진행하며 최대한 적용해보려고 리팩토링을 진행했습니다.

생각보다 코드를 수정하는 시간이 재미있었던 것 같아요.



💭 앞으로 보완하면 좋을 부분

1. 클래스와 객체 추가 학습

이번 과제를 진행하면서 클래스객체를 많이 사용했습니다.

그러다보니 특정한 기능을 구현해야할 때, 클래스로 만들지 객체로 만들지 많이 고민했던 것 같습니다.

사용자의 입출력을 담당하는 View같은 경우엔, 아래와 같이 객체로 만들었는데요.

const InputView = {
  async getUserNumber(message) {
    ...
  },

  async getUserCommand(message) {
    ...
  },
}

export default InputView;

아직까진 "이런 상황에선 클래스고 이런 상황에선 객체다!" 라는 완벽한 확신이 들지 않는 것 같아요.

그래서 추후 과제를 수행하면서 클래스와 객체에 대한 공부를 하며 적용해봐야 할 것 같습니다.


2. 시간 제어하기

한번 진행해봤던 과제였지만, 원하는 구조로 재설계하려다 보니 생각보다 시간이 오래 걸렸어요.

README를 너무 상세하게 작성하느라 그런감이 없지 않아 들어서,
다음번에는 핵심 요구 기능들만 축약해서 정리를 하고 빠르게 개발에 들어갈 수 있도록 해야겠다는 다짐을 했어요.

가장 좋은 방법으로는 4시간의 시간 제약을 걸어두고, 프로젝트를 처음부터 끝까지 구현해보면서 연습해봐야 할 것 같습니다.

profile
성장에는 성장통이 있기 마련이다.

0개의 댓글