[우아한테크코스] 프리코스 2주차 리뷰

호이초이·2024년 11월 15일
0
post-thumbnail

[2주차 깃허브 링크]

오늘은 2주차에 했던 과제에 대해서 리뷰를 해볼까한다.

1주차 공통 피드백

  • 요구 사항을 정확하게 준수한다
  • 기본적인 Git 명령어를 숙지한다
  • Git으로 관리할 자원을 고려한다
  • package-lock.json의 역할을 이해한다
  • 커밋 메시지를 의미 있게 작성한다 (링크)
  • 커밋 메시지에 이슈 또는 풀 리퀘스트 번호를 포함하지 않는다
  • 풀 리퀘스트를 만든 후에는 닫지 말고 추가 커밋을 한다
  • 오류를 찾을 때 출력 함수 대신 디버거를 사용한다
  • 이름을 통해 의도를 드러낸다
  • 축약하지 않는다
  • 공백도 코딩 컨벤션이다
  • 공백 라인을 의미 있게 사용한다
  • 스페이스와 탭을 혼용하지 않는다
  • 의미 없는 주석을 달지 않는다
  • 코드 포매팅을 사용한다
  • JavaScript에서 제공하는 API를 적극 활용한다

과제 설명

주제 : 초간단 자동차 경주게임을 구현한다.

  • 주어진 횟수 동안 n대의 자동차는 전진 또는 멈출 수 있다.
  • 각 자동차에 이름이 부여되며, 전진하는 자동차를 출력할 때 자동차 이름은 같이 출력한다.
  • 자동차 이름은 쉼표(,)를 기준으로 구분하며 이름은 5자 이하만 가능하다.
  • 사용자는 몇 번의 이동을 할 것인지를 입력할 수 있어야 한다.
  • 전진하는 조건은 0에서 9 사이에서 무작위 값을 구한 후 무작위 값이 4 이상일 경우다.
  • 자동차 경주 게임을 완료한 후 누가 우승했는지를 알려준다. 우승자는 한명 이상일 수 있다.
  • 우승자가 여러 명일 경우 쉼표(,)를 이용하여 구분한다.
  • 사용자가 잘못된 값을 입력할 경우 "[ERROR]"로 시작하는 메시지와 함께 Error를 발생시킨 후 애플리케이션은 종료되어야한다.
  • npm run start로 작업물 확인
  • npm run test로 jest를 돌려 test 코드 확인하기

프로그래밍 요구 사항

  • indent(인덴트, 들여쓰기) depth를 3이 넘지 않도록 구현한다. 2까지만 허용한다.
    ex) while문 안에 if문 -> 들여쓰기 2
  • 3항 연산자를 쓰지 않는다.
  • 기능 별 메소드 분리하기

폴더 구조

project-root
│
├── __tests__                # 테스트 폴더
│   └── ApplicationTest.js   # 애플리케이션 테스트 파일
│
├── src                      # 소스 코드 폴더
│   ├── App.js               # App 컴포넌트
│   └── index.js             # 메인 엔트리 파일
│
├── .gitignore               # Git 무시 파일
├── .npmrc                   # npm 설정 파일
├── package-lock.json        # npm 패키지 잠금 파일
├── package.json             # npm 패키지 설정 파일
└── README.md                # 프로젝트 설명 파일

1. README.md

구현할 기능 목록

  • 자동차 이름 문자열 입력
  • 몇 번 이동할지 횟수 입력
  • 자동차 이름 문자열을 쉼표(,)단위로 구분
    • 구분해서 각각의 자동차 별로 관리 (객체)
  • 입력한 이동횟수를 바탕으로 자동차 별 전진 값 구하기
    • n회 마다 -> 0-9까지 숫자들로 자동차들에게 전진 및 정지 부여
      • 0-9 숫자 중 4이상일 경우만 전진 (전진 및 정지 함수 따로 구현)
    • 1회가 끝날때마다 "이름 : -" 표시로 횟수 출력하기
  • 최종우승자 출력하기
    • "-" 횟수의 갯수를 바탕으로 최종우승 자동차 출력
      • "-"문자 repeat 문자열 메소드 이용
    • 갯수가 같을 경우 공동 우승자 처리
  • 에러처리
    • 자동차 입력
      • 자동차이름 5자 이상 입력시 에러 처리
      • 쉼표 없으면 에러차리 -> 1대도 가능
    • 이동횟수 입력
      • 숫자가 아닌 다른 값이 들어올 경우, 에러처리
      • 양의 정수만 입력 처리

