본문 바로가기
Developer/후기

[FE] 우아한 테크코스 6기 프리코스 2주차 회고: 자동차 경주 게임

by 청량리 물냉면 2023. 11. 3.
반응형

 

시작

이번 과제에서 중점으로 삼아야 하는 것은 함수 분리와, 각 함수별 테스트 작성이었다.

이전 과제에서 다음 과제부터는 테스트 코드에 익숙해지고 직접 테스트를 작성해 보겠다는 소감을 작성했었기 때문에, 이번 과제가 나에게 많은 도움이 될 수 있을 것 같다. 

추가로 이전 과제와는 달리 요구사항이 추가되었는데, 함수의 indent는 최대 2여야 하고 git 커밋 메시지는 커밋 메시지 컨벤션을 참고해 기능 단위로 작성해야 한다. 

 

기능 요구 사항

 

구현 기능 목록

구현한 기능 목록은 다음과 같다.  

 

입력받기 

[ ] 자동차 이름 입력받기 (쉼표 기준으로 n대, 이름은 5자 이하) 

[ ] 시도 횟수

 

예외 

[ ] 자동차 이름 입력: 이름 5자 초과 입력

[  ]  자동차 이름 입력: 이름 입력 없음

[  ]  자동차 이름 입력: 이름 중복 입력

[  ]  시도할 횟수 입력: 숫자가 아닌 문자(알파벳, 기호 등등) 입력

[  ]  시도할 횟수 입력: 음수 입력

[  ]  시도할 횟수 입력: 횟수 입력 없음

 

결과 연산하기

[ ] 컴퓨터가 랜덤으로 0-9 중 하나의 수를 고르기 ➡ 4 이상일 경우 전진, 4 미만일 경우 정지  (사용자가 입력한 '시도 횟수'회 동안 반복)

[ ] 사용자 입력 횟수 종료 후, 각 자동차의 '-'의 갯수를 세어 우승자 계산하기

 

결과 출력하기 

[ ] 사용자 입력 횟수 각 turn 종료 후,  각 차수별 실행결과 출력

[ ] 사용자 입력 횟수 모두 종료 후,  공동 우승자 (쉼표 기준으로)와 최종 우승자 출력

 

순서도 작성

 

폴더 구조  설계

📄 App.js ...게임 시작부터 종료까지 루틴을 관리하는 로직

 

📂 racingGame 레이싱 게임 진행에 필요한 파일을 관리하는 폴더
ㄴ📄 RacingGame.js ...레이싱 게임에 필요한 로직을 관리

 

📂 utils 유틸들을 모아놓은 폴더

ㄴ📄 inputHandler.js ...유저에게 입력을 받고 입력 받은 값을 검증 후 리턴하는 함수  

ㄴ📄 constants.js ...상수값을 관리하는 함수

ㄴ📄 validateInput.js ...입력값 검증 함수 (inputHandler에서 검증까지 처리하므로 해당 파일 구현하지 않음)

ㄴ📄 calculateWinners.js ...최종 우승자 계산하는 함수

ㄴ📄 generateRandomNumber.js ...랜덤 숫자 생성하는 함수

 

📂 models 모델을 관리하는 폴더

ㄴ📄 Car.js ...차 이름, 전진 횟수, 전진 함수, 전진 횟수 출력 함수

 

📂 errors  예외 처리 파일을 관리하는 폴더

ㄴ📄 CarError.js ...자동차 이름 입력받을 시, 에러 종류에 따라 에러 메시지 리턴

ㄴ📄 TryError.js ...시도횟수 입력받을 시, 에러 종류에 따라 에러 메시지 리턴

ㄴ📄 CommonError.js ...빈 문자열 입력받을 시, 에러 종류에 따라 에러 메시지 리턴 (추가)

 

챌린징

1. 테스트 코드 작성 시 에러

import { inputTryNumberHandler } from "../src/utils/inputHandler.js";

describe("시도 횟수 입력 함수 테스트", () => {
  test("올바른 숫자 입력", () => {
    // Arrange
    const input = "5";

    // Act & Assert
    expect(() => inputTryNumberHandler(input)).not.toThrow();
  });

  test("음수 값 입력", () => {
    // Arrange
    const input = "-3";

    // Act & Assert
    expect(() => inputTryNumberHandler(input)).toThrow("[ERROR]");
  });

  test("숫자가 아닌 값을 입력", () => {
    // Arrange
    const input = "abc";

    // Act & Assert
    expect(() => inputTryNumberHandler(input)).toThrow("[ERROR]");
  });

  test("빈 문자열 입력", () => {
    // Arrange
    const input = "";

    // Act & Assert
    expect(() => inputTryNumberHandler(input)).toThrow("[ERROR]");
  });
});

 

