[우아한테크코스] 프리코스 2주차 회고

정균·2022년 11월 8일
0

우아한테크코스

목록 보기
2/15
post-thumbnail

2주차 미션 - 숫자 야구 게임

미션 레포지토리 보러가기

🚀 기능 요구 사항

기본적으로 1부터 9까지 서로 다른 수로 이루어진 3자리의 수를 맞추는 게임이다.

  • 같은 수가 같은 자리에 있으면 스트라이크, 다른 자리에 있으면 볼, 같은 수가 전혀 없으면 낫싱이란 힌트를 얻고, 그 힌트를 이용해서 먼저 상대방(컴퓨터)의 수를 맞추면 승리한다.
    • 예) 상대방(컴퓨터)의 수가 425일 때
      • 123을 제시한 경우 : 1스트라이크
      • 456을 제시한 경우 : 1볼 1스트라이크
      • 789를 제시한 경우 : 낫싱
  • 위 숫자 야구 게임에서 상대방의 역할을 컴퓨터가 한다. 컴퓨터는 1에서 9까지 서로 다른 임의의 수 3개를 선택한다. 게임 플레이어는 컴퓨터가 생각하고 있는 서로 다른 3개의 숫자를 입력하고, 컴퓨터는 입력한 숫자에 대한 결과를 출력한다.
  • 이 같은 과정을 반복해 컴퓨터가 선택한 3개의 숫자를 모두 맞히면 게임이 종료된다.
  • 게임을 종료한 후 게임을 다시 시작하거나 완전히 종료할 수 있다.
  • 사용자가 잘못된 값을 입력한 경우 throw문을 사용해 예외를 발생시킨후 애플리케이션은 종료되어야 한다.

추가된 프로그래밍 요구사항

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

🎯 2주차의 목표

이번 주차 미션에서 중점 둘 목표는 함수 분리테스트 코드 작성이다.

✏️ 2주차에서 배운점

1. 자바스크립트 클래스

1-1. 갑자기 클래스를 학습한 이유

이전에 자바를 배울 때는 클래스를 지겹도록 사용 했었지만, 자바스크립트에서 클래스는 사용 경험이 거의 없다.

물론 자바스크립트에서는 함수형 프로그래밍이 대세라지만 복잡한 구조의 프로젝트를 다루는 실무에서는 객체 지향 프로그래밍이 꽤 잦다고 한다.

아무튼 자바스크립트의 클래스는 당장 필요하진 않지만 언젠가 배워야 할 개념이라고 생각했다.

1주차 미션에서 다른 사람의 PR들을 보고..

지난 1주차 미션에서 다른 사람 PR을 보다가 종종 클래스로 짠 코드들이 눈에 띄었다.

객체 지향으로 멋들어지게 짠 코드를 보면서 나도 저렇게 짜보고 싶다고 생각했다.

그러다가 이번 2주차 미션에서 App.js가 클래스로 이루어져 있는걸 봤고, 이번 기회에 JS 클래스의 구조와 사용법을 제대로 배워봐야겠다고 느꼈다.

1-2. 클래스 학습 과정

먼저 자바스크립트 클래스의 사용법부터 학습했다.

기존에 자바로 클래스를 사용해봤다보니 배우는데 어렵지 않았고, 약간 자바의 클래스를 조금 더 단순화한 버전의 느낌이었다.

코어 자바스크립트 에서 클래스 사용법을 주로 학습했다.

그 다음으로는 자바스크립트 클래스의 특징을 학습했다.

자바스크립트의 클래스는 ES6부터 도입되었으며, 기존의 프로토타입의 상속과 생성자 함수을 활용한 문법적 설탕(syntactic sugar)라고 한다.

솔직히 말하면 프로토타입은 자주 들어보긴 했지만 구체적인 개념은 잘 알지 못했다.

프로토타입은 클래스와는 또 다른 OOP의 방식이며, 자바스크립트의 상속에 대한 핵심적인 개념이다.

프로토타입에 대한 개념이 명확하게 이해가지 않았는데, 프리코스 커뮤니티에서 어떤 분이 프로토타입에 대한 글을 추천해주셨고, 한줄 한줄 정독했다.

