[우아한테크코스] 프리코스 2주차 회고록

Jerry·2023년 11월 2일

우아한테크코스

목록 보기
2/3
post-thumbnail

1주 차 공통 피드백


1주 차 공통 피드백을 살펴보면서 잘 지켜진 부분도 있고 잘 지켜지지 않은 부분도 있었다.
그 중 기억에 남는 것 몇 개를 적어보자.

요구사항을 정확히 준수한다

1주 차 회고록에서도 작성했지만 가장 기본을 지키지 않으면 아무리 완벽한 코드를 작성한다 한들 의미가 없다.
결국은 요구사항을 준수하는 돌아가는 코드를 작성해야 한다.

linter와 Code Formatter의 기능을 활용한다

활용해보니 활용하지 아니할 이유가 없다.



2주 차 목표


함수 분리

함수가 한 가지 일만 하게 하기 위해 노력했다.

tryToMoveForward() {
  if (this.#isQualifiedForMovingForward()) this.#moveForward();
}

#isQualifiedForMovingForward() {
  const randomNumber = Random.pickNumberInRange(
    this.#START_INCLUSIVE,
    this.#END_INCLUSIVE,
  );
  return this.#STANDARD_NUMBER <= randomNumber;
}

#moveForward() {
  this.#moveCount += 1;
}

처음에는 위 함수 3개가 하나의 함수에 작성되어 있었다.
커밋메시지를 보자.


함수별 테스트 작성

import ERROR_MESSAGE from '../src/ErrorMessage.js';
import Validator from '../src/Validator.js';

describe('ValidatorTest', () => {
  test('자동차 이름이 빈 값일 경우 EMPTY 에러 발생', () => {
    expect(() => Validator.validateCarNames([''])).toThrow(ERROR_MESSAGE.empty);
  });
  test('자동차 이름이 빈 값이 아닐 경우 에러 발생하지 않음', () => {
    expect(() => Validator.validateCarNames(['jerry'])).not.toThrow();
    expect(() => Validator.validateCarNames(['jerry', 'tom'])).not.toThrow();
  });

  test('자동차 이름이 6자 이상일 경우 MAX_LENGTH 에러 발생', () => {
    expect(() => Validator.validateCarNames(['abcdef'])).toThrow(
      ERROR_MESSAGE.length,
    );
    expect(() => Validator.validateCarNames(['abc', 'abcdef'])).toThrow(
      ERROR_MESSAGE.length,
    );
  });
  test('자동차 이름이 5자 이하일 경우 에러 발생하지 않음', () => {
    expect(() => Validator.validateCarNames(['abcde'])).not.toThrow();
    expect(() => Validator.validateCarNames(['tom', 'jerry'])).not.toThrow();
  });

  test('시도할 횟수가 1에서 100 사이의 숫자가 아닌 경우 ONE_TO_HUNDRED 에러 발생', () => {
    expect(() => Validator.validateNumberOfRound(NaN)).toThrow(
      ERROR_MESSAGE.oneToHundred,
    );
    expect(() => Validator.validateNumberOfRound(0)).toThrow(
      ERROR_MESSAGE.oneToHundred,
    );
  });
  test('시도할 횟수가 1에서 100 사이의 숫자인 경우 에러 발생하지 않음', () => {
    expect(() => Validator.validateNumberOfRound(1)).not.toThrow();
    expect(() => Validator.validateNumberOfRound(100)).not.toThrow();
    expect(() => Validator.validateNumberOfRound(40)).not.toThrow();
  });
});

보면 Validator.validateCarNames()Validator.validateNumberOfRound() 함수만 호출하고 있는데 에러메세지는 다양하게 뜬다.

사용자에게는 에러가 발생하면 어떤 부분이 잘못됐는지 자세히 말해줘야 한다고 생각했고 다른 객체에서 호출할 때는 단순하게 호출하는 게 맞다고 생각했다.

선언 부분은 이렇다.

import ERROR_MESSAGE from './ErrorMessage.js';

class Validator {
  static #MIN_LENGTH = 1;
  static #MAX_LENGTH = 5;

  static #START_INCLUSIVE = 1;
  static #END_INCLUSIVE = 100;

