오늘은 2주차에 했던 과제에 대해서 리뷰를 해볼까한다.
- 요구 사항을 정확하게 준수한다
- 기본적인 Git 명령어를 숙지한다
- Git으로 관리할 자원을 고려한다
- package-lock.json의 역할을 이해한다
- 커밋 메시지를 의미 있게 작성한다 (링크)
- 커밋 메시지에 이슈 또는 풀 리퀘스트 번호를 포함하지 않는다
- 풀 리퀘스트를 만든 후에는 닫지 말고 추가 커밋을 한다
- 오류를 찾을 때 출력 함수 대신 디버거를 사용한다
- 이름을 통해 의도를 드러낸다
- 축약하지 않는다
- 공백도 코딩 컨벤션이다
- 공백 라인을 의미 있게 사용한다
- 스페이스와 탭을 혼용하지 않는다
- 의미 없는 주석을 달지 않는다
- 코드 포매팅을 사용한다
- JavaScript에서 제공하는 API를 적극 활용한다
주제 : 초간단 자동차 경주게임을 구현한다.
Error
를 발생시킨 후 애플리케이션은 종료되어야한다.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
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();
}
2. initializeCars
메서드
initializeCars(carInput) {
const carNames = carInput.split(",").map((name) => name.trim());
carNames.forEach((carName) => {
this.carObject[carName] = 0;
});
}
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;
}
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(", ")}`);
}
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);
}
}
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);
}
10. throwError 메서드
throwError(message) {
throw new Error(`[ERROR] ${message}`);
}
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]");
});
우테코에서 주어진 기본 테스트말고, 내가 테스트하고 싶은 예외처리들을 테스트케이스로 추가 구현해봤다.
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주차 과제에서도 많은 고민을 거쳐 진행할 계획이며, 기능 명세서도 꼼꼼히 읽고 이해하여 각 요구사항을 충실히 반영할 것입니다. 또한, 여러 새로운 지식들을 적극적으로 습득해 제 것으로 만들어가고 싶습니다.