main

우테코 프리코스 2주차(자동차 경주)

Log
33

1주차를 마지고 난 뒤, 많은 분들의 코드를 보고 리뷰도 해보면서 소위말해.. 현타를 조금 느꼈습니다.😭
잘하시는 분들이 너무 많더라고요... 그리고 제가 나름 스스로 적용해보았던 MVC패턴이니.. OOP니.. 제대로 적용한 게 아니라는 걸 다른 분들의 코드를 보고 뽝! 느낄 수 있었습니다. 그래도 선방했다고 느꼈던 건, 혼자 jest를 공부해보고 테스트코드를 짜보았다는 것이었습니다. 그리고 클래스 문법을 써서 제대로 메서드를 사용해봤다는 것만으로도 그래도.. 많이 적응했다 ^^...

그래서 저번 1주차 코스를 마무리하고, 제 자신 스스로에게 아쉬웠던 점과 그리고 앞으로 2주차에서도 적용해봐야할 것에 대해 종합적으로 정리해보았습니다.


📌 아쉬웠던 점과 스스로 개선해나가야할 점

  • 노드버전을 맞춰주기
    • 다른 분들의 코드를 보니, .nmvrc 파일이나 package.jsonengine 속성을 추가해서 노드 버전을 맞춰주는 것을 보아, 저도 적용하는것이 좋겠다는 생각이 들었어요.
  • 상수 파일 구분하기
    • 상수 파일을 constant 폴더로 빼서 관리하고, Object.freezeseal 메서드를 사용해서 관리하기
  • eslint, prettier 적용하기
    • 적용해야겠어요.. 물론 이번 2주차 요구사항이긴했지만..
  • MVC 패턴을 적용해 로직 구분 및 코드스플리팅
    • 저는 그냥 변수는 Model이고 print하는 부분들은 view, 나머지 로직처리는 controller라고 생각했는데, 그게 아니라 모두 폴더기준으로 나눠 로직을 정리하고, 각각의 class만들어 구분하는 것이 정확한 로직 구분이라고 생각이 들었습니다.


📌 1주차 피드백 살펴보기 및 해석

나눠주셨던 1주차 피드백을 보고 느낀점과 저만의 해석을 적어볼까합니다.
나머지 피드백들은 그렇구나! 했는데 가장 눈에 돋보였던 것들은
EOL부분과 JS API적극 활용이었습니다.

  • EOL
    • 최종 제출하는 코드에서 EOL을 확인한다. 환경에 따라 의도한 바와 다르게 개행 문자 처리가 되지 않도록 EOL 설정을 확인한다.
  • JS API 적극 활용
    • 함수(메서드)를 직접 구현하기 전에 JavaScript API에서 제공하는 기능인지 검색을 먼저 해본다.
      JavaScript API에서 제공하지 않을 경우에 직접 구현한다.

이 EOL 관련해서는 아래에 더 추가적으로 해석을 적으려구요. Prettier에서 EOL관련 설정을 해본적은 있는데 솔직하게 말하면 쓰면서 어떤 설정인지 제대로 파악해본 적이 없었어서..🥲
그리고 for문과 forEach메서드 중에서 어떤 걸 써야하냐?! 고 생각했었는데, 확실하게 우테코에서 주신 공통피드백을 보고 forEach를 쓰는게 맞겠다고 확신했습니다. 예시로는 배열의 join메서드를 내주셨는데, split이나 등등 메서드를 사용해서 가독성을 높힐 수 있다면 사용해라 라는 의미 같았습니다.
즉, 가독성 높혀라...

What is EOL?

아래 포스팅에 EOL에 대해서 확인하실 수 있습니다!😊
End of Line이란? (LF, CRLF) 차이



📌 2주차 프로그래밍 요구 사항, 과제 진행 요구 사항

기능 요구 사항은 기능 목록을 작성할 때, 더 자세히 살펴봐야하므로 먼저 저는 프로그래밍 요구사항와 과제 진행 요구 사항에 대해서 파악해볼까합니다.


프로그래밍 요구사항