1-1. received function did not throw 에러 발생

await expect(api(`verify/${profile.auth.verifyToken}`, {method: 'POST'}).json())
  .rejects.toThrow();

문제를 해결하기 위해 스택오브플로어를 참고했는데, 위와 같이 코드를 작성하여 에러를 해결하라고 조언하고 있다.(원문 링크)

기존에 주어진 ApplicationTest.js를 참고하여 아래와 같이 고쳐 보았다.

...
describe("시도 횟수 입력 함수 테스트", () => {
  test("올바른 숫자 입력", async () => {
    // Arrange
    const input = "5";

    // Act & Assert
    await expect(async () => inputTryNumberHandler(input)).not.toThrow();
  });

  ...
  //아래 테스트들도 동일한 방식으로 작성
    );
  });
});

 

1-2. 수정 후 이번에는 시간초과 에러 발생.

(thrown: "Exceeded timeout of 5000 ms for a test. Add a timeout value to this test to increase the timeout, if this is a long-running test. See https://jestjs.io/docs/api#testname-fn-timeout.)

해결책으로 각 테스트의 함수의 async를 전부 빼버렸다.

...
describe("시도 횟수 입력 함수 테스트", () => {
  test("올바른 숫자 입력", () => {	//이 부분 async 삭제
    // Arrange
    const input = "5";

    // Act & Assert
    expect(async () => inputTryNumberHandler(input)).not.toThrow();
  });

  ...
  });
});

정상적으로 테스트가 진행된다.

 

+ 추가)

inputTryNumberHandler() 함수는 원래 매개변수가 없는 함수이다. 

그런데 위의 테스트를 진행할 때는 inputTryNumberHandler(input) 으로, 인수에 input 변수를 넣어 함수를 호출하고 있다. 

테스트에 익숙하지 않아 input 값을 어떻게 함수에 전달할 지 몰라서 변수를 인수로 한 번 넣어봤는데 테스트가 동작은 되길래 이렇게 하면 되는구나~ 하고 있었는데 공부하는 과정에서 여러 테스트 케이스를 접하다 보니 틀린 방식으로 테스트하고 있었다는 걸 깨달았다...

여기저기 구글링하고 공식문서도 참고하다가 아래처럼 테스트 코드를 다시 짰다.

import { inputTryNumberHandler } from "../src/utils/inputHandler.js";
import { Console } from "@woowacourse/mission-utils";

describe("시도 횟수 입력 함수 테스트", () => {
  test("올바른 숫자 입력", async () => {
    // 모의(mock) Console.readLineAsync 설정
    Console.readLineAsync = jest.fn().mockResolvedValue("5");

    // 함수 호출 및 기대값 검사
    await expect(inputTryNumberHandler()).resolves.toBe("5");
  });

  test("음수 값 입력", async () => {
    Console.readLineAsync = jest.fn().mockResolvedValue("-3");
    await expect(inputTryNumberHandler()).rejects.toThrow("[ERROR]");
  });

  test("숫자가 아닌 값을 입력", async () => {
    Console.readLineAsync = jest.fn().mockResolvedValue("abc");
    await expect(inputTryNumberHandler()).rejects.toThrow("[ERROR]");
  });

  test("빈 문자열 입력", async () => {
    Console.readLineAsync = jest.fn().mockResolvedValue("");
    await expect(inputTryNumberHandler()).rejects.toThrow("[ERROR]");
  });
});

위의 테스트 코드는 Jest모의(mock)를 사용하여 inputTryNumberHandler 함수를 테스트하는 코드이다.

이 코드에서 눈여겨 봐야 할 주요 사항은 다음과 같다.

1. Console.readLineAsync를 모의(mock)하여 사용자 입력을 제어한다. 이후 각 테스트 케이스에서 적절한 입력을 시뮬레이트한다.
2. awaitasync를 사용하여 함수를 비동기적으로 호출하고 반환값을 검사한다.
3. 각 테스트 케이스에서 expect 함수를 사용하여 함수 호출 결과를 검사하며, resolves, rejects, toThrow 함수를 이용하여 반환값과 예외를 확인한다.

 

< 코드에 사용된 함수 정리 >

  • mockFn.mockResolvedValue(value): 비동기 테스트에서 비동기 함수를 모의(mock)하는 데 사용
  • resolves: Promise가 해결되고 결과 값이 다음 일치자(다음 일치자는 '.'으로 이어짐)와 같은지 테스트
  • rejects: Promise가 다음 일치자의 이유로 거부되는지 테스트

 