테스트 코드 추가 작성

  • 자동차 이름수 5글자 제한
  • 이동 횟수 음수 처리
  • 이동 횟수 소수 처리
  • 이동 횟수 숫자 아닌 경우 처리
  • 빈문자열 처리

2주차까지는 파일을 분리하지 않고, App.js 파일에서만 구현을 했다. 이번 과제 역시, 기능이 단순하고 코드의 양이 적었기 때문에 단일 책임 원칙을 적용하는 것이 더 좋다고 판단했다. 그래서 하나의 클래스 안에서 기능별로 함수를 나누어 구현했다.

기능 구현

app.js 파일

  carObject = {};

 // 전진 or 멈춤 기준
  static MOVE_OR_STOP_FLAG = 4;

  static ERROR_MESSAGES = {
    EMPTY_STRING: "빈 문자열은 입력할 수 없습니다.",
    DUPLICATE_STRING: "중복된 자동차 이름은 입력할 수 없습니다.",
    MAX_STRING: "자동차이름은 5자 이하만 가능합니다.",
    INVALID_NUMBER: "양의 정수만 입력 가능합니다.",
  };

0.자동차 저장 객체 및 상수처리
carObject는 자동차 이름을 키(key)로, 해당 자동차의 이동 거리를 값(value)으로 저장하는 객체이다.
ex) { "car1": 2, "car2": 3 }
MOVE_OR_STOP_FLAG: 자동차가 이동할지 멈출지 결정하는 기준 값이다.(기본 4) 랜덤하게 생성된 숫자가 이 값보다 크거나 같으면 자동차가 전진한다.
ERROR_MESSAGES: 입력 검증 실패 시 출력할 다양한 에러 메시지를 정의한 객체이다.

1. run 메서드

async run() {
  const carInput = await Console.readLineAsync("경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분)\n");
  const numberInput = await Console.readLineAsync("시도할 횟수는 몇 회인가요?\n");

  this.validateCarInput(carInput);
  this.initializeCars(carInput);

  this.validateNumberInput(numberInput);
  this.racingRound(numberInput);
  this.selectWinner();
}
  • 역할:
    프로그램의 시작 메서드로, 사용자로부터 자동차 이름과 경주 횟수를 입력받는다.
  • 동작:
    - 입력값을 검증하기 위해 validateCarInput과 validateNumberInput을 호출한다.
    - 자동차 이름을 초기화하고, 입력된 횟수만큼 경주 라운드를 진행한다.
    - 최종 우승자를 출력한다.

2. initializeCars 메서드

initializeCars(carInput) {
  const carNames = carInput.split(",").map((name) => name.trim());

  carNames.forEach((carName) => {
    this.carObject[carName] = 0;
  });
}
  • 역할:
    자동차 이름 목록을 carObject 객체에 등록하고 초기 거리값을 0으로 설정한다.
  • 동작:
    carInput 문자열을 쉼표로 구분하여 각 자동차 이름을 분리하고, carObject에 초기값을 설정한다.

3. racingRound 메서드

racingRound(numberInput) {
  Console.print(`\n실행 결과`);
  for (let i = 0; i < numberInput; i++) {
    this.simulateRaceRound();
    this.printRaceRound();
  }
}
  • 역할:
    입력된 횟수만큼 경주 라운드를 진행한다.
  • 동작:
    - simulateRaceRound 메서드로 각 라운드의 자동차 이동을 처리한다.
    - printRaceRound 메서드로 라운드 결과를 출력한다.

4. simulateRaceRound 메서드

simulateRaceRound() {
  Object.keys(this.carObject).forEach((carName) => {
    if (this.moveOrStop()) this.carObject[carName]++;
  });
}
  • 역할:
    각 라운드에서 자동차의 이동 여부를 결정하고, 이동하면 거리를 증가시킨다.
  • 동작:
    moveOrStop 메서드를 사용해 이동 여부를 결정한다. 조건이 충족되면 carObject의 거리를 1 증가시킨다.

