Javascript

우아한 테크코스 6기: 프리코스 2주차 후기

Jinmidnight 2023. 10. 31. 14:36

2주차 과제를 하기에 앞서 메일을 통해 받은 1주차 공통 피드백을 꼼꼼히 살펴보고자 했습니다.

제 자신이 올바른 방향으로 성장해 나아가고 있는지 객관적으로 확인하고 싶었기 때문입니다.

 

아는 내용이 있어도 복습한다는 마음가짐으로, 구글 docs로 이루어진 1주차 공통 피드백추가 학습 자료 전부를 면밀히 살펴보고 학습했습니다. 그리고, 헷갈리거나 새로 배운 내용 중심으로 아래 게시글에 정리했습니다.

 

https://jinmidnight.tistory.com/36

 

우아한 테크코스 6기: 프리코스 1주차 Front-End 피드백 정리

우아한 테크코스 6기 프리코스 1주차 과제에 대한 공통 피드백이 주어졌다. 프리코스 2주차 과제를 하기에 앞서, 더 나은 과제 수행을 위해 해당 피드백을 꼼꼼히 살펴보며 정리하고자 한다. 1주

jinmidnight.tistory.com


2주차 과제 코드

https://github.com/woowacourse-precourse/javascript-racingcar-6/pull/724

 

[자동차 경주] 박진효 미션 제출합니다. by jinmidnight01 · Pull Request #724 · woowacourse-precourse/javascript-r

 

github.com

 

2주차 미션을 시작하면서, 1주차 때와 마찬가지로 코드 스타일을 적용하고 기능별로 브랜치를 생성하여 작업했습니다.

 

  1. 코드 스타일: eslint(airbnb style), prettier, JSDoc을 설치 및 채택했습니다.
  2. 브랜치 전략: feature별로 생성한 브랜치들에서 작업한 뒤, develop 브랜치에 합쳤고 최종적으로 jinmidnight01 브랜치에 병합하는 방식으로 진행했습니다. 차이점이 있다면 이번에는 1주차 피드백을 바탕으로 merge와 rebase를 혼용하여 작업했습니다.

 

또한, 미션에서 제시한 커밋 메세지 컨벤션에 맞게, 최대한 신경써서 commit 메세지를 작성했습니다.

 

 

 

다음으로, 2주차 미션 메일에서 강조한 함수 분리테스트 작성을 중점으로 과제를 수행했습니다.

 

1. 함수(클래스) 분리: MVC 패턴

처음으로 작업한 기능 그룹은 Constants입니다. MESSAGES 객체와 CONDITIONS 객체를 생성하여 추후에 쓰일 값들을 상수로 구현했습니다. 

 

다음으로 작업한 기능 그룹은 Models입니다. Race 모델Car 모델을 생성했습니다. Race 모델은 경주에 참여하는 자동차 목록(racingCars)과 총 시행되는 라운드(시도) 횟수(totalRound)를 property로 갖습니다. Car 모델은 자동차의 이름(name)과 누적 전진 횟수(numberOfAdvance)를 property로 갖습니다. 모든 property는 private으로 설정하여, getter 함수로 해당 데이터를 가져오도록 구현했습니다. 특히, Car 모델에서는 전진 횟수를 증가시키는 advance함수도 구현했습니다.

 

다음으로 작업한 기능 그룹은 Views입니다. InputView 클래스OutputView 클래스로 나누어 구현했습니다. InputView에는 경주 자동차를 입력받는 함수 askCarNames, 총 라운드(시도) 횟수를 입력받는 함수 askTotalRound를 구현했습니다. OutputView에는 실행 결과 제목을 출력하는 함수 printResultTitle, 라운드별 경주 실행 결과를 출력하는 함수 printProcedureOfRace, 우승자를 출력하는 함수 printWinners를 구현했습니다.

 

마지막으로 작업한 기능 그룹은 Controllers입니다. raceGameController 클래스를 생성하여 프로그램의 핵심 로직을 구현했습니다. Models과 Views에 있는 클래스/함수들을 활용하여, 처음부터 끝까지의 게임 로직을 담당한 함수 run을 구현했습니다. 또한, 자동차 이름을 총 3가지의 기준으로 유효성 검사한 함수 isValidCarNames, 라운드(시도) 횟수를 총 2가지의 기준으로 유효성 검사한 함수 isValidTotalRound를 구현했습니다. 경주 자동차의 전진과 관련하여, 무작위 값을 선택하는 함수 selectRandomNumber, 전진 가능 여부를 확인하는 함수 canCarAdvance, 각각의 자동차가 가지는 전진 횟수를 라운드별로 업데이트하는 함수 updateNumberOfAdvance를 구현했습니다. 마지막으로 우승자를 연산하는 함수 calculateWinners함수를 구현했습니다.

 