2. 결과 출력 테스트 케이스 짜기

  test("결과 출력(우승자 여러 명)", async () => {
    // given
    const MOVING_FORWARD = 4;
    const inputs = ["pobi,woni,jun", "7"];
    const randoms = Array(28).fill(MOVING_FORWARD);

    const logSpy = getLogSpy();

    mockQuestions(inputs);
    mockRandoms([...randoms]);

    // when
    const app = new App();
    await app.play();

    // then
    const logCalls = logSpy.mock.calls;
    const lastLogCall = logCalls[logCalls.length - 1][0];

    expect(lastLogCall).toContain("최종 우승자 : pobi, woni, jun");
  });

눈 여겨 볼 코드는 아래 두 줄이다.

    const logCalls = logSpy.mock.calls;
    const lastLogCall = logCalls[logCalls.length - 1][0];

위 테스트 코드는 Jest와 모의(Mock) 함수를 사용해 log(기록)함수를 호출한 뒤, 호출 정보를 저장한 배열을 만드는 코드이다.

 

1. logCalls(mock.calls) 배열logSpy 모의(mock)함수 호출 시 전달된 모든 인자 값들을 담고 있는 배열이다.
2. logCalls.length - 1을 사용하여 logCalls 배열의 마지막 호출을 가져온다. 여기서 logCalls.length은 logCalls 배열의 길이이다.
3. logCalls[logCalls.length - 1][0]를 사용하여 마지막 호출(call)의 첫 번째 인자를 가져온다. 이때 log 함수는 logSpy에 의해 호출되며, 첫 번째 인자에는 log 함수에 전달된 메시지 문자열이 포함되어 있다.

레이싱 코드의 경우, 앞서 다른 출력들이 존재하고 결과는 가장 마지막에 출력되기 때문에 가장 마지막 호출을 가져왔다. 

즉, 이 코드는 마지막 로그 호출의 메시지 문자열을 lastLogCall 변수에 저장하여 검증에 활용하는 코드이다.

 

 

소감

이번 과제는 '함수의 분리'와 '함수의 테스트케이스 작성'이라는 목표를 최대한 상기하며 구현했다. 따라서 소감문도 '함수 분리'와 '테스트케이스 작성' 위주로 작성해 보려고 한다.

'함수 분리'는 indent를 2 이하로 유지하라는 조건에 따라 진행해 보았다. indent 유지에 집중하며 함수를 분리해 본 경험은 처음이었는데 함수를 분리하니 훨씬 코드가 깔끔해지고 가독성이 높아졌다고 느꼈다. 아직 함수 분리가 어색하긴 하지만 코드를 정갈하게 짜는 과정이 재미있게 느껴져서 즐겁게 프로그래밍하였다.

'테스트 케이스 작성'은 '함수 분리'보다 난이도가 있는 작업이었다.
이전 과제에서는 테스트 코드에 대한 지식의 부재로 아쉽게도 테스트에 통과하지 못했다. 하지만 이후에 추가로 Jest에 관해 공부해 보며 테스트 코드를 어떻게 실행해야 하는지, 어떤 식으로 테스트 코드가 작성되는지 대강의 흐름을 알게 되었다. 또한 우아한 테크 코스에 참여 중인 다른 개발자분들의 코드를 살펴보며 테스트 코드는 이런 식으로 작성할 수 있구나 하는 깨달음도 얻었다.
이러한 지식을 바탕으로 이번 과제에서는 테스트 통과에 중점을 두고 테스트 코드를 먼저 분석해 보았다. 아직 지식의 부족으로 완벽한 이해는 힘들었지만 우테코 디스코드에 공유된 테스트 코드 분석 글을 읽어보며 테스트 코드를 이해하려 노력했다. 하나의 기능을 구현할 때마다 테스트 코드를 실행하며 이해가 되지 않는 부분은 찾아가며 코드를 작성했다.
과제를 구현하며 제일 인상 깊었던 순간은 기존에 주어진 테스트 코드를 이용해서 테스트를 작성했지만, 도저히 입력값에 따른 결괏값(최종우승자)을 출력하지 못해 고심하던 때, mock.calls를 이용해 logSpy의 모든 인자를 불러왔던 때이다. 이후 logSpy.mock.calls[logCalls.length - 1][0]로 가장 마지막 호출의 첫 번째 인자를 가져와 최종 우승자를 출력해 낼 수 있었는데, 인터넷 검색과 Jest 공식 문서를 통해 기존에 주어지지 않은 방식으로 테스트 코드를 작성했고 테스트에 통과했기 때문에 기억에 남는다. 주어진 힌트를 최대한으로 활용해 공부하고 이용하는 과정에서 많은 성장을 이룰 수 있다는 것을 알게 되었던 값진 경험이었다.

소감문에서 작성한 내용 이외에도 이번 과제에서 많은 것을 배워가며 성장한 것 같다. 지난번 과제에서 학습했던 지식을 많이 활용했던 만큼, 다음번 과제에서도 이번 과제에서 얻은 역량을 활용하여 더 나은 코드를 작성하는, 발전하는 개발자가 되고 싶다고 생각했다.

 

 

