이 글은 우아한테크코스 6기 프리코스 1주차 미션 종료 직후 작성된 회고 글입니다.

스스로 고민한 내용이 글에 많이 포함되어 있기 때문에 본인 코드에 대해 충분히 고민한 후 읽으시는 것을 추천합니다.

아직 부족한 부분이 많기 때문에 잘못된 내용이 포함되어 있다면 피드백 부탁드립니다. 🥲


🎯 주제

작년 프리코스 1주차에는 온보딩 이라고 해서 코딩테스트(?) 느낌의 7문제들이 나왔었다. 이번에도 그런 유형의 문제가 나올 줄 알았는데 올해는 숫자 야구 게임 이 1주차 미션으로 나왔다. 숫자 야구 게임은 4기, 5기에도 나왔던 문제였기 때문에 올해 또 나올 것이라고 생각은 전혀 못했다.

숫자 야구 게임이 1주차에 나온 것을 보고 2주, 3주, 4주차 문제의 난이도가 상당히 높아질 것 같다는 생각도 들었다. 어려운 문제가 많으면 배워가는 양도 상당히 많아질 것 같아서 기대가 되기도 하고 설레기도 하였다.

사실 작년 5기 숫자야구 게임 문제도 풀어봤지만, 작년 숫자야구 게임은 2주차에 등장했고, MVC 패턴 을 학습하기 이전이었다. 따라서 이번 1주차 미션에는 여름 방학에 학습한 MVC 패턴 을 적용해서 문제를 해결하고자 하였다. MVC 패턴에 관해서는 정리한 글을 아래 첨부하겠다.

👉 MVC 패턴 관련 정리 글 보러가기

👉 MVC 패턴 적용한 ‘메뉴 추천’ 정리 글 보러가기

🎯 문제 요구 사항

문제에서 요구하는 사항을 꼼꼼하게 읽었다.

npm install
$node -v

npm install 을 해주고 node-v로 노드 버전도 확인했다. 최종적으로 제출했을 때 버전이 다르면 테스트케이스가 작동이 안된다고도 써있었기 때문에 꼼꼼히 확인했다.

쭉 읽다가 정말 흥미로운 사실을 하나 알게 되었다.

  • API를 사용하라고 알려주는데 구체적인 사용법은 Random 밖에 알려주지 않았다. 스스로 module을 찾아보면서 어떻게 사용하는지 터득해보라는 의미였던 것 같다. 저것만 봤는데도 너무 짜릿했다. 남들이 떠먹여주는 것이 아닌 내가 생각해보고 코드를 작성해보면서 실행해보며 배운다는 점이 인상깊었다.

  • readLineAsync??? readLineAsync이었다. 입력값을 받을 때 비동기 를 사용해보라는 의미같았다. 벌써 내가 모르는 개념이 나와서 배울 것이 하나 생겼다는 점에서 살짝 긴장이 되면서 설렜던 것 같다.

🎯 기능 목록 작성

어렸을 때 많이 했던 게임이라 원리를 이해하는 것에서 오랜 시간이 걸리지 않았다. 노트에 실제로 게임을 해보면서 어떤 기능들이 필요할 지 생각해봤다. 구체적인 내용보다는 기능을 구현하면서 하나하나 지워나가기 위해 짧게 작성했다.

## 기능 목록

- 3자리 랜덤 숫자 생성하는 기능 [✅]
- 사용자에게 숫자를 입력받는 기능 [✅]
- 입력값의 유효성을 검사하는 기능 [✅]
- 입력받은 숫자를 '스트라이크 / 볼'로 계산하는 기능 [✅]
- 계산을 바탕으로 힌트를 출력하는 기능 [✅]
- 재시작을 물어보는 기능 [✅]

그리고 Model, View, Controll로 나눠서 로직을 짜보면서 흐름을 어떻게 이어가야 할 지에 대해 좀 많은 고민을 했던 것 같다. 화이트보드에 작성한 대략적인 함수와 모듈로 구성된 로직을 작성하였다.

🎯 고난 그리고 배움

이미 한번 풀었던 문제이기 때문에 힘든 고난은 없을 줄 알았다. 그러나 생각보다 큰 고난을 여러번 마주하게 되었다.

❌ [ERR_MODULE_NOT_FOUND] 에러

로직대로 파일을 나누고 기능을 구현하였다. 다른 파일에서 모듈을 받아서 사용하려고 했을 때, 다음과 같은 오류가 발생하였다.

