초간단 자동차 경주 게임을 구현한다.
주어진 횟수 동안 n대의 자동차는 전진 또는 멈출 수 있다.
각 자동차에 이름을 부여할 수 있다. 전진하는 자동차를 출력할 때 자동차 이름을 같이 출력한다.
자동차 이름은 쉼표(,)를 기준으로 구분하며 이름은 5자 이하만 가능하다.
사용자는 몇 번의 이동을 할 것인지 입력할 수 있어야 한다.
전진하는 조건은 0에서 9 사이에서 무작위 값을 구한 후 무작위 값이 4 이상일 경우이 다.
자동차 경주 게임을 완료한 후 누가 우승 했는지를 알려준다. 우승자는 한 명 이상일 수 있다.
우승자가 여러 명일 경우 쉼표(,)를 이용하여 구분한다.
사용자가 잘못된 값을 입력할 경우, ERROR로 시작하는 에러를 발생시킨 후 어플리케이션은 종료되어야 한다.
입력
pobi,woni,jun
5
출력
pobi : -
woni : ---
jun : ---
최종 우승자 : pobi
최종 우승자 : pobi, jun
경주할 자동차 이름을 입력하세요.( 이름은 쉼표 (,) 기준으로 구분)
pobi,woni,jun
시도할 횟수는 몇 회인가요?
5
실행 결과
pobi : -
woni :
jun : -
pobi : -
woni :
jun : --
pobi : --
woni : -
jun : ---
pobi : ---
woni : --
jun : ----
pobi : ----
woni : ---
jun : -----
최종 우승자 : pobi, jun
1주차보다 난이도가 확실히 내려간 느낌이 들었다.
정확하게 말하면 구현보다는 구조에 초점을 맞춘 것 같았다.
1주차에 코드리뷰에서 app.js에 모든 코드를 넣었는데 가독성이 좋지 않다는 피드백을 받았다. 그래서 이번엔 입출력과 핵심 기능을 하는 부분을 나누어 구현하는 것에 초점을 맞추었다.
크게 이 세 가지로 나누어 구현했다. 폴더 구조는 각 입출력, 유효성검사, 핵심 로직을 따로 분리했다.
constraints
: 에러 메세지
getInput
: 입력 메세지
output
: 출력 메세지
validations
: 입력 유효성 검사
raceLogin
: 자동차 경주 실행 로직
우테코 프리코스를 하다 보니 느낀 것은 입출력 유효성 검사에 매우 잘 알게 된다는 것이다. 알고리즘 풀 때 입력이 잘못되면 알아서 에러를 띄워 주었는데, 여기선 내가 직접 에러를 만들고 에러가 발생하는 조건까지 만들어야 한다.
자동차경주에서 시도횟수를 예를 들어보자. 기능 요구사항에는 시도횟수에 대한 요구 사항이 기재되어 있지 않았다. 스스로 구현해야 했다. 내가 스스로 생각한 입력조건은 이렇다.
시도횟수가 0회일 때 자동차 경주를 하지 않았다고 판단했기에 시도횟수가 1이어야 한다는 조건을 추가했다.
입력을 숫자로 한정하기 위해 자바스크립트에서 쓰는 isNaN을 사용했다.
둘 다 NaN
인지 판별할 때 사용된다. 하지만 차이점도 있다.
isNaN
NaN
이거나, 값을 숫자로 변환했을 때 NaN
이면 true
Number.isNaN
NaN
이어야만 true
예시
isNaN(null); // false
Number.isNaN(null); // false
isNaN('25'); // false // "25"는 숫자 25로 변환된다.
isNaN('25.25'); // false // "25.25"는 숫자 25.25로 변환된다.
Number.isNaN(NaN); // true
Number.isNaN(Number.NaN); // true
Number.isNaN({}); // false
Number.isNaN('hello'); // false
Number.isNaN(NaN); // true
isNaN({}); // true
isNaN('hello'); // true
isNaN(NaN); // true
나는 Number.isNaN을 사용했는데 나중에 보니 잘못 사용했다는 것을 깨달았다.
빈 문자열 검사 -> Number.isNaN -> 정수인지 검사 -> 양수인지 검사
순서로 시도횟수 유효성 검사를 했는데
export function tryCountNumberCheck(tryCount) {
if (Number.isNaN(tryCount)) {
throw new Error(ERROR_MESSAGES.NOT_NUMBER);
}
}
Number.isNaN은 입력이 NaN인 경우만 true를 반환하므로, 나머지는 에러를 발생시키지 않고 넘어간다. 그런 다음 정수인지 검사하는데
export function tryCountIntegerCheck(tryCount) {
const parseToFloat = parseFloat(tryCount);
if (!Number.isInteger(parseToFloat)) {
throw new Error(ERROR_MESSAGES.NOT_INTEGER);
}
소수인 경우에만 변환하고 나머지는 NaN을 반환한 다음, 변환된 결과가 정수로 반환 가능하지 않다면 에러를 던진다.
에러를 잘게 쪼개어 놔서 가독성이 올라가긴 하지만, 굳이 쓸 필요없는 함수들을 작성하여 유효성 검사의 깊이가 깊어졌다. 또한 Number.isNaN은 NaN만 검사하므로 불필요한 기능이 추가되었다.
정규식을 사용하면 숫자외의 것들과 소수점이 붙은 숫자 또한 걸러낼 수 있다.
static validateIsOnlyDigits(value) {
if (!/^\d+$/.test(value)) {
throw new Error(ERROR_MESSAGES.INVALID_INPUT);
}
}
에러 단위는 하나로 줄어서 어떤 에러가 났는지 알 수 없지만, 모든 에러가 하나의 코드로 처리된다.
에러 단위를 잘게 쪼개어 어떤 에러인지 알려주는게 좋을까 아니면 모든 에러를 처리하는 간단한 코드로 줄였지만 어떤 에러인지 모르는 게 좋을까?
미션이 아니라 프로젝트 개발이라고 생각해보자. 이 프로젝트에는 로그인 기능이 있고 사용자는 로그인을 하고 있다. 비밀번호를 잘못 쳤다.
1 : 잘못된 비밀번호입니다.
2 : 비밀번호는 대소문자와 숫자로만 구성되어야 합니다.
사용자는 어떤 걸 보고 더 편리함을 느낄까? 아마 2번일 것이다. 에러단위를 잘게 쪼개는 것도 개발할 때 중요한 덕목같다.
배열로 전진횟수 관리
자동차는 랜덤횟수가 4이상일 때 전진한다.
전진을 의미하는 -
는 출력만 하면 되고, 횟수는 자동차 수만큼의 배열을 생성해 해당 인덱스의 자동차가 전진하는 경우 value를 1 증가시킨다.
export function moveCarForward(randomNumber, moveCountArray, index) {
if (randomNumber >= 4) {
moveCountArray[index] += 1;
}
}
실행마다 전진 출력
자동차 경주가 1회 실행될 때마다 각 자동차의 전진 상태를 출력해야 한다.
전진횟수는 moveCountArray
에 저장되어 있다. 해당 배열을 순회하며 value x '-'
만큼 출력하면 된다.
import { Console } from '@woowacourse/mission-utils';
export function printRaceStatus(carName, moveCount) {
Console.print(`${carName} : ${'-'.repeat(moveCount)}`);
}
repeat
은 문자열을 반복할 때 사용하는 메서드다.
// 예시
for (let i=0; i<5; i++) {
console.log('*'.repeat(i+1));
}
// *
// **
// ***
// ****
// *****
실제 출력은 이렇다.
// 1회 실행했을 때
pobi :
wonu : -
jun : -
레이스 실행하기
import { generateRandomNumber } from './generateRandomNumber.js';
import { moveCarForward } from './moveCarForward.js';
import { printRaceStatus } from './printRaceStatus.js';
export function runRaceRound(carNames, moveCountArray) {
carNames.forEach((_, index) => {
const RANDOM_NUMBER = generateRandomNumber();
moveCarForward(RANDOM_NUMBER, moveCountArray, index);
printRaceStatus(carNames[index], moveCountArray[index]);
});
}
자동차 경주를 한 번 실행했을 때의 동작이다.
랜덤 함수 검증 -> 전진 -> 전진 상태 출력
메인 로직
startRace(carNames, tryCount) {
const MOVE_FORWARD_COUNT_ARRAY = new Array(carNames.length).fill(0);
Console.print('\n실행 결과');
Array.from({ length: tryCount }).forEach(() => {
runRaceRound(carNames, MOVE_FORWARD_COUNT_ARRAY);
Console.print('');
});
return MOVE_FORWARD_COUNT_ARRAY;
}
유효성 검사가 끝난 carNames, tryCount
를 매개변수로 하여 carNames
길이의 값이 0인 배열을 생성한다. 그리고 tryCount
만큼 자동차 경주를 실행한다.
처음에 구조를 잘 짜보자고 시작했던 포부와 다르게 아쉬움이 많이 남았다. 입력 에러를 처리하는 것에서 시간을 많이 썼고, 핵심 로직을 깔끔하게 구현하지 못했다고 생각한다.
나는 자동차 이름과 전진횟수를 각각의 다른 배열로 관리하여, 인덱스로 우승자를 찾았다. 아래가 전진횟수를 관리하는 배열이다.
// carNames 길이 만큼 값이 0인 배열을 생성
const MOVE_FORWARD_COUNT_ARRAY = new Array(carNames.length).fill(0);
두 배열이 독립적으로 동작하기 때문에 하나의 배열을 수정하면 데이터 불일치 문제가 발생한다. 또한 전진 횟수를 구하기 위해 자동차이름의 인덱스를 찾고, 전진횟수에서 다시 값을 가져오는 과정 자체가 복잡하다.
// 조회
const moves = carNames.get("CarB");
// 갱신
carData.set("CarA", carData.get("CarA") + 1);
map
으로 관리하면 수정되어도 데이터 불일치가 없고, 자동차 이름만 알면 전진횟수를 바로 구할 수 있어 코드가 간결해진다.
확신
2주차 프리코스를 하면서 느낀 것 : 우물 안의 개구리
디스코드 커뮤니티에선 MVC 패턴, TDD 같은 용어들이 튀어나왔다. (아무것도 모름.) 폴더 구조와 기능을 나누는 데도 급급한 2주차였는데 내가 저걸 할 수 있을까 하는 생각이 들었다.
내가 내린 결론은 내가 할 수 있는 것을 하자 였다. 구현도 제대로 못하는데 저런 용어들에 현혹될 수 없었다. 차차 배워가면서 나중에 자연스레 알게 되겠지 하는 마음으로 임했다. 무엇보다 벌써 저걸 잘 하면 취업을 하는게 낫지 않나라는 포비님의 말에 조금 안심이 되었다.
성장
원래 나는 내 코드를 다른 사람에게 보여주기 부끄러워하는 사람이었다. 코드 리뷰는 그걸 단번에 깨 부수어 주었다. 모르는 건 찾아 보게 되고 궁금한 건 편하게 물어볼 수 있는 코드 리뷰 기간은 개발을 시작한 지 얼마 안 된 나에게 최고의 시간이었다. 동시에 나도 나중에 질 좋은 리뷰를 마구마구 써 줄 수 있는 개발자가 되고 싶다는 생각도 들었다. 이상으로 많은 것을 느꼈던 2주차 프리코스였다.