노드 버전이나 자바스크립트를 활용해서 코드를 적는다는 점, 테스트 모두 통과해아하고 하는 등 전반적인 건 이전과 같았습니다.
추가된 것은 아래 2개의 사항이었습니다.

  • indent(인덴트, 들여쓰기) depth를 3이 넘지 않도록 구현한다. 2까지만 허용한다.
    • 예를 들어 while문 안에 if문이 있으면 들여쓰기는 2이다.
    • 힌트: indent(인덴트, 들여쓰기) depth를 줄이는 좋은 방법은 함수(또는 메소드)를 분리하면 된다.
  • Jest를 이용하여 본인이 정리한 기능 목록이 정상 동작함을 테스트 코드로 확인한다.
    • 테스트 도구 사용법이 익숙하지 않다면 __tests__/StringTest.js를 참고하여 학습한 후 테스트를 구현한다.

그리고 이번에는 명시적으로 이번에 우테코 라이브러리 중에 이 메서드를 사용해! 라고 적어주셨네요. 사실 Random 메서드중에서 어떤거 써야대나.. 꽤나 좀 했었기때문에 😂 감사했습니다..

  • Random 값 추출은 Random.pickNumberInRange()를 활용한다.
    사용자의 값을 입력 받고 출력하기 위해서는 Console.readLineAsync, Console.print를 활용한다.

과제 진행 요구 사항

  • 기능을 구현하기 전 docs/README.md에 구현할 기능 목록을 정리해 추가한다.
  • Git의 커밋 단위는 앞 단계에서 docs/README.md에 정리한 기능 목록 단위로 추가한다.
    • 커밋 메시지 컨벤션 가이드를 참고해 커밋 메시지를 작성한다.

이번에는 README.md뿐만 아니라 기능목록 단위마다 커밋을 의미있게 넣어라! 라고 이야기하는 것 같았어요.
그래서 저번에도 적용은 했었지만, 커밋컨벤션 적용해야할 것 같다고 생각했고 이번에는 브렌치컨벤션도 넣으면 좋겠다! 라고 생각했습니다.



📌 기능목록 파악 도입 이전! TodoList

일단 제가 뭐부터 해야하는지 TodoList부터 적기로 했어요.

✏️ TodoList

  • node 버전 맞추기
    • pacakge.json에 다음과 같은 코드를 추가해준다.
      "engines": {
      "npm": ">=9.6.7",
      "node": ">=18.17.1"
    }
  • 컨벤션 작성
    • docs폴더 내부에 Convention.md 파일을 생성해준다.
    • 코드 컨벤션, 커밋컨벤션, 브렌치 컨번션을 관련 내용을 작성한다.
  • eslint, prettier 세팅
    • package.json을 .gitignore에 추가한다.
    • EOL은 auto로 설정한다.
    • indent는 2까지만 허용해준다.
    // eslintrc에 rules에 아래의 코드를 추가해준다.
    // indent 조건을 지키기 위해 다음 라인을 추가
    "max-depth": ["error", 2],
    // Console.log 코드제거
    "no-console": "warn
    • 이외에 커스텀으로 eslint를 설정해준다.
    • prettier 설정을 해준다.
  • 폴더 구조
    • src > model, controller, view 를 만들어준다.
    • src > constants, utils도 만들어준다.
  • 기능 목록 작성
  • 기능 목록을 커밋 단위를 고려해 리스트를 정리한다.
  • 목록은 docs 폴더 READ.ME에 정리한다.
  • 기능 목록에 맞게 커밋하며 코드를 구현한다.
    • 모든 컨벤션을 준수한다.

지금부터의 글 내용은 TodoList 순서에 기반하여 작성되었습니다.


📌 컨벤션 작성

컨벤션은 아래와 같이 기입해주었습니다.

커밋 컨벤션

  • feat : 새로운 기능 추가
  • fix : 버그/오타(typo)/로직 등 코드를 수정한 경우
  • refactor: 코드 리팩토링
  • style : 코드 포맷팅, 세미콜론 누락 수정 등 내부 로직 변경이 없이 코드를 수정한 경우
  • docs : README 문서 및 MD 파일 수정
  • test : 테스트 코드, 리팩토링 테스트 코드 추가
  • chore : 빌드 업무 수정, 패키지 매니저 수정
  • remove : 코드/파일 삭제
  • dep : 패키지 설치/삭제 등 의존성 관련 수정
  • etc : 기타

코드 컨벤션