해당 글은 내가 프로토타입을 이해하는데 큰 도움을 받았고, 특히 철학적 개념에 빗대어 클래스와 프로토타입을 설명하는 부분에서 큰 깨달음을 얻었다.

1-3. 느낀점

클래스의 사용법을 잠깐 공부하려고 했는데 어쩌다보니 프로토타입의 원리까지 학습했다.

그렇다보니 처음 이틀동안은 거의 미션을 진행하지 못했다.

사실 이틀 동안 공부하면 할수록 내 자신이 너무 부끄러웠다.

이제까지 자바스크립트의 기본인 클래스와 프로토타입도 제대로 모르면서 새로운 라이브러리, 프레임워크 학습에만 중점을 뒀었다.

물론 새로운 기술도 중요하지만 뛰어난 프론트엔드 개발자가 되기 위해선 자바스크립트의 핵심을 단단하게 하는 것이 중요하다고 느꼈다.

아직은 짧은 시간동안 클래스와 프로토타입를 겉핥기만 한 정도지만, 더욱더 열심히 학습해서 자바스크립트라는 언어를 나의 것으로 만들어야 앞으로의 성장에도 큰 도움이 될 것이라고 생각했다.

2. 테스트와 TDD

2-1. TDD

이번 코수타(코치와 수다 타임)에서 TDD 방식에 대해 언급되었었고, 이전에도 기술 블로그나 테크 컨퍼런스 같은 곳에서도 TDD의 중요성을 자주 들었었다.

또한 이번 주차 메일에서 함수별 테스트 코드를 작성해보라고 되어 있었는데, 함수 단위 테스트에 TDD를 적용하면 큰 시너지 효과를 발휘할 것 같아서 어설프지만 TDD를 적용해보기로 했다.

먼저 작성한 요구 사항 목록에 대해서 하나의 큰 요구 사항마다 테스트를 먼저 작성하고, 이 테스트에 맞는 코드를 작성했다.

위 사진은 플레이어 입력 검증에 대한 커밋들이다.

커밋을 보면 다음과 같은 순서로 요구사항을 충족했다.

  1. 플레이어로부터 입력 기능에 대한 테스트를 작성한다.
  2. 입력 기능을 구현한다.
  3. 테스트 코드와 기능 코드에서 서로 맞지 않는 부분이 있어서 테스트 코드를 수정한다.
  4. 입력 테스트에 이어, 입력한 값을 검증하는 기능에 대한 테스트 코드를 작성한다.
  5. 입력 검증 기능을 구현한다.
  6. 코드들을 리팩토링한다.

이번 미션의 모든 기능에 대해서 위와 같은 테스트 코드 작성 → 기능 구현의 과정을 거쳤다.

TDD 방식을 처음 적용하다보니 테스트 코드 작성에 엄청나게 오랜 시간을 쏟았다.

더욱 많은 연습을 통해 테스트 코드 작성에 익숙 해져야겠다는 생각이 들었다.

2-2. Jest

Jest를 이용하여 본인이 정리한 기능 목록이 정상 동작함을 테스트 코드로 확인한다.

이번 주차 미션에서 위와같은 요구사항이 추가되었다.

처음에는 jest를 처음 들어봐서 당황했지만, 알고보니 1주차 미션에서 사용했던 자바스크립트 테스트 도구였다.

지난 주차에는 단순히 npm test 로 테스트 코드를 실행만 해봤지만, 이번 주차부터는 테스트 코드를 직접 짜야했다.

지난 주차에 테스트 케이스 정답이 맞는지 확인하는 excpect.toEqual() 함수가 사용 됐었는데, Jest 공식 문서를 확인해보니 toEqual() 외에도 정말 많은 함수들이 존재했다.

다행히 공식문서에 각 함수들의 사용법이 예시와 함께 자세히 명시되어 있어서 해당 사이트를 자주 참고 했다.

ApplicationTest.js

직접 테스트 코드를 작성하기 전에, 주어진 ApplicationTest.js가 어떤 구조로 되어있는지 알고싶었다.

expect의 함수들은 공식 문서를 보고 어렵지 않게 이해할 수 있었다.