2. 테스트 코드 작성

테스트 코드를 작성하기 위해서는 jest에 대한 이해가 필요했습니다.

따라서, 기존에 주어진 2개의 테스트 파일 중 학습할 내용이 더 있는 ApplicationTest.js 파일을 분석했습니다.

 

 

ApplicationTest.js

 

프로그램이 실행될 때마다 값이 바뀌는 함수나 기능은 Mock function으로 설정하여 테스트가 진행됐습니다.

 

구체적으로, mockQuestions 함수는 두 번의 입력(자동차 이름 목록, 시도 횟수)을 받는 MissionUtils.Console.ReadLineAsync 함수를 Mock funciton으로 설정했습니다. 그리고, inputs 배열의 input 값을 순서대로 해당 함수의 값으로 반환하도록 구현됐습니다.

const mockQuestions = (inputs) => {
  // 입력값을 받는 함수를 Mock Function으로 설정
  MissionUtils.Console.readLineAsync = jest.fn();
 
  // 입력값을 순차적으로 반환하도록 설정 (shift() 함수를 이용)
  MissionUtils.Console.readLineAsync.mockImplementation(() => {
    const input = inputs.shift();
    return Promise.resolve(input);
  });
};

 

다음으로, mockRandoms 함수는 자동차 수만큼 실행되는 MissionUtils.Random.pickNumberInRange 함수를 Mock function으로 설정했습니다. 이 함수는 각 자동차의 전진 여부를 결정짓는 임의의 값을 reduce 함수를 활용하여 반환합니다. mockReturnValueOnce() 함수는 메서드 체이닝으로 연결될 수 있기에, reduce 함수를 사용한 것으로 보입니다.

const mockRandoms = (numbers) => {
  // 랜덤값을 반환하는 함수를 Mock Function으로 설정
  MissionUtils.Random.pickNumberInRange = jest.fn();
  
  // 랜덤값을 순차적으로 반환하도록 설정 (reduce() 함수를 이용)
  numbers.reduce((acc, number) => {
    return acc.mockReturnValueOnce(number);
  }, MissionUtils.Random.pickNumberInRange);
};

 

마지막으로, getLogSpy 함수는 출력값을 받는 MissionUtils.Console.print 함수를 Mock function으로 설정했습니다. 이 함수는 print 함수로 출력되는 모든 내용들을 추적하기 위해 구현됐습니다.

const getLogSpy = () => {
  // 출력값을 받는 함수를 Mock Function으로 설정
  const logSpy = jest.spyOn(MissionUtils.Console, "print");
  // Mock Function을 초기화
  logSpy.mockClear();
  return logSpy;
};

 

ApplicationTest에서 총 두 가지 테스트가 진행됐습니다. 첫 번째는 전진-정지 테스트로, 위 함수들을 활용해 전진 횟수를 직접 설정하고, expecttoHaveBeenCalledWith 함수를 통해 실행 결과에 예상하는 출력값이 포함되어 있는지 확인했습니다. 두 번째는 이름에 대한 예외 처리 테스트로, 특정 입력값에 대해 reject 함수를 실행시켜 에러 메세지 [ERROR]를 잘 포함하고 있는지 확인했습니다.