기본적으로 지켜야할 규칙

  • 소스의 변수명, 클래스명 등에는 영문 이외의 언어를 사용하지 않는다.
  • 클래스, 메서드 등의 이름에는 특수 문자를 사용하지 않는다.
  • 상수명은 SNAKE_CASE로 작성한다.
  • indent(인덴트, 들여쓰기) depth를 3이 넘지 않도록 구현한다. 2까지만 허용한다.
  • Random 값 추출은 Random.pickNumberInRange()를 활용한다.
  • 사용자의 값을 입력 받고 출력하기 위해서는 Console.readLineAsync, Console.print를 활용한다.
  • 축약하지 않고 이름을 통해 의도를 드러내야한다.
  • 공백 라인을 의미 있게 사용한다
  • EOL(End Of Line)
  • JavaScript에서 제공하는 API를 적극 활용한다
  • 불필요한 console.log를 남기지 않는다

스스로 추가한 규칙

  • MVC 컴포넌트들은 대문자로 시작한다.
  • 이외에 utilsvalidate하는 함수들은 모두 소문자로 시작한다.
  • 함수는 모두 동사로 시작한다.
  • boolean 값을 리턴하거나 관련된 함수는 모두 is~~로 시작되게 함수명을 짓는다.

브렌치 컨벤션

  • feat: 기능 추가
  • refactor: 리팩토링
  • fix: 에러 수정
  • remove: 코드 제거

브렌치명 뒤에는 -을 추가하여 상세히 브렌치명을 따서 사용한다.
ex. feat-stringModel


📌 eslint, prettier 설정, 폴더구조

eslint, prettier 설정을 하기 이전에, pacakge.json을 직접적으로 손을 대면 안되는데, 어떻게 설정을 해야하려나? 생각이 들었어요. 많은 서칭과.. 생각을 해본결과 .gitignorepacakge.json을 추가해주면 되겠다! 라는 생각이 들었습니다. 일단 설정관련해서는 너무 포스팅이 길어질 것 같아서, 따로 포스팅을 옮겨두었습니다.

폴더구조는 TodoList 적은 바와 같이 똑같이 만들어주었습니다.


📌 기능 목록 리스트

기능 목록 리스트는 아래 링크에서 확인하실 수 있습니다.😊
생각보다 길어서, 그냥 링크로 첨부합니다 허허
기능 목록 리스트


📌 기능구현하기

TodoList에 맞춰 모든 작업을 끝내고, 본격적으로 기능 구현을 시작하였습니다.
구현에는 총 이틀정도 걸렸습니다. (테스트 코드 제외)
이전에는 App.js 하나의 파일에서 작업했기 때문에 구현 로그 작성이 편했는데, 이번에는 MVC 패턴 적용, 상수관리, utils 관리를 합쳐서 하다보니 제가 전체적으로 구현했던 내용을 적기가 쉽지 않네요.
먼저 제가 구현한 레포 링크를 공유하고, 다른 우테코에 참석하시는 분들이 주셨던 피드백을 최대한 반영하려고 했다는 것 위주로 포스팅해볼까해요.
자동차경주 구현 레포
레포에서 ddaeunbb 브렌치에서 구현 코드를 보실 수 있습니다.😉


📌 기능 구현에 반영하고자 했던 것

사실 저는 1주차 때 다른분들의 레포에 리뷰를 남기면서 배우는 것 밖에 없었습니다. 리뷰를 남기더라도 거의 다 배우고갑니다.. 배우고 갑니다... 🙏🏻만 남길 수 있었던 것 같아요.🥲
뭔가 다른분들과 함께 리뷰하고 코드를 보러가는 것이 일종의 몰입을 유도한게 아닐까? 란 생각이 들었어요. 또 다른 사람들의 코드를 보면서 함께 성장해라! 라는 의미도 내포되어있다고 느꼈습니다.


MVC 패턴
많은 분들이 레포를 보면서 MVC패턴을 정말 깔끔하게 잘 적용하신 분들이나 로직을 잘 구분해서 코드가 간결하고 깔끔한 분들의 레포를 2~3개정도만 찾아 골라 정했습니다. 그리고 어떻게 구현하셨는지 코드를 다 까봤고, 제 코드에도 2주차에 적용하면 좋을 것 같다는 생각에 많이 참고를 하고 적용을 해보았어요.