toHaveBeenCalledWith()은 어떤 함수가 해당 인자와 함께 사용되었는지 판별하는 테스트, toThrow()는 에러가 발생하는지 확인하는 테스트였다.

그러다가 구조를 파악하면서 spyOn나 mock 함수들이 있었는데 이 함수들이 무엇을 의미하는지 궁금했다.

const mockQuestions = (answers) => {
  MissionUtils.Console.readLine = jest.fn();
  answers.reduce((acc, input) => {
    return acc.mockImplementationOnce((question, callback) => {
      callback(input);
    });
  }, MissionUtils.Console.readLine);
};

const mockRandoms = (numbers) => {
  MissionUtils.Random.pickNumberInRange = jest.fn();
  numbers.reduce((acc, number) => {
    return acc.mockReturnValueOnce(number);
  }, MissionUtils.Random.pickNumberInRange);
};

const getLogSpy = () => {
  const logSpy = jest.spyOn(MissionUtils.Console, "print");
  logSpy.mockClear();
  return logSpy;
};

찾아보니 mock 함수들은 jest.fn()로 설정한 함수에 임의의 가짜 값을 넣을 수 있다고 한다.

mockQuestions() 함수는 mockImplementationOnce() 함수를 통해 Console.readLine()에 가짜 함수를 집어넣고, Console.readLine() 함수가 실행 될 때 가짜 함수를 실행 하도록 한다.

즉, Console.readLine() 함수를 실행 할 때마다 가짜 input 값들이 입력되도록 한다.

mockRandoms() 함수는 mockReturnValueOnce() 함수를 통해 pickNumberInRange() 함수가 실행 될 때 가짜 값인 number들을 반환하도록 한다.

ApplicationTest.js의 구조를 파악하니 이 후 테스트 코드를 작성하는데 훨씬 수월했다.

작성한 테스트 코드

이번 미션을 진행하면서 작성한 테스트 코드다.

주어진 ApplicationTest.js와 StringTest.js를 제외하면 5개의 기능별 테스트 코드를 짰다.

테스트 코드는 작성했던 요구사항 목록에 맞춰서 작성했고, 플레이어 입력과 입력 검증은 같은 파일로 테스트를 진행했다.

내가 작성한 테스트의 자세한 코드는 여기에서 볼 수 있다.

2-3. 느낀점

생각보다 테스트 코드를 짜는 것은 엄청 어려운 일이었다.

기능 구현을 하는 것 보다 테스트 코드를 짜는데 거의 두 세배의 시간은 쓴 것 같다.

TDD를 처음 해보기도 했고, jest 명령어가 익숙하지 않기도 해서 그렇게 오랜 시간이 걸린 것 같다.

더 많은 테스트 코드 작성 연습을 통해 테스트 코드 작성 속도를 올려야겠다.

이번에 TDD를 처음 시도해보면서 느낀 TDD의 장점은, 확실히 요구 사항이 훨씬 명확해지는 것 같다.

테스트 코드에 맞춰 기능을 구현하도록 하니 다른 기능으로 새지 않고 해당 테스트 해결에만 몰두해서 할 수 있었다.

또한, 단위 테스트를 기반으로 하기 때문에 리팩토링을 할 때 문제가 생기는 지점을 빨리 찾을 수 있어서 유지보수의 시간을 줄일 수 있었다.

3. 함수 분리

함수는 한 가지 일만 하도록 최대한 작게 만들어라

이번 미션의 주요 요구사항 중 하나이고, 이를 지키기 위해 많이 고민했다.

함수 분리는 다행히도 이전 주차에서 클린 코드를 학습하며 미리 시도해봤던 것이 많은 도움이 되었다.

3-1. App.js를 나누자


함수를 최대한 기능 단위로 잘게 쪼개려고 하다보니 많은 함수가 생겼었는데, 처음에는 이 많은 함수들이 App.js에만 전부 모여있었다.

이렇게 하다 보니 찾고 싶은 함수를 빠르게 찾기가 힘들었고, 이에 대응하기 위해 파일 분리를 시도했다.

게임 흐름 구조에서 핵심적인 함수인 play() 함수와 end()를 제외하고 전부 분리를 시도했다.