5. moveOrStop 메서드

moveOrStop() {
  return Random.pickNumberInRange(0, 9) >= App.MOVE_OR_STOP_FLAG;
}
  • 역할:
    - 자동차가 이동할지 멈출지 결정한다.
  • 동작:
    0부터 9 사이의 랜덤 숫자를 생성하여 MOVE_OR_STOP_FLAG보다 크거나 같으면 이동하도록 한다.

6. printRaceRound 메서드


printRaceRound() {
  Object.entries(this.carObject).forEach(([carName, distance]) => {
    Console.print(`${carName} : ${"-".repeat(distance)}`);
  });
  Console.print("\n");
}
  • 역할:
    - 현재 라운드에서 각 자동차의 이동 거리를 출력한다.
  • 동작:
    - 각 자동차 이름과 이동 거리를 화면에 출력하며, 거리는 - 기호로 표시된다.

7. selectWinner 메서드

selectWinner() {
  const maxDistance = Math.max(...Object.values(this.carObject));
  const winners = Object.keys(this.carObject).filter(
    (carName) => this.carObject[carName] === maxDistance
  );
  Console.print(`최종 우승자 : ${winners.join(", ")}`);
}
  • 역할:
    - 최종 우승자를 결정하고 출력한다.
  • 동작:
    - carObject에서 가장 먼 거리를 달린 자동차를 찾고, 해당 거리를 달린 자동차들을 winners 배열에 저장하여 출력한다.

8. validateCarInput 메서드

validateCarInput(carInput) {
  if (!carInput.trim()) this.throwError(App.ERROR_MESSAGES.EMPTY_STRING);
  if (carInput.split(",").some((carName) => carName.trim().length > 5))
    this.throwError(App.ERROR_MESSAGES.MAX_STRING);
  if (new Set(carInput.split(",")).size !== carInput.split(",").length) {
    this.throwError(App.ERROR_MESSAGES.DUPLICATE_STRING);
  }
}
  • 역할:
    자동차 이름 입력값을 검증하여 규칙에 맞지 않으면 에러를 발생시킨다.
  • 동작:
    빈 문자열 검사, 5자 이하 제한 검사, 중복 이름 검사를 수행하고 규칙에 맞지 않으면 에러를 발생시킨다.

9. validateNumberInput 메서드

validateNumberInput(numberInput) {
  if (!numberInput || !numberInput.trim())
    this.throwError(App.ERROR_MESSAGES.EMPTY_STRING);
  if (!/^[1-9]\d*$/.test(numberInput))
    this.throwError(App.ERROR_MESSAGES.INVALID_NUMBER);
}
  • 역할:
    - 시도 횟수 입력값을 검증하여 규칙에 맞지 않으면 에러를 발생시킨다.
  • 동작:
    - 빈 입력값 검사, 양의 정수 검사(1 이상의 정수만 허용)를 수행하고 규칙에 맞지 않으면 에러를 발생시킨다.

10. throwError 메서드

throwError(message) {
	throw new Error(`[ERROR] ${message}`);
}
  • 역할:
    - 에러 발생 시 공통된 형식으로 메시지를 출력하고 예외를 던진다.
  • 동작:
    - message를 받아 [ERROR]와 함께 에러를 발생시킨다.

결과

  1. npm test 명령어로 테스트 코드 실행

//ApplicationTest.js
test.each([
    { inputs: ["pobi,javaji"], description: "자동차 이름이 5자 초과" },
    { inputs: ["pobi,woni", "-1"], description: "이동 횟수가 음수" },
    { inputs: ["pobi,woni", "2.5"], description: "이동 횟수가 소수" },
    { inputs: ["pobi,woni", "abc"], description: "이동 횟수 숫자 x" },
    { inputs: [""], description: "빈 문자열 입력" },
  ])("예외 테스트 - $description", async ({ inputs }) => {
    // given
    mockQuestions(inputs);

    // when
    const app = new App();

    // then
    await expect(app.run()).rejects.toThrow("[ERROR]");
  });