무조건적으로 그분의 코드를 가져다가 사용한 것은 아니었고, 아 어떻게 구현하셨구나 정도만 파악하고 제 코드로 스스로 고민하면서 반영하였습니다. 배우기위해선 항상 레퍼런스가 있어야하기 때문이죠. 물론 2주차 과제 코드를 참고한게 아니라 1주차때 구현하셨던 MVC 패턴을 뜯어보았습니다. 절대 다른분들의 2주차 코드를 치팅!하지 않았어요.


코드 분리
다른 분들이 제 코드를 리뷰해주시면서, 가장 많이 보았던 것이 코드분리 였습니다.. 상수나 여러 로직들을 utils로 사용해서 구분하시는게 어떻겠냐? 라는 말씀을 많이 주셔서 특히나 이번에는 상수, 코드분리를 미친듯이 했습니다. 아래는 분리했던 상수 코드입니다. 출력예시문도 따로 다른 파일에 구분해두었어요.

  • src > constants > constants.js
export const REGEXP = /^[a-z]+$/;
export const CARNAME_LENGTH = 5;
 
export const SPLIT_STANDARD = ',';
export const JOIN_STANDARD = ', ';
 
export const FIRSTNUM = 0;
export const LASTNUM = 9;
 
export const MOVE_NUM = 4;
export const MOVE_STR = '-';

이외에도 정말 많은 코드 리뷰 절반 이상..거의 70~80%가 코드 로직 분리해라..! 라는 말이 많으셨어서, 이번에는 최대한 빼낼 수 있는 로직들을 모두 빼서 정리하려고 한 것 같아요.

utils 폴더에는 typeConvertor.js 파일을 만들어 아래처럼 로직 구분도 하였습니다.

import { SPLIT_STANDARD } from '../constants/constants';
 
export const strToStrArr = (str) => str.split(SPLIT_STANDARD);
 
export const strToNum = (str) => +str;
 
export const strArrTostr = (strArr) => strArr.join(', ')

이외에도 많은 부분들을 로직 구분하였읍니다..


중복 비교부분 코드
이번에도 중복 비교를 하는 코드가 필요하더라구요. 이전에 1주차에서는 아래와 같이 set을 사용하고, 다시 배열로 변경하여 비교하는 로직을 짰었어요.

isUnique: (input) => {
        const uniqueArr = [...new Set([...input])];
            return input.length === uniqueArr.length;
      },

많은 분들 레포를 보니 setsize 메서드를 써서 구현하셨고, 또한 저도 제 코드 피드백으로 적어주신게 있어서 이번에는 아래와 같이 구현하였습니다.

export const isUnique = (strArr) => {
  const uniqueArr = new Set(strArr);
  const result = strArr.length === uniqueArr.size;
  if(!result) throw new Error(ERROR_MESSAGE.UNIQUE);
}

📌 리팩토링

컨트롤러 로직 분리

코드를 다 적고 나서 웬만한 코드는 다 분리를 하거나 최소한의 길이로 코드를 적었다고 생각했는데, MVC패턴을 같이 적용하면서 MVC패턴의 특징을 몸소 체험할 수 있었습니다.ㅎㅎ.. 바로 컨트롤러의 코드가 길어진다는 것!
View,나 Model부분은 코드가 짧은데 Controller 부분의 코드가 길어지긴하더라고요.
가장 길었던 부분은 이 파트였습니다.

 moveCars(){
    const totalMove = this.#moveCount.getCount(); // 게임의 횟수를 가져오고
    const players = this.#carPlayers.getPlayers(); // player의 배열을 가져오고 (문자열[])
    const result = this.#gameResult.getResult(); // 게임 결과를 담는 배열을 가져온다. (빈배열[])
    this.#outputView.printGameStart(); // 실행 결과 문구 출력
    for(let i = 1; i <= totalMove; i+=1){ // 게임 횟수만큼
      players.forEach((player, idx) => { // 플레이어마다
        this.#gameResult.moveCarsResult(idx); // 랜덤난수를 뽑고 전진할지말지를 실행한다.
        this.#outputView.printGameResult(player, result[idx]);  // 그다음 게임 결과를 출력한다.
      })
      Console.print('\n'); // 게임 결과 끝에 linebreak를 해준다.
    }
  }