  static validateCarNames(array) {
    if (Validator.#isEmpty(array[0])) throw new Error(ERROR_MESSAGE.empty);
    if (!array.every((string) => Validator.#isLength(string))) {
      throw new Error(ERROR_MESSAGE.length);
    }
  }

  static validateNumberOfRound(value) {
    if (!Validator.#isBetween(value)) {
      throw new Error(ERROR_MESSAGE.oneToHundred);
    }
  }

  static #isEmpty(value) {
    return value === '';
  }

  static #isLength(
    string,
    min = Validator.#MIN_LENGTH,
    max = Validator.#MAX_LENGTH,
  ) {
    return min <= string.length && string.length <= max;
  }

  static #isBetween(
    value,
    startInclusive = Validator.#START_INCLUSIVE,
    endInclusive = Validator.#END_INCLUSIVE,
  ) {
    return startInclusive <= value && value <= endInclusive;
  }
}

export default Validator;

테스트에서 #isEmpty(), #isLength(), #isBetween() 함수를 모두 확인할 수 있게 작성했다.

테스트를 작성하는 이유

모든 기능 구현을 마친 뒤 적는 테스트 코드는 숙제처럼 느껴진다. 하지만 기능 구현과 함께 테스트를 작성하면 그 이점을 확실히 느낄 수 있다.

귀찮음⬇️

일단 코드를 실행시켜야 하는 귀찮음이 줄어든다.
node src/index.js -> 자동차 수 입력 -> 시도할 횟수 입력 -> 결과 확인

이 과정을 npm test 한 번으로 단축시킬 수 있다.
지금은 저 과정이 귀찮지 않을 수 있지만 더 커다란 프로젝트에서는 어떨까?

리팩터링⬆️

커밋 메시지를 보면 많은 리팩터링이 눈에 띤다.
테스트 코드를 작성하기 이전에는 코드의 리팩터링이 필요해 보여도 수정하다가 코드가 망가질까 망설였다.
테스트 코드로 인한 빠른 피드백은 어느 부분이 테스트에 실패했는지 바로바로 확인할 수 있어서 리팩터링에 대한 부담은 줄어들고 깔끔해지는 코드를 보는 만족감은 올라간다.



2주 차 미션을 하면서 시도해 본


Static private fields and methods

언제 이걸 써보나 했는데 처음으로 이번 주차 때 Console 클래스의 필드에 적용해봤다.
이곳에 쓰는 게 적절한지는 솔직히 잘 모르겠는데 이번 주차에서부터 eslint를 사용하면 this를 쓰지 않는 메서드는 static method를 쓰라고 class-methods-use-this 경고가 떠서 왜 그래야 할까 생각해보는 시간을 가졌었다.
static을 쓰지 않고 그냥 인스턴스 메서드로 만들어도 사용할 수 있는데 왜 굳이 static을 쓰는 걸까? 검색해보니 static을 쓰는 것이 객체지향에 어긋난다는 말도 있었다.
하지만 입력 받는 메서드는 애초에 상태를 변화시킬 게 없지 않나 생각하면 static 메서드를 써도 괜찮지 않을까.
이번 미션의 목표는 static이 아니라 함수를 분리하고 각 함수별로 테스트를 작성하는 것이기 때문에 일단 사용해 보고 싶던 static private fields을 썼다.

import { Console } from '@woowacourse/mission-utils';
import Validator from './Validator.js';

class View {
  static #DELIMITER = ',';
  static #CAR_NAMES_QUERY = `경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분) `;
  static #NUMBER_OF_ROUNDS_QUERY = '시도할 횟수는 몇 회인가요? ';
  static #TOTAL_RESULTS_HEAD = '실행 결과';
  static #FORWARD_MARK = '-';
  static #WINNERS_HEAD = '최종 우승자';