평소와 같이 module.exportsrequire 를 사용했는데 이 방법에서 문제가 발생하다는 블로그 글을 하나 확인했다. 실제로 package.json 을 확인해보니 아래와 같이 코드가 작성되어 있었다.

  "description": "우아한테크코스 프리코스 숫자 야구 게임 미션",
  "type": "module"

요구사항에서 **‘절대 package.json을 수정할 수 없습니다.’**라고 쓰여져 있었기 때문에 type을 ES5에서 쓰이는 commonJS가 아닌 ES6에서 사용되는 module 형태로 작성해야 했다.

import Name from 'Path';

낯설었지만 익숙해지려 노력하였다. 객체를 import하거나 함수를 import할 때 사용법이 살짝 다르길래 아래와 같이 따로 정리해서 적어두었다.

  • 객체를 내보내기 위해서는 exports default {$객체명}을 사용해야 한다.
  • 모듈당 하나의 객체를 내보내고 싶을 때 사용하는 키워드로, 하나의 객체만 내보낼 수 있기 때문에 한 모듈 안에는 하나의 default export가 존재한다.
  • 모듈당 여러개의 객체가 존재한다면 default 를 사용하지 않고 { }안에 객체 이름을 넣어준 채로 export 한다.
  • import할 때에는 아무 이름으로 원하는 객체에 접근할 수 있다. 정상적으로 작동한다.

하지만 오류는 없어지지 않았다…? 대체 왜…? 하라는 대로 했는데? 정말 위 오류를 해결하기 위해 4시간정도 투자를 했던 것 같다. 그러다 어떤 블로그 글을 보게 되었는데 다음과 같이 쓰여 있었다. O JHL님의 NodeJS 블로그 참고

nodejs는 package.json의 type을 module로 설정한 경우 위 코드는 ‘import A from “./src/A.mjs” ‘로 인식한다.

파일명을 .mjs 로 바꿔주니 모든 것이 해결되었다.

위와 같은 방법으로 일시적인 오류는 해결하였으나, .mjs 파일로 바꾼 것이 그냥 괜히 찜찜했다. 충분히 .js 만으로 해결할 수 있을 것 같아서 확장자명까지 명시해봤다. 기존에는 import파일명 까지만 입력하면 경로가 자동완성 되었지만 직접 확장자명까지 입력해보았다.

import { GuideMessage } from '../constant/Constant'; // Before
import { GuideMessage } from '../constant/Constant.js'; // After

그랬더니 오류가 완전히 해결되었다.

❌ 프로그램 종료…? 어떻게…?

프로그래밍 요구사항 을 읽어보면 프로그램을 종료할 때 process.exit() 을 호출하지 말라고 명시되어 있다. 이유가 조금 궁금했다. 사실 저런 기능이 있는 것도 처음 알았다. 구글에 검색해보니 process.exit() 는 매우 강력한 종료 도구여서 쓰지 않는 것이 좋다고 한다.

작년과 같이 API가 주어진 것이 아니어서 내가 기능을 추가적으로 만들어야 하나 싶은 고민까지 했었지만 생각보다 답은 간단했다. return 만 써주니 프로세스가 강제 종료되었다.

❌ jest 실행 오류

정말…이것땜에 애를 먹었다. 결과적으로 이 오류는 14시간만에 해결이 되었다.

npm test 를 통해 주어진 테스트 케이스를 실행시킬 수 있다. 기능 구현이 다 끝났을 때 테스트를 실행시켰는데 이상한 오류가 떴다.

보통 PASS 혹은 FAIL 둘 중 하나만 나오는게 정상인데 RUNS 만 뜨고 실행이 되지 않았다. 분명 console.log 로 플레이하는 것에서는 문제가 발견될만한 지점도 나오지 않았다. 근데 계속 에러 내용에 ERROR 처리 했던 부분이 뜨길래 에러를 관리하는 파일만 계속 건들게 되었다.

InputView 파일에 있던 callback 함수에 문제가 생긴 줄 알고 함수를 계속 분해하니 아예 위와 같이 이제는 실행도 되지 않았다. 구글링 및 JEST 공식 문서까지 들어가봐도 이런 오류를 겪은 사람도 없었고 나오지도 않았다.

총 테스트케이스가 2개였고 1번째 테스트 케이스는 잘 작동하는 것 같아서 이번엔 ApplicationTest.js 파일을 분해해봤다. 두번째 테스트 케이스를 지워보고 첫번째 테스트 케이스를 실행했더니 위와 같았다.