이부분 코드가 가장 길어서, 일단 코드 가독성이 좋지 않은 for문보다 forEach처럼 배열의 메서드를 사용할 수 없을까란 생각이 들었어요. 그리고 이중 for문을 돌고 있어서 이를 분리해서 코드를 작성해야겠다고 생각했습니다. 메서드명도 확실하게 의도를 담지 못한다고 생각해서 로직을 아래와 같이 구분하였습니다.

 
  playCountGames(){
    let totalMove = this.#moveCount.getCount(); // 게임 실행 횟수 가져와서
    this.#outputView.printGameStart(); // 실행 결과 문구 출력
    while(totalMove > 0){ // 실행 횟수가 0될때까지
      this.moveEachCars(); // 각각의 차들을 움직이겠다.
      totalMove -= 1; // 다돌면 실행횟수 빼준다.
    }
  }
 
  moveEachCars(){ 
    const players = this.#carPlayers.getPlayers(); // 플레이어 리스트 가져온다 (문자열[])
    const result = this.#gameResult.getResult(); // 게임결과를 담을 빈배열가져오기
    players.forEach((player, idx) => { // 플레이어마다 돌면서
      this.#gameResult.moveCarsResult(idx); // 난수를 뽑아 움직인다
      this.#outputView.printGameResult(player, result[idx]); // 그리고 그 결과를 출력한다.
    })
    this.#outputView.printLineBreak(); // 줄바꿈을 출력한다.
  }

확실하게 로직을 두개로 나누니까 의도가 더 명확해진 것 같다고 느꼈습니다. 코드도 더 이해하기 쉬워진 것 같아요.
Console.print('\n');로 적었던 부분도 아래 로직으로 나눠 관리했습니다.

// OutputView.js
  printLineBreak(){
    Console.print('\n');
    }

지금 보니까 줄바꿈도 상수로 관리해야겠네요 ^_^


피드백 반영하기 (중복인지 확인하는 코드)

이부분은 아까 위에서 적었어서 코드만 적겠습니다!

// 수정 이전
export const isUnique = (strArr) => {
  const uniqueArr = [...new Set(strArr)];
  const result = strArr.length === uniqueArr.length;
  if(!result) throw new Error(ERROR_MESSAGE.UNIQUE);
}
 
// 수정 이후
export const isUnique = (strArr) => {
  const uniqueArr = new Set(strArr);
  const result = strArr.length === uniqueArr.size;
  if(!result) throw new Error(ERROR_MESSAGE.UNIQUE);
}

에러났던 부분 수정하기

자꾸 테스트를 돌리면 난수까지 돌리고 결과를 반영하는데까지는 정상적으로 작동하는데, 우승자를 뽑는데에서 자꾸 그 사람의 이동 결과를 나타내더라고요.
예를들면 최종우승자 : daeun 이런식이여야하는데, 최종우승자 : - 이런식으로...
그래서 이상한 부분이 있다고 확인하고 코드를 수정하게 되었습니다.

export const findSameLenElement = (strArr, len) => {
  const result = strArr.filter(str => str.length === len);
  return result;
}

위에 코드를 보면, strArr은 ['--', '-'] 이런 게임 결과값을 담은 배열이고, 이 배열 중에서 가장 긴 문자열 길이를 담은 값입니다. 예시에서 라면 len은 2로 들어올거예요.
근데 코드를 보면, filter를 사용하였고, 결국 result'--'만 담기게 될테니까 로직을 잘못짠거더라고요.
그래서 아래처럼 수정해주었습니다.

export const findSameLenPlayer = (strArr, players, len) => {
  const result = [];
  strArr.forEach((str, idx)=> {
    if(str.length === len) result.push(players[idx]);
  })
  return result;
}

원래 findSameLenElement였는데 의도가 명확치 않다고 판단해서 findSameLenPlayer으로 변수명을 수정해주었습니다. 그리고 나서 players라는 매개변수를 더 추가해주었어요. player는 차 이름들이 담긴 배열입니다.
따라서 배열을 돌면서 길이가 같으면 player의 이름을 result라는 배열에 추가해주었습니다.



📌 테스트 코드 작성

테스트 코드는 아래와 같이 작성하였습니다.

  • InputViewTest.js

    • 경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분) 문구가 출력되면서 입력을 받는지 확인
    • 시도할 횟수는 몇 회인가요? 문구가 출력되면서 입력을 받는지 확인
    • 자동차 이름에 대한 검증 테스트가 잘 이뤄지는지
    • 시도할 횟수에 대한 검증 테스트가 잘 이뤄지는지
  • OutputViewTest.js

    • 실행 결과 문구가 출력되는지
    • 플레이어 별 결과 값 출력되는지
    • 최종 우승자 : 문구와 함께 최종 우승자를 출력하는지 테스트'

    테스트 코드는 아래와 같습니다.