이렇게 하니 App.js의 코드도 훨씬 간결해졌고 테스트 할 때도 파일별로 테스트 할 수 있어서 좋았다.

또한, 해당 함수들에서 또 분리할만한 코드를 같은 파일내에서 분리할 수 있으니 코드를 더 세분화할 수 있었다.

예를 들어 스트라이크와 볼의 개수를 구하는 함수는 스트라이크 개수를 구하는 함수, 볼 개수를 구하는 함수로 더 세분화하여 나눌 수 있었다.

3-2. Console.readLine() 와의 전쟁

이번 미션에서 고민을 많이 했던 부분 중 하나는 App 클래스의 play() 함수와 end() 함수였다.

두 함수의 공통점은 Console.readLine()을 사용 한다는 점이다.

사용자의 입력을 받는 함수인 readLine() 함수가 비동기 처리 되기 때문에 이 부분에 대한 처리를 많이 고민했다.

콜백 함수 내에 코드를 직접 작성하면 indent 1개를 달고 시작하므로 이 방식은 별로 사용하고 싶지 않았다.

new Promise

처음에는 동기 처리를 해주기 위해 async await 방식을 사용하려고 했다.

위 방식이 new Promise로 readLine() 함수를 감싸고 이를 호출하는 임시 함수temp()에서 async await로 부르는 방식으로 사용하려고 했다.

그러나 이 방식은 실패했다.

Jest 공식 문서에는 Promise를 테스트 하려면 .resolve나 .reject를 붙여줘야 한다고한다.

제공된 ApplicationTest.js 테스트 코드를 건드릴 수는 없으니 .resolve나 .reject를 붙일 수 없기 때문에 위 방식은 포기 했다.

콜백 함수를 감싸는 함수?

콜백 함수를 감싸는 함수를 따로 만들어서 사용한다면 indent를 줄일 수 있다.

그러나 이 방식으로 하니깐 자꾸 에러가 났다.

이유를 찾아보니 콜백함수에서 가리키는 this와 클래스 함수가 가리키는 this가 서로 다르기 때문이라고 한다.

이를 해결해주기 위해 여러가지 방법이 존재하는데, 나는 이 중에서 bind() 함수로 명시적으로 this를 바인딩 하도록 했다.

이렇게 하면 콜백함수에 대한 함수를 따로 정의 해줄수 있고, 이로 인해 indent를 줄일 수 있었다.

4. (번외) 제출 플랫폼의 예기치 못한 오류..

모든 기능 코드와 테스트 코드를 점검하고 저녁 먹기 전 여유롭게 제출하려고 했으나..

제출 플랫폼에서 계속해서 예기치 못한 오류가 떴다.

처음엔 서버 오류인가 싶었지만 계속해서 오류가 뜨는 걸 보아하니 내 코드에 문제가 있다고 확신했다.

제출 플랫폼에선 어떠한 에러인지 뜨지 않기 때문에 원인을 찾기 위해 무작정 커밋을 날렸다.

20개 가까이 수정 커밋을 시도했고, 제출 시간은 3시간가량 남았지만, 오류는 사라지지 않았다.

2주간의 노력이 물거품이 될 수도 있단 생각에 손발을 벌벌 떨며 계속해서 원인을 찾고 싶어 코드를 미친 듯이 수정했고, 결국..

원인을 찾았다.

상수 변수들을 따로 파일에 모아놨었는데, 이 파일을 지우니 통과가 떴다.

아마 불러오는 과정에서 무슨 문제가 있었던 것 같다.

이 짧은 시간을 통해 세상엔 내가 예상하지 못한 변수들이 잔뜩 있으니 더욱더 대비를 단단히 해야겠다고 느꼈고, 에러 원인을 찾을 수 없는 상태에서 디버깅하는 경험은 정말 힘들다는 걸 깨달았다.

앞으로 코드를 짤 때도 에러를 찾기 쉽게 테스트 코드를 잘 작성 해야겠다고 생각했다.

에러를 찾기 위한 필사적 커밋 몸부림의 흔적..

profile
💻✏️🌱

0개의 댓글