기능 구현 코드를 다 했고, console.log도 해봤는데 여기서 막히니까 밥도 먹지 못하고, 잠도 잘 수 없었다. 어떻게든 이 문제를 해결하고자 하는 의지가 강했다. 구글에 검색해도 나오지 않는 오류였기에 내 코드에 문제가 분명한 것 같았다. 이 문제에 대해 14시간동안 고민을 하던 중 문득 ApplicationTest.js 파일을 한번 분석하고자 하였고, 해당 코드에서 Promise 라는 키워드를 보게 되었다. 검색을 해보니 비동기 와 관련이 있는 키워드였고 async , await 이 함께 정리가 되어있었다.

이건 무조건 비동기 에 대해 완벽히 알아야만 해결할 수 있는 문제라고 생각이 들었고, 그때부터 공식 문서와 여러 블로그 글을 읽어보면서 최대한 이해하기 쉽게 정리했다. 사실 너무 배우고 싶었던 개념이고, 중요하다고 생각이 들어서 작성한 코드에 바로바로 적용해가면서 변화를 살펴보고자 했다. 실제로 방대한 내용을 포함하고 있었지만 큰 욕심을 부리지 않고 현재 이 문제를 해결하기 위해 필요한 내용에 집중했고, 해당 오류를 해결할 수 있었다.

그 때의 짜릿함은 살면서 많이 겪어보지 못한 경험이었다. 오랜 시간의 고민과 시행착오를 겪으며 학습한 내용이었고, 결국 스스로의 힘으로 해결했다는 성취감 때문인 것 같다.

🎯 새로 배운 내용

🔖 비동기 (Promise, async, await)

예전에 모던 자바스크립트 를 공부하다가 목차에서 Promiseasync 에 대해 본 기억이 있다. 심지어 벨로그 트렌드 글에서도 비동기에 대해 많은 글이 있었다. 하지만 내가 작성하는 코드에서 비동기 에 대해 쓸 일이 크게 없었다.

이번 프리코스 1주차 코드 App.jsasync play() 라는 함수를 보고 “왔구나…” 싶었다. 처음 배우는 내용이기 때문에 잘 해낼 수 있을까 하는 걱정도 있었지만 새로운 것을 알게 된다는 설렘이 더 컸다. 막대한 양의 내용이었지만 우선 에러를 해결하기 위한 학습을 하였고, 이후에는 리팩토링을 하면서 학습한 것을 적용하였다. 이 곳에 많은 시간을 쏟았다.

이 블로그에 그 내용 모두 적을 수 없어서 자세한 내용은 직접 정리한 블로그 글을 참고하면 된다.

👉 [우아한테크코스 프리코스 6기] 비동기 확실하게 이해하기

🔖 더 나은 commit 방법

사실 기존에 수정한 코드에 대해서 전부 git add . 명령어를 입력하는 습관이 있었다. 왜 쉬운 방법이 있는데 굳이 파일을 하나하나 다 입력해야 해? 라는 생각이 있었다.

하지만 이번에 commit을 기능별로 구현하라는 안내 문구를 보았다. 실제로 브랜치에 많이 들어가지 않아서 몰랐지만 실제로 들어가보니 내가 해당 기능만 commit에 올리고 싶었는데 메시지와는 다르게 다른 파일들도 수정되어 한꺼번에 commit되어 있는 것을 보니 가독성이 매우 떨어졌다.

git add src/constant/Constant.js

→ 이렇게 하면 내가 원하는 파일만 커밋 완료! git status를 확인하면 내가 아직 add 하지 않은 파일을 빨간색으로 표시되어있는 것을 볼 수 있다.

🎯 리팩토링

한번 풀어봤던 문제였기 때문에 기능 구현은 생각보다 오래 걸리지 않았다. 다음으로 중요한 것은 리팩토링 과정이었다.

✏️ 통일성 있게 코드 작성하기!

우선 변수명에 상당히 많은 시간을 쏟았다. 처음 이 코드를 보는 사람도 내가 구현한 기능이 어떤 기능을 할 지 직관적으로 파악이 가능할 정도로 시간을 많이 쏟았다. 이후에는 한 모듈 안에서 메서드들이 얼마나 통일성 있게 짜여져 있는 지에 대해 고민을 하고 수정했다.

class BaseballGame() {
  {...}

  getStrikeCount() {...}

  getBallCount() {...}

  resetGame() {...}
}

위 코드는 처음에 Model을 구현한 부분 중 일부이다. resetGame() 메서드를 제외하고는 모두 get 접두어로 시작하였다.

class BaseballGame() {
  {...}