새롭게 학습한 내용

화살표 함수와 일반 함수의 차이

 

JavaScript - 화살표 함수와 일반 함수의 차이 - CODE:H

화살표 함수(Arrow Function)란 화살표함수는 ES6에서 새로 추가된 내용이다. 기존 함수 표현식과 비교하면 간결한표현으로 간단하게 사용가능하다. function fun() { // 일반함수 ... } const arrFun = () => { //

hhyemi.github.io

 

utils 폴더 > 함수 모듈 vs 클래스 모듈

1. 함수 모듈 (Function Module): utils 폴더에 각 함수를 개별 파일로 구현하여 별도의 모듈로 분리하는 방식.

이 방식은 작은 프로젝트나 간단한 작업에서 유용하다. 함수를 묶어서 클래스로 정의할 필요가 없는 경우에 사용한다.

// utils/constants.js
// utils/validator.js
// utils/calculateWinners.js
// utils/generateRandomNumber.js

2. 클래스 모듈 (Class Module): 함수들을 하나의 클래스 내에서 정의하고, 해당 클래스를 모듈로 사용하는 방식.

이 방식은 논리적으로 관련 있는 함수를 묶어서 모듈화하고, 클래스 내에서 상태 정보나 상호 작용을 처리할 때 유용하다. 클래스를 사용하여 모듈화할 경우 객체 지향 프로그래밍(OOP)의 개념을 활용할 수 있다.

// utils/Utils.js
class Utils {
  static generateRandomNumber() {
    // Generate RandomNumber Logic
  }

  static validateInput(input, errorType) {
    // Validation Logic
  }

  static calculateWinners(cars) {
    // Calculation Logic
  }
}

module.exports = Utils;

어떤 방식을 선택할지는 프로젝트의 크기, 코드의 구조, 팀의 개발 스타일 등에 따라 다르다. 

더 작은 프로젝트에서는 함수 모듈이 간결하고 직관적일 수 있으며, 더 큰 프로젝트에서는 클래스 모듈을 사용하여 코드를 더 모듈화하고 구조화할 수 있다. 주요 목표는 코드를 읽기 쉽고 관리하기 쉽게 만드는 것이며, 이를 위해 코드 모듈화의 방식을 선택하면 된다.

 

commonJS 방식 vs ES Modules 방식

package.json

 "type": "module",

type이 module로 명시되어 있으므로, 이번 과제에서도 module 방식을 사용할 예정이다. commonJS 방식 사용 시 require is not defined in es module scope, you can use import instead 라는 에러가 발생한다.

(참고: https://tinyurl.com/ykmsfme8)

 

Jest

Jest: logSpy

 

우테코 테스트 코드로 본 Jest mock function - 3

우테코 5기 미션에 주어진 테스트 코드를 보며 공부해 본 내용입니다.

velog.io

Jest: Expect

 

Jest - Expect 기능 익히기 (3)

이번 포스트에서는 Jest 의 Expect 에 대해서 알아보겠습니다. Expect 기능? 테스트를 작성할 때 값이 특정 조건을 충족하는지 확인할 필요가 있습니다. Expect는 여러가지 상황을 검증할 수 있는 수많

www.js2uix.com

 

Expect · Jest

When you're writing tests, you often need to check that values meet certain conditions. expect gives you access to a number of "matchers" that let you validate different things.

jestjs.io

Jest: Mock

 

Mock Functions · Jest

Mock functions are also known as "spies", because they let you spy on the behavior of a function that is called indirectly by some other code, rather than only testing the output. You can create a mock function with jest.fn(). If no implementation is given

jestjs.io

Jest: Mock - mock.calls

 

Jest 사용법 (7) - 모의 함수 (Mock Functions)

JavaScript 테스팅 도구인 Jest의 사용법의 마지막. 모의 함수(Mock Functions)에 대해서 알아 봅니다.

velog.io

 

const + Object.freeze()로 객체 동결시키기(재할당 금지)

 

[Javascript] const와 Object.freeze()를 이용하여 Immutable한 Object 만들기

프로그래밍을 하다보면 개발자의 실력과는 별개로 언제나 "Human Error"가 발생할 수 있다. 특히 개발자의 실수로 인해 의도치 않게 전역 공간이나 외부 스코프의 값이나 상태가 변경되는 “Human Err

yorr.tistory.com

 

 

 

소스 코드

https://github.com/Yoonyesol/javascript-racingcar-6/tree/yesol

 

GitHub - Yoonyesol/javascript-racingcar-6

Contribute to Yoonyesol/javascript-racingcar-6 development by creating an account on GitHub.

github.com

 

반응형