describe("자동차 경주 게임", () => {
  // 테스트 1번
  test("전진-정지", async () => {
    // given
    const MOVING_FORWARD = 4;
    const STOP = 3;
    const inputs = ["pobi,woni", "1"];
    const outputs = ["pobi : -"];
    const randoms = [MOVING_FORWARD, STOP];
    const logSpy = getLogSpy();
    
    mockQuestions(inputs);
    mockRandoms([...randoms]);

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

    // then
    outputs.forEach((output) => {
      expect(logSpy).toHaveBeenCalledWith(expect.stringContaining(output));
    });
  });

  // 테스트 2번
  test.each([
    [["pobi,javaji"]],
    [["pobi,eastjun"]]
  ])("이름에 대한 예외 처리", async (inputs) => {
    // given
    mockQuestions(inputs);

    // when
    const app = new App();

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

 

 

InputViewTest / OutputViewTest

 

이렇게 테스트 파일을 분석한 후, InputViewTestOutputViewTest를 구현했습니다. InputViewTest에서는 이름의 길이, 이름의 공백 포함 여부, 이름의 중복 여부로  자동차 이름의 입력값 유효성 테스트를 구현했고, OutputViewTest에서는 라운드별 실행 결과와 우승자 출력 테스트를 구현했습니다.

// InputViewTest.js
describe("자동차 이름 유효성 검사", () => {
  test.each([
    [["david,sonata,abc"]],
    [[",science"]],
  ])("이름의 길이가 1~5자리인지", async (inputs) => {
    // given
    mockQuestions(inputs);

    // when
    const app = new App();

    // then
    await expect(app.play()).rejects.toThrow(MESSAGES.CAR_NAME_INPUT_ERROR_LENGTH);
  });

  test.each([
    [["bo i, SSa , se l"]],
    [[" yaho,js "]]
  ])("이름에 공백이 없는지", async (inputs) => {
    // given
    mockQuestions(inputs);

    // when
    const app = new App();

    // then
    await expect(app.play()).rejects.toThrow(MESSAGES.CAR_NAME_INPUT_ERROR_BLANK);
  });

  test.each([
    [["po,pori,ri,po"]],
    [["ab,bc,cd,de,cd"]]
  ])("이름이 중복되지 않는지", async (inputs) => {
    // given
    mockQuestions(inputs);

    // when
    const app = new App();

    // then
    await expect(app.play()).rejects.toThrow(MESSAGES.CAR_NAME_INPUT_ERROR_DUPLICATION);
  });
});

 

// OutputViewTest.js
describe("게임 결과 출력 테스트", () => {
  test("라운드의 결과 및 우승자 출력", async () => {
    // given
    const MOVING_FORWARD = 5;
    const STOP = 3;
    const inputs = ["pobi,woni", "2"];
    const outputs = ["pobi : -", "pobi : --", "woni : ", "최종 우승자 : pobi"];
    const randoms = [MOVING_FORWARD, STOP, MOVING_FORWARD, STOP];
    const logSpy = getLogSpy();

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

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

    // then
    outputs.forEach((output) => {
      expect(logSpy).toHaveBeenCalledWith(expect.stringContaining(output));
    });
  });
});

마무리

1. 이번 과제에서 MVC 패턴으로 함수/클래스들을 구현하다 보니, 더 효율적으로 작업을 할 수 있었던 것 같습니다. 다만, 기능 그룹별(MVC)로 폴더를 나누어서 작업하는데, 굳이 브랜치까지 나누어서 작업할 필요가 있나 하는 생각을 했습니다. 다음 과제부터는 develop 브랜치에서 계속 작업하다가, 가끔 feature 규모가 커져 따로 작업을 해야될 때만 feature 브랜치를 파서 작업해도 될 것 같다는 생각을 했습니다. 즉, 불필요한 feature 브랜치 생성은 지양하는 것이 나을 것 같다는 생각을 했습니다. 이러는 편이 혼자 작업할 때는 더 빠르고 효율적일 것 같다고 느꼈기 때문입니다. 

 

2. 1주차 과제 때부터 함수를 분리하여 구현했기에 이번 과제에서도 좀 더 수월하게 작업할 수 있었지만, README.md에서 전체적인 MVC패턴단시간 내에 작성하기에는 아직 부족한 단계라 많이 연습하고 노력해야겠다는 생각을 했습니다.

 

3. 함수/클래스 이름을 명명하는 것도 저만의 방식으로 패턴화시켜, 직관적으로 이해할 수 있는 수준의 이름을 바로바로 짓는 연습도 해야겠다고 생각했습니다.

 

4. 앞으로의 과제들에서는 테스트 코드를 보다 더 세분화하여 작성해야겠다고 생각했습니다. 특히, jest 관련 테스트 코드는 깊게 학습해본 것이 처음인지라, 이번 과제를 꼼꼼히 회고하면서 다음 과제에 보다 더 다양하고 구체적인 테스트 코드들을 구현하고자 합니다.

 

5. rebase를 사용할 때는 이전에 다른 브랜치와의 병합이 이루어지지 않은 독립적인 브랜치에서만 사용하도록 주의해야겠다는 생각을 했습니다. 안그러면 Git 꼬임 현상이 발생할 확률이 높아진다는 것을 몸소 확인했기 때문입니다.

 

2주차 과제 기간 동안에도 빠른 성장을 할 수 있어서 감사하고 뿌듯했고, 다음 과제들에서도 의미있는 학습 경험을 통해 즐겁게 성장하고 싶다는 생각을 했습니다.

 

 

출처:

https://jestjs.io/docs/mock-function-api#mockfnmockclear