우테코에서 주어진 기본 테스트말고, 내가 테스트하고 싶은 예외처리들을 테스트케이스로 추가 구현해봤다.

  1. npm start 명령어로 직접 프로그램 실행

이슈사항

처음 입력받은 횟수로 랜덤값 돌리는 함수를 아래처럼 구현했었다.


  //numberInput 바탕으로 랜덤값 돌리는 함수
  getRandomNumber(numberInput) {
    for (let i = 0; i < numberInput; i++) {
      for (let j = 0; j < this.carObject.length; j++) {
        this.carObject[carObject[j]] += this.moveOrStop(
          Random.pickNumberInRange(0, 9)
        );
      }
      Console.print(this.carObject);
    }
  }

이렇게 했는데, 값이 반영이 안됐다.

문제는 this.carObject의 키 값에 접근할 때 발생하는 것이었다. carObject.length는 undefined를 반환하기 때문에 반복문에서 for (let j = 0; j < this.carObject.length; j++)가 정상적으로 동작하지 않는 것.. (너무 기본적이면서 바보같은 실수였다.)
객체는 배열처럼 .length 속성을 가지고 있지 않으므로 Object.keys() 또는 for...in을 사용해야 반복문을 사용할 수 있다.

 Object.keys(this.carObject).forEach((carName) => {
    this.carObject[carName] += this.moveOrStop(
      Random.pickNumberInRange(0, 9)
    );

마무리

프리코스를 통해 단순히 정답을 찾는 것에 그치지 않고, 문제를 해결해나가는 과정을 즐기며 끝까지 완주하는 것을 목표로 삼았습니다. 또한, 문제를 분석하고 더 나은 코드를 작성하기 위해 끊임없이 고민해 보고자 했습니다. 현재 2주차를 진행한 시점에서, 처음 목표했던 대로 꾸준히 도전하고 있다고 생각합니다. 문제를 해결할 때 빠른 정답보다는 하루하루 고민하고 탐구하는 과정이 큰 도움이 되고 있으며, 지속적인 리팩토링과 코드 개선을 위해 노력하는 저 자신이 대견스럽게 느껴지기도 합니다. 이 과정에서 발생한 문제나 오류 상황들을 블로그에 정리하고 복습하는 시간 역시 큰 도움이 되고 있습니다. 또한 앞으로 목표를 하나 더 추가하고자 결심했습니다. 프리코스를 통해 jest 테스트나 우아한 테크코스에서 제공하는 라이브러리들, 자바스크립트의 클래스화 등 처음 접하는 개념들을 습득하는 기회를 얻으며 이러한 부분을 더 깊이 이해하고 제 지식으로 완전히 채우고자 하는 목표가 생겼습니다. 프리코스는 단순한 과제를 넘어 제 성장에 중요한 기회를 제공하고 있음을 몸소 느끼게 되었습니다.

2주차 과제에서는 1주차 공통 피드백을 적극 반영하려고 노력했습니다. 우선 커밋 메시지를 최대한 의미 있게 작성하고자 했으며, 작업을 마친 후 한 번에 Pull Request를 올리기보다는 PR을 올려두고, 필요한 부분을 계속 수정하는 방식으로 접근했습니다. 함수명이나 변수명 역시 축약하지 않고 최대한 의미를 담아 작성하려고 했습니다.
추가로 제공된 문자열 덧셈기 피드백 영상을 참고하여, 기능별로 필요한 부분들을 우선 주석으로 적어놓고, 이를 기반으로 기능별 메소드 구현에 집중하려 노력했습니다. 기본 기능이 잘 작동하도록 하는 데 최선을 다했으며, 발생할 수 있는 예외 상황을 고려하여 테스트 코드를 추가로 작성했고, 거기에 맞춰 에러 처리를 구현해보았습니다.

3주차 과제에서도 많은 고민을 거쳐 진행할 계획이며, 기능 명세서도 꼼꼼히 읽고 이해하여 각 요구사항을 충실히 반영할 것입니다. 또한, 여러 새로운 지식들을 적극적으로 습득해 제 것으로 만들어가고 싶습니다.

profile
칼을 뽑았으면 무라도 썰자! (근데 아직 칼 안뽑음)

0개의 댓글