  static async askCarNames() {
    const answer = await Console.readLineAsync(View.#CAR_NAMES_QUERY);
    const stringWithoutSpaces = answer.replace(/ /g, '');
    const array = stringWithoutSpaces.split(View.#DELIMITER);
    Validator.validateCarNames(array);
    return array;
  }

  static async askNumberOfRounds() {
    const answer = await Console.readLineAsync(View.#NUMBER_OF_ROUNDS_QUERY);
    const number = Number(answer);
    Validator.validateNumberOfRound(number);
    return number;
  }

  static writeTotalResultsMessage(totalResults) {
    let message = `\n${View.#TOTAL_RESULTS_HEAD}\n`;
    totalResults.forEach((map) => {
      message += this.writeRoundResult(map);
    });
    return message;
  }

  static writeRoundResult(map) {
    let message = '';
    map.forEach((moveCount, name) => {
      message += `${name} : ${View.#FORWARD_MARK.repeat(moveCount)}\n`;
    });
    return `${message}\n`;
  }

  static printTotalResults(totalResults) {
    const message = View.writeTotalResultsMessage(totalResults);
    Console.print(message);
  }

  static writeWinnersMessage(winners) {
    return `${View.#WINNERS_HEAD} : ${winners.join(`${View.#DELIMITER} `)}`;
  }

  static printWinners(winners) {
    const message = View.writeWinnersMessage(winners);
    Console.print(message);
  }
}

export default View;

상수를 하나의 파일에 모아두는 게 맞을까

이번 미션에서는 에러 메시지의 경우에는 따로 파일을 만들었고 한 파일에서만 사용되는 문자열, 숫자 같은 경우에는 해당 파일에 private으로 작성했다.

const ERROR_MESSAGE = {
  empty: '[ERROR] 빈 값입니다.',
  length: '[ERROR] 각 이름은 5자 이하로 입력하세요.',
  oneToHundred: '[ERROR] 1에서 100 사이의 숫자를 입력하세요.',
};
Object.freeze(ERROR_MESSAGE);

export default ERROR_MESSAGE;

💡 TIP
Object.freeze() 메서드는 객체를 동결합니다.

class Validator {
  static #MIN_LENGTH = 1;
  static #MAX_LENGTH = 5;

  static #START_INCLUSIVE = 1;
  static #END_INCLUSIVE = 100;
class RacingCar {
  #START_INCLUSIVE = 0;
  #END_INCLUSIVE = 9;
  #STANDARD_NUMBER = 4;
class View {
  static #DELIMITER = ',';
  static #CAR_NAMES_QUERY = `경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분) `;
  static #NUMBER_OF_ROUNDS_QUERY = '시도할 횟수는 몇 회인가요? ';
  static #TOTAL_RESULTS_HEAD = '실행 결과';
  static #FORWARD_MARK = '-';
  static #WINNERS_HEAD = '최종 우승자';

Map 객체

Map과 Set 객체를 알게 된 이후 Set 객체는 중복 요소를 제거할 때 자주 사용하는데 Map 객체는 사용할 일이 없었는데 이번에 사용해봤다!
Map.forEach()는 일반 배열에서 사용되는 forEach()와 매개변수가 다르다.

recordNumberOfRound(number) {
  this.#totalResults = Array.from({ length: number }, () => new Map());
}
static writeRoundResult(map) {
  let message = '';
  map.forEach((moveCount, name) => {
    message += `${name} : ${View.#FORWARD_MARK.repeat(moveCount)}\n`;
  });
  return `${message}\n`;
}


추가적으로 생각해 볼


index.js 파일

1주 차에는 index.js 파일이 없었는데 생겨있었다.

private 메서드는 테스트를 어떻게 해야 할까



코드 리뷰


2주 차 미션을 제출하고 새벽에 코드 리뷰를 하는 시간을 가졌다. 장장 3시간에 걸친 코드 리뷰를 마치고 지친 상태로 자려는데 내 PR에도 리뷰를 달아주신 분들이 계셔서 확인했다.

신경 쓴 부분들에 칭찬을 받는 일이 이렇게 기분 좋은 일일 줄이야🥹

나는 코드 리뷰 쓸 때 잘 쓴 코드를 보고 감탄만 하고 넘어가고 아쉬운 부분에만 리뷰를 남겼는데 앞으로는 칭찬하는 코드도 남겨야 겠다고 다짐하며 행복하게 침대에 누웠다.



혹시 피드백 해주실 사항이 있다면 꼭 남겨주세요 :)
아주 사소한 것이라 해도 상관 없습니다!

PR 링크
https://github.com/woowacourse-precourse/javascript-racingcar-6/pull/616

profile
I'm jerry

0개의 댓글