🎯 주제
2주차 미션은 자동차 경주 미션이었다. 1주차 미션에서 배운 내용과 코드리뷰를 토대로 이번 미션에 적용하려고하니 기대가 샘솟았다. 요구사항을 살펴본 바로는 Jest를 활용해 테스트 코드를 작성해야 할 것 같다. 이번에는 테스트 코드를 어떻게 작성해야 하는지에 대해 더욱 심층적으로 학습하고 적용해보려 한다.
👨🏻💻 공들인 부분
📄 기능 목록 작성하기
저번 미션을 통해 기능 목록의 중요성을 인지하게 되면서 이번 미션에서도 구현을 시작하기 전에 기능 목록을 작성하였다. 저번 미션에서처럼 입력, 출력, 핵심 기능을 기준으로 나누었고 추가로 입력에 대한 예외 상황도 고려하여 작성하였다.
# 기능 목록
- 입력 기능
[ ] 자동차 이름을 입력받는 기능
[ ] 시도할 횟수를 입력받는 기능
- 출력 기능
[ ] 사용자가 입력한 횟수만큼 자동차의 전진상태를 출력하는 기능
[ ] 최종 우승자를 출력하는 기능
- 핵심 기능
[ ] 각 자동차를 전진 혹은 정지시키는 기능
[ ] 최종 우승자를 가려내는 기능
# 예외 상황
- 자동차 이름 입력 예외 상황
[ ] 입력한 자동차 이름이 5자 이하가 아닐 경우
[ ] 맨 앞과 마지막이 쉼표로 끝 날 경우
- 시도 횟수 입력 예외 상황
[ ] 숫자가 아닌 경우
기능 목록 작성에는 정답이 없음을 알기에 구현 과정에서 필요에 따라 수정할 계획이다. 기능 목록을 수정해보며 내가 생각하는 최적의 기능 목록 작성법이 무엇인지 터득할거라고 생각했다. 이러한 접근 방식 덕분에 기능 목록 작성에 대한 부담감이 확실히 줄어든 것 같다.
🗂️ MVC 패턴 적용
자연스럽게 MVC패턴을 고려하게 되었다. 폴더에 어떤식으로 함수를 분리할지가 머리속에 그려지는 것 같았다. 정답은 없지만 미리 계획을 세워두면 기능 구현 때 도움이 될 것 같다는 생각이 들어 다음과 같이 작성해보았다.
MVC 패턴을 적용하여 비슷한 역할을 하는 함수들을 분리해보았다. 작성하면서 '이렇게 까지 해도 괜찮을까?'라는 생각이 들었지만 자연스럽게 떠오르는 아이디어를 풀어나가며 다양하게 적용해보는 것이 보다 효율적인 프로그램 구현 방법을 찾는 과정임을 생각하며 작성하게 되었다.
🏄♂️ 플로우 차트 그리기
기능 목록과 폴더구조에 따른 함수 분리까지 구상하는 것도 중요하지만 저번 미션에서는 컨트롤러 구현 시 에러로 인해 상당한 시간을 투자했던 기억이 난다. 그 원인은 제대로 된 프로그램 흐름을 파악하지 못했기 때문이라고 판단했고 이를 해결하기 위해 플로우 차트를 그려보기로 결정했다. 플로우 차트를 그리는 방법은 다양하지만 표준 플로우 차트 기호를 준수하였다.
❓ 표준 플로우 차트 기호
표준 기호들의 역할을 파악하고 이를 기반으로 그린 플로우 차트는 다음과 같다.
플로우 차트를 직접 그려보니 협업 시 프로그램의 순서를 시각화하여 이해를 돕는데 큰 도움이 될 것 같다는 생각이 들었다. 하지만 플로우 차트를 그리는 것은 생각보다 많은 시간이 필요했다. 이로 인한 부담감을 느꼈고, 특히 최종 코딩테스트에서는 5시간이라는 제한된 시간 안에 플로우 차트까지 그리는 것은 비효율적일 것 같다는 생각이 들었다. 따라서 플로우 차트를 대신할 수 있는 다른 효율적인 방안을 찾아야겠다는 생각이 들었고, 그 결과 프로그램 순서를 리스트화하는 방법을 생각해냈다.
- 사용자로부터 자동차 이름을 입력받는다.
- 자동차의 이름을 입력하지 않았는지 확인한다.
- 자동차의 이름이 5자 이하인지 확인한다.
- 자동차의 이름들을 입력할 시 잘못된 쉼표 구분을 하고 있는지 확인한다.
- 자동차의 이름이 중복되었는지 확인한다.
- 사용자로부터 시도 횟수를 입력받는다.
- 시도 횟수를 입력하지 않았는지 확인한다.
- 숫자가 아닌 값을 입력하지 않았는지 확인한다.
- 사용자로부터 입력받은 자동차를 인스턴스를 생성한다.
- 실행결과를 출력한다.
- 실행결과 문구를 출력한다.
- 자동차들의 인스터스를 통한 전진상태를 출력한다.
- 최종 우승자를 출력한다.
입력과 출력에 대한 순서를 크게 설정하고 그 안에서 수행하는 유효성 검사나 핵심 기능들을 세부적으로 리스트화하여 순서도를 작성하였다. 확실히 플로우 차트를 그리는 것보다 더 빠른 시간 안에 이해하고 정리할 수 있었다. 이를 바탕으로 컨트롤러를 작성하는데 큰 문제는 없었다. 하지만 앞으로의 미션을 수행하면서 효과적인 본인만의 기준을 찾는 것이 중요하다는 생각이들었다.
🚀 코드리뷰 반영하기
1️⃣ 구조 분해 할당으로 export, import하기
다음과 같은 리뷰를 받았다. 기존에는 MissionUtils 객체를 import해서 Console과 Random을 접근했었는데 이를 직접 가져올 수 있다는 것을 알게되었다. 자세한 이유를 알고 싶어서 node_modules 내에 있는 @woowacourse/mission-utils를 확인해보았다.
확인 결과 index.js에서 Random과 Console을 객체로 export하고 있었기 때문에 굳이 MissionUtils로 import하지 않아도 됐던 것이었다. 이를 통해 기존에 작성한 코드도 export default로 자체를 export하다 보니 property에 접근할 때마다 중복되는 코드가 발생하였다는 것을 깨달았다. 만약 더 많은 상수가 정의되고 접근할 것이 많아진다면 문제는 훨씬 심해질 것으로 예상했다. 그래서 이번 주차 미션에서는 모든 파일의 객체들을 구조 분해하고 export하여 중복되는 코드를 줄이기로 결정했다. 예를 들어 constants 내부에 있는 ErrorString.js는 구조 분해 할당을 하여 다음과 같이 작성하였다.
const ErrorString = {
ERROR_CARS_LENGTH: '[Error] 자동차를 5개 이하로 입력해주세요.',
ERROR_CARS_SPLIT:
'[Error] 자동차 이름 입력시 구분을 잘 해주세요.(이름은 쉽표(,) 기준으로 구분합니다.)',
ERROR_COUNT_NOT_NUMBER: '[Error] 숫자가 아닌 값을 입력하셨습니다.',
}
export const { ERROR_CARS_LENGTH, ERROR_CARS_SPLIT, ERROR_COUNT_NOT_NUMBER } =
ErrorString
이를 import하고 사용할 때는 다음과 같이 작성하면 된다. 이렇게 하면 객체에 일일이 접근할 필요가 없어 코드가 훨씬 간결해진 것을 확인할 수 있다.
import {
ERROR_CARS_LENGTH,
ERROR_CARS_SPLIT,
ERROR_COUNT_NOT_NUMBER,
} from '../constants/ErrorString.js'
validateCarsInput(inputCars) {
const inputCarsArray = inputCars.split(',')
if (inputCarsArray.length > 5) new Error(ERROR_CARS_LENGTH)
}
2️⃣ 단일 책임 원칙?
생각해보면 generateResultMessage와 같은 메소드를 두어 책임을 분리했다면 메시지를 출력하는 역할만을 수행하는 기능을 만들 수 있었다. 이번 미션에는 단일 책임 원칙에 주의하여 로직을 분리하는 데에 신경을 썼다. 함수의 depth가 깊어지거나 내용이 너무 길다고 판단될 때마다 다시 한번 고민해보았다. 예를 들어 다음은 OutputView.js에 있는 메시지를 출력하는 기능에 대한 내용이다.
const OutputView = {
printResultStartString: () => {
Console.print(OUTPUT_RESULT_START)
},
printResulString: (cars, count) => {
for (let i = 0; i < count; i += 1) {
cars.map(car => driveAndStopCars(car))
Console.print('\\n')
}
},
printWinnersString: cars => {
const winners = findWinners(cars)
Console.print(`최종 우승자 : ${winners.join(', ')}`)
},
}
이전 미션에서는 결과를 출력하는 기능에 볼, 스트라이크, 낫싱의 메시지를 생성하는 기능을 포함했었는데 이번 미션에서는 이들을 각각의 함수로 분리하여 utils 폴더에서 관리하도록 했다. printResultString 함수를 살펴보면 이름과 레이싱 상태의 메시지를 출력하는 기능을 driveAndStopCars로 분리했다. 그 아래에 있는 printWinnersString 함수도 findWinners라는 함수로 분리하여 최종 우승자 메시지를 출력하는 기능을 별도로 분리했다. 이처럼 하나의 함수에 하나의 책임만 부여하도록 신경써 작성하였더니, 코드가 훨씬 간결해진 것을 확인할 수 있었다.
3️⃣ class와 object 고려하기
1주차 미션에서는 InputView와 OutputView, 즉 view에 해당하는 부분을 객체 형태로 작성하였다. 그러나 리뷰에서는 이를 클래스로 통일하는 것이 좋을 것 같다는 의견을 남겨주셨다. 사실 다른 파일들은 클래스 형태로 작성하였지만 view만 객체 형태로 작성하고 있었다. view가 다중 인스턴스를 생성하지 않기 때문에 객체로 간단하게 구현하고 싶었던 것 같다. 그래서 클래스와 객체 중 선택할 때 고려해야 할 판단 기준이 무엇인지 의문이 들었다. 찾아본 결과 크게 4가지를 고려할 수 있었다.
첫번째는 재사용성이다. 클래스를 사용하면 다중 인스턴스를 생성할 수 있기 때문에 재사용성이 좋다. 그러나 이번 미션에서는 view가 다중 인스턴스를 필요로 하지 않기 때문에 클래스를 사용하기에는 적합하지 않다고 판단하였다.
두번째는 유지보수성이다. 클래스를 사용하면 상속 등을 통해 확장 가능성이 있다는 장점이 있다. 하지만 이번 미션에서는 View의 확장성이 필요 없었기 때문에 이 역시 클래스를 사용하기에는 적합하지 않다고 느꼈다.
세번째는 단순성이다. 객체로 작성하면 코드가 더 간단할 수 있다. 클래스를 정의하고 인스턴스화하는 것보다 객체를 만드는 것이 더 직관적일 수 있다고 판단하여 객체를 사용하는 것이 적합하다고 느꼈다.
네번째는 프로젝트 크기이다. 프로젝트 규모가 크다면 클래스로 작성하면 체계적으로 관리가 가능하지만 이번 미션의 규모는 그렇게 크지 않았기 때문에 객체를 사용하는 것이 간단하다고 생각하였다.
이 4가지 요소를 모두 고려해볼 때, 객체로 작성하는 것이 더 적합하다고 생각하였다. 이 리뷰를 통해 클래스로 통일하지는 않았지만 객체와 클래스 설계 시 판단 기준을 재정립하는 데에 도움이 될 수 있었다. InputView를 예로 들면 작성한 코드는 다음과 같다.
import { Console } from '@woowacourse/mission-utils'
import { INPUT_CARS_NAME, INPUT_COUNT } from '../constants/InputString.js'
import {
ERROR_CARS_EMPTY,
ERROR_CARS_LENGTH,
ERROR_CARS_SPLIT,
ERROR_CARS_DUPLICATED,
ERROR_COUNT_EMPTY,
ERROR_COUNT_NOT_NUMBER,
} from '../constants/ErrorString.js'
const InputView = {
readCarsInput: async () => {
// ...
},
readCountInput: async () => {
// ...
},
validateCarsInput: inputCars => {
// ...
},
validateCountInput: inputCount => {
// ...
},
}
export const {
readCarsInput,
readCountInput,
validateCarsInput,
validateCountInput,
} = InputView
🤯 시행착오
🤡 Jest를 이용한 테스트 코드 작성하기
테스트 코드에 대해선 말로만 들어봤지 실제로 프론트엔드에서 사용해본 경험은 없었다. 이번 미션 요구사항에는 작성한 기능에 대한 테스트 코드를 작성하라는 항목이 있었기 때문에 Jest 프레임워크에 대한 기본적인 개념들을 확인해볼 필요가 있었다.
❓ Jest란
Jest는 페이스북에서 개발한 자바스크립트 테스팅 라이브러리이다. Jest는 Test Runner와 Test Matcher, 그리고 Test Mock 프레임워크를 제공하고 있다.
- describe(): 테스트 스위트를 정의하는 함수이다. 테스트 스위트는 특정 기능이나 모듈에 대한 관련된 테스트 케이스를 그룹화하는 역할을 한다.
- test(): 실제 테스트 케이스를 정의하는 함수이다. 각 test 함수는 특정 조건 또는 상황에 대한 테스트를 말한다.
- expect(): 값을 검사하고 검증하는 함수이다. 일반적으로 테스트 케이스 내에서 어떤 값이나 동작을 검사할 때 expect 함수를 사용한다.
- matcher: expect 함수와 함께 사용되고 예상한 결과와 실제 결과를 비교하고 테스트를 평가하는 역할을 한다. Jest는 다양한 Matcher를 제공하는데 예상한 결과와 실제 결과를 비교하고 테스트가 성공했는지 여부를 결정한다. 일반적으로 toEqual, toBe, not.toBe, toContain, toThrow 등이 있다.
🧐 코드 분석
Jest에 대한 이해를 높이기 위해 __test__폴더 내에 있는 ApplicationTest.js 파일을 분석하기로 결정했다.
✅ Mocking 메소드에 대하여
1️⃣ jest.fn
Jest는 jest.fn() 함수를 제공하여 가짜 함수(mock function)를 생성할 수 있다. 가짜 함수를 생성하는 이유는 실제 입력을 기다리지 않고도 원하는 입력값을 이용하여 테스트 케이스를 시뮬레이션할 수 있기 때문이다. jest.fn의 종류는 다음과 같다.
- mockReturnValue(): mocking 함수가 호출될 때 항상 특정 값을 반환하도록 설정할 수 있다.
- mockImplementation(): mocking 함수가 호출될 때 특정 동작을 수행하도록 설정할 수 있다.
- mockResolvedValue() / mockRejectedValue(): Promise를 반환하는 mocking 함수에서 사용된다. mockResolvedValue() 함수를 사용하여 Promise가 성공할 때 반환할 값을 설정하고, mockRejectedValue() 함수를 사용하여 Promise가 실패할 때 반환할 값을 설정할 수 있다.
이를 바탕으로 mockQuestions와 mockRandoms 두 함수가 하는 역할을 살펴보았다.
const mockQuestions = (inputs) => {
MissionUtils.Console.readLineAsync = jest.fn();
MissionUtils.Console.readLineAsync.mockImplementation(() => {
const input = inputs.shift();
return Promise.resolve(input);
});
};
mockQuestions 함수는 mockImplementation을 통해 재정의하여 사용자의 다음 입력값을 순서대로 반환하는 가짜 함수를 만들었다는 것을 알 수 있다.
const mockRandoms = (numbers) => {
MissionUtils.Random.pickNumberInRange = jest.fn();
numbers.reduce((acc, number) => {
return acc.mockReturnValueOnce(number);
}, MissionUtils.Random.pickNumberInRange);
};
mockRandoms 함수는 numbers 배열을 순회하면서 누적된 결과를 생성하는 가짜 함수임을 알 수 있다.
2️⃣ jest.spyOn
jest.spyOn() 함수는 특정 객체의 함수가 얼마나 많이 호출되었는지 그리고 어떤 인자가 넘어갔는지에 대한 정보를 얻을 수 있는 기능을 제공한다.
- toHaveBeenCalled(): 해당 메서드가 최소 한 번 호출되었는지 확인한다.
- toHaveBeenCalledTimes(n): 해당 메서드가 정확히 n 번 호출되었는지 확인한다.
- toHaveBeenCalledWith(arg1, arg2, ...): 해당 메서드가 특정 인수와 함께 호출되었는지 확인한다. arg1, arg2, 등은 예상되는 인수이다.
- toHaveBeenLastCalledWith(arg1, arg2, ...): 해당 메서드가 마지막 호출될 때 특정 인수와 함께 호출되었는지 확인한다.
- toHaveBeenNthCalledWith(n, arg1, arg2, ...): 해당 메서드가 호출 중 n 번째 호출되었을 때 특정 인수와 함께 호출되었는지 확인한다.
- toHaveReturned(): 해당 메서드가 어떤 값을 반환했는지 확인한다.
- toHaveReturnedTimes(n): 해당 메서드가 정확히 n 번 값을 반환했는지 확인한다.
- toHaveLastReturned(): 해당 메서드가 마지막 호출될 때 어떤 값을 반환했는지 확인한다.
- toHaveNthReturned(n, value): 해당 메서드가 호출 중 n 번째 호출되었을 때 특정 값을 반환했는지 확인한다.
이를 기반으로 getLogSpy 함수가 수행하는 역할을 살펴보았다.
const getLogSpy = () => {
const logSpy = jest.spyOn(MissionUtils.Console, "print");
logSpy.mockClear();
return logSpy;
};
개념을 이해한 후에는 getLogSpy의 역할을 생각보다 쉽게 해석할 수 있었다. 먼저, MissionUtils.Console 객체의 print 메소드를 스파이로 설정하였다. 그 후에 mockClear()를 사용하여 스파이의 동작을 초기화하고 초기화된 스파이 객체를 반환하는 함수임을 알 수 있다. 이를 통해 getLogSpy 메소드를 호출하여 객체를 변수에 할당하고 Console.print 메소드에 대한 정보를 검증(expect)할 때 사용할 수 있도록 하는 함수인 것을 알 수 있었다.
✍️ 기능별 테스트 코드 작성
Jest에 대한 기본적인 개념과 작성법에 대해 어느 정도 이해하였다고 가정하고 본격적으로 어떤 기능들을 테스트할 것인지 결정해보았다. 먼저 테스트 스위트를 크게 나누어 보았다.
자동차 경주의 핵심 기능, 자동차 이름 입력의 예외 상황, 시도 횟수 입력의 예외 상황이 이 세 가지 주요 요소였다. 그리고 각 함수에 대한 테스트 케이스를 다음과 같이 설정하였다.
**자동차 경주의 핵심 기능 테스트**
- 자동차 경주 게임
- 자동차 입력에 따른 Car클래스 인스턴스 생성 기능
- 사용자가 입력한 횟수만큼 자동차의 전진상태를 출력하는 기능
- 최종 우승자를 가려내는 기능
**입력에 따른 유효성 테스트**
- 자동차 이름 입력 예외 상황
- 아무 값도 입력하지 않았을 경우
- 입력한 자동차의 이름이 5자 이하가 아닐 경우
- 자동차 이름이 서로 같을 경우
- 맨 앞과 마지막이 쉼표로 끝날 경우
- 시도 횟수 입력 예외 상황
- 아무 값도 입력하지 않았을 경우
- 숫자가 아닌 경우
위와 같이 구상한 후에는 InputValidTest.js와 CarRacingTest.js로 나누어 테스트 코드를 작성하였다.
🐞 디버깅
1️⃣ 자동차의 전진상태를 반환하는 함수(driveAndStopCars)
import { Console, Random } from '@woowacourse/mission-utils'
const driveAndStopCars = car => {
if (Random.pickNumberInRange(0, 9) >= 4) car.drive()
Console.print(`${car.getName()} : ${car.getStatus().join('')}`)
}
export default driveAndStopCars
기존에는 dirveAndStopCars 함수에서 자동차의 전진 상태를 결정하고 이를 콘솔에 출력하는 방식으로 작성하였다. 그러나 테스트 코드를 작성하면서 의문점이 생겼다. 바로 해당 함수를 테스트 코드로 작성하고자 했지만 Console.print() 함수를 사용하고 있어서 테스트하기 어려웠다. 이에 대해 다시 생각해보니 OutputView의 역할은 콘솔에 출력하는 것이지만 이 역할을 driveAndStopCars 함수에서 수행하고 있었다. 이는 해당 함수가 담당하지 않아야 할 책임을 부여하는 문제를 일으키면서 테스트 코드를 작성하는 데에도 어려움을 초래하였다. 따라서 이를 다음과 같이 수정하였다.
// driveAndStop.js
import { Random } from '@woowacourse/mission-utils'
const driveAndStopCars = car => {
if (Random.pickNumberInRange(0, 9) >= 4) car.drive()
return `${car.getName()} : ${car.getStatus().join('')}`
}
export default driveAndStopCars
// OutputView.js
printResulString: (cars, count) => {
for (let i = 0; i < count; i += 1) {
Console.print(cars.map(car => driveAndStopCars(car)).join('\\n'))
Console.print('\\n')
}
}
driveAndStopCars 함수는 자동차의 전진 상태에 대한 배열 값을 반환하도록 수정되었고 이를 호출하는 OutputView의 printResultString에서는 출력만 담당하도록 변경하였다. 이 변경으로 인해 driveAndStopCars 함수에 대한 반환 값으로 테스트 코드를 작성할 수 있게 되었다.
const mockRandoms = numbers => {
Random.pickNumberInRange = jest.fn();
numbers.map(number => Random.pickNumberInRange.mockReturnValueOnce(number));
};
test('사용자가 입력한 횟수만큼 자동차의 전진상태를 출력하는 기능', () => {
const randoms = [1, 5, 2, 7, 3, 1];
mockRandoms(randoms);
const input = 'pobi, kang'
const count = 3
let receivedValue
const expectedValue = ['pobi : ', 'kang : --']
const carRacingController = new CarRacingController()
const carInstances = carRacingController.createCarInstances(input)
for (let i = 0; i < count; i++) {
receivedValue = carInstances.map(car => driveAndStopCars(car))
}
expect(receivedValue).toStrictEqual(expectedValue)
})
무작위 예측값을 고정하고 각각의 자동차 인스턴스마다 driveAndStopCars 함수를 호출하여 최종 결과값을 receivedValue에 할당하여 테스트하였다.
2️⃣ 맨 앞과 마지막이 쉼표로 끝 날 경우
예외 상황을 처리하기 위해 사용자로부터 차량 이름을 입력받을 때 맨 앞이나 뒤에 쉼표가 있을 경우 에러 메시지를 출력하도록 처리하려 했다. 그러나 테스트와 index.js를 실행해봤을 때 중복 오류 메시지가 출력되는 것을 발견하였다. 이를 통해 입력 처리 부분에 문제가 있다는 것을 파악하고 코드를 다시 확인해보았다.
validateCarsInput: inputCars => {
const inputCarsArray = inputCars.split(',').map(inputCar => inputCar.trim())
console.log(inputCarsArray)
if (inputCars.length === 0) throw new Error(ERROR_CARS_EMPTY)
inputCarsArray.map(inputCar => {
if (inputCar.length > 5) throw new Error(ERROR_CARS_LENGTH)
})
if (inputCarsArray.length !== new Set(inputCarsArray).size)
throw new Error(ERROR_CARS_DUPLICATED)
if (inputCarsArray.includes('')) throw new Error(ERROR_CARS_SPLIT)
},
확인해본 결과 문제는 '공백이 포함되어 있는 경우'가 '중복이 발생하는 경우'보다 후순위에 있었던 것이었다. 왜냐하면 맨 앞과 뒤에 동시에 쉼표가 있을 경우 두 개의 공백을 인식하게 되고, 이를 중복으로 파악하게 되기 때문이다. 따라서 '공백이 포함되어 있는 경우'를 '중복이 발생하는 경우'보다 우선 순위를 높여서 이 문제를 해결할 수 있었다. 변경된 코드는 다음과 같다.
validateCarsInput: inputCars => {
const inputCarsArray = inputCars.split(',').map(inputCar => inputCar.trim())
console.log(inputCarsArray)
if (inputCars.length === 0) throw new Error(ERROR_CARS_EMPTY)
inputCarsArray.map(inputCar => {
if (inputCar.length > 5) throw new Error(ERROR_CARS_LENGTH)
})
if (inputCarsArray.includes('')) throw new Error(ERROR_CARS_SPLIT)
if (inputCarsArray.length !== new Set(inputCarsArray).size)
throw new Error(ERROR_CARS_DUPLICATED)
},
🛠️ 리팩토링
1️⃣ 단일 책임 원칙 신경쓰기(createCarInstances)
모든 요구사항을 충족시킨 후, 코드 리뷰에서 단일 책임 원칙에 대한 의견을 듣고 controller의 코드를 다시 살펴보았다.
import Car from '../model/Car.js'
import { readCarsInput, readCountInput } from '../view/InputView.js'
import {
printResultStartString,
printResulString,
printWinnersString,
} from '../view/OutputView.js'
class CarRacingController {
#carInstances
inputCars = async () => {
// ...
}
inputCount = async cars => {
// ...
}
printRacingResult = (cars, count) => {
const carsArray = cars.split(',').map(car => car.trim())
this.#carInstances = carsArray.map(car => (car = new Car(car)))
printResultStartString()
printResulString(this.#carInstances, count)
return this.printRacingWinners()
}
printRacingWinners = () => {
// ...
}
}
export default CarRacingController
사용자로부터 자동차 이름들과 시도 횟수를 입력받는 메소드에서는 각 기능들이 잘 분리되어 있다고 생각하였다. 그러나 경기 상태를 출력하는 메소드에서 사용자로부터 입력받은 자동차 이름들을 인스턴스화하고 있음을 확인하였다. 이 부분은 inputCars 메소드에서 처리하면 더 적절하다고 판단하였다. 이에 따라 다음과 같이 리팩토링을 진행하였다.
import Car from '../model/Car.js'
import { readCarsInput, readCountInput } from '../view/InputView.js'
import {
printResultStartString,
printResulString,
printWinnersString,
} from '../view/OutputView.js'
class CarRacingController {
#carInstances
inputCars = async () => {
const cars = await readCarsInput()
const carsArray = cars.split(',').map(car => car.trim())
this.#carInstances = carsArray.map(car => (car = new Car(car)))
return this.inputCount()
}
inputCount = async () => {
// ...
}
printRacingResult = count => {
printResultStartString()
printResulString(this.#carInstance, count)
return this.printRacingWinners()
}
printRacingWinners = () => {
// ...
}
}
export default CarRacingController
해당 로직을 inputCars 메소드에서 처리하도록 변경하고, 불필요한 매개변수와 인수를 모두 제거하였다. 이렇게 변경한 결과, 기능에 맞게 잘 분리된 것을 확인할 수 있었다.
2️⃣ 시작 지점을 명확하게(startCarRacing)
import CarRacingController from './controller/CarRacingController.js'
class App {
#carRacingController = new CarRacingController()
async play() {
await this.#carRacingController.inputCars()
}
}
export default App
기존에는 App.js에서 controller를 인스턴스화하고 inputCars를 호출하는 방식을 사용하였다. 그러나 이런 방식이면 다른 사람이 코드를 볼 때 해당 메소드가 시작 지점인지 명확하게 파악하기 어려울 것 같다는 생각이 들었다. 그래서 controller에 시작 지점을 명시하는 메소드를 추가하였다.
// CarRacingController.js
import Car from '../model/Car.js'
import { readCarsInput, readCountInput } from '../view/InputView.js'
import {
printResultStartString,
printResulString,
printWinnersString,
} from '../view/OutputView.js'
class CarRacingController {
#carInstances
startCarRacing = async () => {
await this.inputCars()
}
// ...
}
export default CarRacingController
// App.js
import CarRacingController from './controller/CarRacingController.js'
class App {
#carRacingController = new CarRacingController()
async play() {
await this.#carRacingController.startCarRacing()
}
}
export default App
startCarRacing이라는 메소드를 추가하여 리팩토링하였다. 이런 방식으로 코드를 작성하니시작 지점이 더욱 명확하게 드러나게 되는 것을 알 수 있다.
3️⃣ 한번에 할당하기(findWinners)
const findWinners = cars => {
const carsStatus = [];
const winners = [];
cars.map(car => carsStatus.push(car.getStatus().length));
cars.forEach(car => {
if (Math.max(...carsStatus) === car.getStatus().length) winners.push(car.getName());
});
return winners;
};
export default findWinners;
기존에는 변수를 따로 선언하고 할당하기 위한 로직을 작성하였다. 그러나 이를 한 번에 처리할 수 있는 방법을 고민하다가 다음과 같은 코드로 리팩토링하게 되었다.
const findWinners = cars => {
const maxStatusLength = Math.max(...cars.map(car => car.getStatus().length));
const winners = cars
.filter(car => car.getStatus().length === maxStatusLength)
.map(car => car.getName());
return winners;
};
export default findWinners;
전진 상태의 최대 길이를 구하기 위해 자동차 인스턴스의 배열을 순회하며 전진 상태를 가져오고 스프레드 연산자를 통해 최대값을 구하여 maxStatusLength에 한 번에 할당하였다.
그 다음으로 최종 우승자를 결정하기 위해 자동차 인스턴스 배열에서 이전에 구한 최대 전진 상태와 동일한 인스턴스를 추려냈다. 이를 다시 순회하여 우승자의 이름 배열을 winners에 한 번에 할당할 수 있었다.
💡 회고
저번 미션에서 다소 어려움을 겪었던 만큼, 이번 미션은 비교적 수월하게 진행한 것 같다. 이번 미션에서는 특히 이전 미션에서 받았던 코드 리뷰와 시행착오를 겪으면서 배운 내용들을 반영하는데 집중하였다.
특히 구현 전에 기능 목록과 플로우 차트를 구상하는 데 시간을 많이 들였다. 미션의 기능 구현과 코드 스타일 적용까지 약 3시간 정도로 빠르게 마무리한 것 같다. 이는 초기 구상과 세팅이 체계적인 설계와 시간 단축에 큰 도움이 된다는 것을 체감하게 되었다.
또한 Jest 프레임워크에 대해 알게 된 것도 큰 수확이었다. 객체 지향 설계를 공부하면서 GoogleTest를 통해 단위 테스트를 해본 경험이 있었지만, 프론트엔드에서도 과연 테스트가 필요할까? 라는 의문을 가지고 있었다. 그런데 이번에 Jest 프레임워크의 장점을 알게되면서 그런 의문을 해결할 수 있었다.
이번 미션까지 진행해보면서 점진적으로 성장하는 나 자신의 모습을 보며 뿌듯함을 느꼈다. 다음 미션에서도 어떤 부분이 나를 성장시킬 수 있을지 기대가 된다. 그동안 배운 것들을 바탕으로 더 많은 사람들의 코드를 보고 코드 리뷰도 받아보면서 부족한 부분을 계속해서 성장시켜 나갈 것이다.
⛳️ 다음 목표
- 프로그램 순서의 이해에 대한 최적의 기준 찾기
- 더 많은 사람들의 코드를 분석하고 서로 코드 리뷰 해보면서 개선점 찾기
- Jest와 함께 테스트 코드에 친숙해지기
🏃♂️ 구현 코드 보러가기
[자동차 경주] 강병현 미션 제출합니다. by llbllhllk · Pull Request #108 · woowacourse-precourse/javascript-racin
github.com
'🚀 우아한테크코스 6기 프리코스' 카테고리의 다른 글
[우아한테크코스 6기] 최종 코딩 테스트 - 온콜 리팩토링 회고록 (0) | 2023.12.21 |
---|---|
[우아한테크코스 6기] 1차 합격 및 최종 코딩 테스트 후기 (4) | 2023.12.17 |
[우아한테크코스 6기] 프리코스 4주차 - 크리스마스 프로모션 회고록 (0) | 2023.11.20 |
[우아한테크코스 6기] 프리코스 3주차 - 로또 회고록 (0) | 2023.11.10 |
[우아한테크코스 6기] 프리코스 1주차 - 숫자 야구 회고록 (0) | 2023.10.26 |