  getStrikeCount() {...}

  getBallCount() {...}

  getResetNumber() {...}
}

따라서 resetGame() 메서드의 이름을 getResetNumber()로 바꾸고 새로운 넘버를 가져오는 기능만 하게끔 하고 실제 게임을 리셋하는 것은 컨트롤러의 몫으로 넘겨주었다.

✏️ 하나의 함수는 하나의 기능만!

// 변경 전
  async inputUserNumber() {
    await InputView.readUserNumber((input) => {
      InputValidator.validateUserNumber(input);
      const inputNumber = input.split("").map(Number);
      const strikeCount = this.#baseball.getStrikeCount(inputNumber);
      const ballCount = this.#baseball.getBallCount(inputNumber, strikeCount);

      return this.checkHint(strikeCount, ballCount);
    });
  }
// 변경 후
  async inputUserNumber() {
    await InputView.readUserNumber((input) => {
      InputValidator.validateUserNumber(input);
      this.calculateCount(input);
    });
  }

  calculateCount(input) {
    const inputNumber = Array.from(input, Number);
    const strikeCount = this.#baseball.getStrikeCount(inputNumber);
    const ballCount = this.#baseball.getBallCount(inputNumber, strikeCount);

    return this.checkHint(strikeCount, ballCount);
  }

하나의 함수 안에서 두 개 이상의 기능을 하지 못하도록 최대한 처음에 만든 함수를 잘게 잘게 쪼갰다.

✏️ 메모리 사용량을 줄이고 효율적인 코드를 만들어보자!

위의 코드에서도 확인할 수 있지만 input.split("").map(Number) 은 효율이 많이 떨어진다는 글을 읽고 Array.from()으로 바꾸는 등 내장 함수를 최대한 많이 활용해보고자 하였다.

사실 반복적인 행동을 해야할 때 아직까지도 for() 반복문을 가장 먼저 작성한다. forEach() 함수와 map()도 많이 작성해봤지만 reduce()를 한번도 제대로 사용해본 적이 없었다. 이번 기회에 스트라이크와 볼의 카운트를 누적해서 계산하는 부분에 reduce()를 적용해 보았다.

👉 reduce 함수 관련 글 보러가기

게임 종료 후 재시작을 하는 테스크 케이스의 시간이 빨라진 것을 확인할 수 있었다.

🎯 개선해야할 점

이번 미션을 진행하면서 아쉬웠던 점은 에러를 해결하기 위해 수정되었던 코드들이 제 타이밍에 commit 되지 못했다는 점이다. 한번 commit이 꼬이니까 깃 컨벤션도 어떤 순서로, 어떤 타이밍에 해야할 지 파악을 제대로 하지 못했다. 다음 미션부터는 git add . 사용을 지양하고 하나의 기능 구현, 혹은 수정이 끝날 때마다 수정한 부분만 git add 경로 를 작성하여 코드 리뷰를 할 때에도 가독성이 좋게 해보고 싶다.

또한 여러 블로그 글을 보면서 TDD 라는 테스트 주도 개발법을 알게 되었는데 JEST 테스트 케이스 파일을 만들 때 한번 접목해서 우테코에서 주어진 케이스 뿐만 아니라 직접 만들어보고 싶은 욕심이 생겼다.

🎯 마무리

이번 주차 한줄평을 다음과 같이 할 수 있을 것 같다.

써먹을 수 있는 공부를 했다.

필요한 기능을 구현하기 위해 하는 공부가 중요하다는 것은 프로젝트를 해보면서 느꼈지만 이렇게 짧은 시간 안에 크게 성장할 것이라는 생각은 해본 적이 없다.일주일도 안되는 시간이었지만 1주차만에 많은 발전을 했다고 생각한다. 정말 우테코에서 강조한 몰입이 이런 것이 아닐까. 밥먹는 시간과 잠자는 시간을 아껴가며 고민하고 분석하며 결국에는 혼자의 힘으로 해결한다는 것이 얼마나 큰 성취감으로 돌아오는 지를 느낄 수 있었다.

앞으로 3번의 미션이 남아있다. 난이도가 점점 어려워질 것 같아서 걱정이 되지만 그 과정에서 얼마나 성장할 지를 생각하면 정말 설렌다.

이번 2주차 미션도 많은 깨달음을 얻을 수 있는 한 주가 되기를 바란다.

🧑🏻‍💻 내가 작성한 코드

👉 우아한테크코스 1주차 미션_숫자 야구 게임

코드 리뷰는 언제든 환영입니다! 🙂