InputViewTest.js

import { Console } from '@woowacourse/mission-utils';
import App from '../src/App';
import { INPUT_MESSAGE } from '../src/constants/message';
import { ERROR_MESSAGE } from '../src/constants/message';
 
const mockConsoleFn = (input) => {
  Console.readLineAsync = jest.fn();
  Console.readLineAsync.mockImplementation(()=> input);
}
 
const mockMultipleConsole = (input1, input2) => {
  Console.readLineAsync = jest.fn()
    .mockImplementationOnce(()=> input1)
    .mockImplementationOnce(()=> input2);
}
 
const getLogSpy = () => {
  const logSpy = jest.spyOn(Console, "readLineAsync");
  logSpy.mockClear();
  return logSpy;
};
 
describe('자동차 경주 입력 문구 출력 테스트', () => {
  test('자동차 이름을 입력하세요. 문구를 출력하는지 테스트', async () => {
    const app = new App();
    mockMultipleConsole('phobi,daeun', '3');
    const logspy = getLogSpy();
    await app.play();
    expect(logspy).toHaveBeenCalledWith(INPUT_MESSAGE.PLAYER_CARS);
  })
 
 
  test('시도할 횟수를 입력받는 문구를 출력하는지 테스트', async () => {
    const app = new App();
    mockMultipleConsole('phobi,daeun', '3');
    const logspy = getLogSpy();
    await app.play();
    expect(logspy).toHaveBeenCalledWith(INPUT_MESSAGE.MOVE_COUNTS);
  })
})
 
describe('자동차 이름 입력 예외 테스트', () => {
 
  test('자동차는 소문자 영어만 입력가능합니다.', async () => {
    const app = new App();
    mockConsoleFn('Daeun,James');
    await expect(()=> app.play()).rejects.toThrow(ERROR_MESSAGE.LOWERCASE);
  })
 
  test('5글자 이하의 영어만 입력가능합니다.', async () => {
    const app = new App();
    mockConsoleFn('android,ios');
    await expect(()=> app.play()).rejects.toThrow(ERROR_MESSAGE.LENFIVE);
  })
 
  test('차 이름은 중복될 수 없습니다.', async () => {
    const app = new App();
    mockConsoleFn('daeun,daeun');
    await expect(()=> app.play()).rejects.toThrow(ERROR_MESSAGE.UNIQUE);
  })
})
 
describe('시도할 횟수 입력 예외 테스트', () => {
  test('문자는 입력할 수 없습니다.', async () => {
    const app = new App();
    mockMultipleConsole('phobi,daeun', 'ten-times');
    await expect(()=> app.play()).rejects.toThrow(ERROR_MESSAGE.NUMBER);
  })
 
  test('문자는 입력할 수 없습니다.', async () => {
    const app = new App();
    mockMultipleConsole('phobi,daeun', 'ten-times');
    await expect(()=> app.play()).rejects.toThrow(ERROR_MESSAGE.NUMBER);
  })
 
  test('정수만 입력할 수 있습니다.', async () => {
    const app = new App();
    mockMultipleConsole('phobi,daeun', '9.5');
    await expect(()=> app.play()).rejects.toThrow(ERROR_MESSAGE.INT);
  })
 
  test('음수는 입력할 수 없습니다.', async () => {
    const app = new App();
    mockMultipleConsole('phobi,daeun', '-13');
    await expect(()=> app.play()).rejects.toThrow(ERROR_MESSAGE.NEGATIVE);
  })
})

OutputViewTest.js

import { Console } from '@woowacourse/mission-utils';
import App from '../src/App';
import { OUTPUT_MESSAGE } from '../src/constants/message';
 
const mockMultipleConsole = (input1, input2) => {
  Console.readLineAsync = jest.fn()
    .mockImplementationOnce(()=> input1)
    .mockImplementationOnce(()=> input2);
}
 
const getLogSpy = () => {
  const logSpy = jest.spyOn(Console, "print");
  logSpy.mockClear();
  return logSpy;
};
 
describe('자동차 경주 콘솔 테스트', () => {
  test('실행 결과가 출력되는지 테스트', async () => {
    const app = new App();
    mockMultipleConsole('phobi,daeun', '3')
    const logspy = getLogSpy();
    await app.play();
    expect(logspy).toHaveBeenCalledWith(OUTPUT_MESSAGE.START);
  })
 
  test('플레이어 별 결과 값 출력되는지 테스트', async () => {
    const app = new App();
    mockMultipleConsole('phobi,daeun', '3')
    const logspy = getLogSpy();
    await app.play();
    expect(logspy).toHaveBeenCalledWith(expect.stringContaining(OUTPUT_MESSAGE.RESULT));
  })
 
  test('최종 우승자를 출력하는지 테스트', async () => {
    const app = new App();
    mockMultipleConsole('phobi,daeun', '3')
    const logspy = getLogSpy();
    await app.play();
    expect(logspy).toHaveBeenCalledWith(expect.stringContaining(OUTPUT_MESSAGE.WINNER));
  })
})


📌 테스트 코드 작성하면서 추가했던 예외사항

테스트 코드를 작성하다보니 예외사항을 추가적으로 더 고려하게 되었습니다.
만약 차이름을 입력할 때, 공백을 하는 경우에 대한 처리도 필요했습니다.

export const REGEXP_BLANK = /\s/g;
 
export const isBlankIncluded = (str) => {
  if(REGEXP_BLANK.test(str)) throw new Error(ERROR_MESSAGE.BLANK);
}

그래서 위 코드와 같이 공백이 있는지 문자열을 검사하고, 있다면 에러를 처리하도록 validation을 추가하였습니다.

이외에도 테스트코드에서 추가해주었습니다.

describe('자동차 이름 입력 예외 테스트', () => {
  test('공백은 입력될 수 없습니다.', async () => {
    const app = new App();
    mockConsoleFn('daeun, james');
    await expect(()=> app.play()).rejects.toThrow(ERROR_MESSAGE.BLANK);
  })
  ...
 
})


📌 2주차를 진행하면서 느낀점

2주차를 진행하고 나서 느낀것이 있다면 그래도 2주동안 정말 많이 성장했다고 느꼈습니다. 우테코를 하기 이전이라면 class관련 해서 많이 무지했을텐데, 그래도 공부하다보니 클래스의 사용법에 대해서 많이 익힌 것 같습니다..
그리고 1주차때 테스트 코드 관련해서 공부해놓은게 정말로 다행이라고 생각들었어요. 나중에 테스트 코드를 짤 수도 있다는 이야기를 들었어서 미리 jest에 대해서 공부를 좀 해두었는데, 이번 2주차때는 그래도 쉽게 테스트 코드를 짤 수 있었던 것 같습니다.

그리고 MVC패턴에 대해서 저는 그냥 개념만 알고 있었는데요, MVC의 패턴의 단점 중 하나로 컨트롤러가 방대해질 수 있다고만 알고 있었습니다. 근데 제 코드로 처음 제대로 적용해보면서 실제로 뷰나 모델에 비해서 컨트롤러가 커지는걸 직접 실감할 수 있었습니다. 코드를 짜다보니 저절로 다른 부분들의 코드보다 컨트롤러의 코드들이 길어질 수 밖에 없는게 정말 신기했어요.

1주차 때 다른분들로부터 받았던 피드백을 기반으로 코드를 분리하고, 파일로 따로 구분하면서 확실히 가독성이 높아지는 것도 느꼈습니다. 확연히 1주차때와는 다른 느낌이 들더군요.. 2주차때는 다른분들의 코드를 더 열심히 보게 될 거라는 생각이 들어요. 뭔가 제 코드가 더 확연히 좋아지는 느낌이 드니, 3주차때는 다른 분들의 코드와 리뷰를 참고해 더 나아지고 싶다는 생각이 드네요. 역시 배우기 위해선 다른 분들의 코드를 많이 보는게 맞다는 생각을 많이 느꼈습니다.

1주차때 다른 분들의 코드를 까보기도하고, 복기도 해보고 다른분들과 피드백을 나누면서 같이 성장할 수 있었던 기회라고 생각해요. 확실히 서로 리뷰와 피드백을 하다보니 2주차 과정은 더욱 더 깊게 몰입할 수 있었던 것 같습니다.
3주차때는 또 제가 다른분들과 함께 어떤 성장을 할지 궁금합니다!😉


김다은 이모지
Daeun Kim
Junior Frontend Engineer