Search

[우아한테크코스] 프리코스 숫자야구 미션 회고

Created
2023/10/25
Tags
JavaScript
Category
Project
Parent item
Sub-item
2 more properties
개인적으로는 미션 수행을 통해 성장한 과정을 기록하고,
누군가에게는 이 글이 도움이 되었으면 하는 마음입니다 :)

1주 차 결과물

게임 실행 화면과 테스트 통과 화면

1주 차 미션 메일 내용

가벼운 미션..?

1주 차 미션인 '숫자 야구 게임'을 보고 반가웠다. 지난 기수들에서 자주 나왔던 미션이었기 때문이다. (필자는 6기에 지원했고) 지난 5기에서는 2주 차 미션이었는데 이번엔 1주 차에 배정돼서 오잉? 했다. 아마 지난 기수 1주 차 '온보딩' 미션이 기능 구현보다는 알고리즘 문제 풀이에 가까워서 그런 게 아닐까? 하는 추측을 해보았다.
첫 메일 내용에 의하면, 개발 환경 세팅과 Git 등 미션 외에도 추가로 익혀야 하는 부분들이 있어 '가벼운 미션'으로 준비했다고 한다. 정말 가벼울지 모르겠지만, 약 일주일이라는 시간이 주어진 만큼 하나하나 꼼꼼하게 해 보자!

신경 써야 하는 것들

미션을 제출하지 않으면 다음 프리코스 단계와 최종 코딩 테스트에 참여할 수 없으니 100% 완벽하게 구현하지 못하더라도 기간 내에 제출하는 것이 중요하겠다. 더불어, 과제를 제출할 때 미션을 진행하면서 느낀 점을 소감문으로 작성하고, 이때 학습한 '과정'을 잘 드러내 달라는 내용이 있었다. 미션을 풀어내는 동안 블로깅을 염두하고 그때그때 기록을 잘해두어야겠다고 생각했다.

프리코스 진행 방식

미션 진행

프리코스 1주 차 미션의 저장소에서 자신의 깃헙에 fork 후 clone 하여 진행한다. 시작부터 git에 대한 기초적인 이해를 필요로 한다. 따라서 git 정도는 자유자재로 다룰 수 있도록 미리 익숙해진다면 미션 진행 동안 어이없는 실수를 방지할 수 있겠다. (물론 우테코 측에서 제공한 이 문서를 참고해도 충분하다.) 나는 어느 정도 git에 대해서는 익숙해진 터라, 최대한 깔끔하고 직관적으로 커밋하는 것을 목표로 했다.

미션 구성

매주 미션은 (1) 기능 요구 사항, (2) 프로그래밍 요구 사항, (3) 과제 진행 요구 사항 세 가지로 구성되어 있다. 따라서 단순히 기능 구현뿐만 아니라 전후로 요구하는 사항들을 꼼꼼히 확인하여 진행하는 것이 중요하겠다. 실제로 프리코스 커뮤니티에서 이러한 요구사항들을 꼼꼼하게 확인하지 않고 질문을 하는 사람들이 많아서 놀랐다. 게다가 요구사항을 지키지 않아서 테스트 통과는 물론 제출 시 오류가 났던 사람들도 있었다. 그러니 이런 것들은 각자가 알아서 꼼꼼하게 챙기시길!

미션 마감 및 기준

미션 진행 가능 기간은 약 일주일, 그리고 미션 제출 가능 기간은 반나절 정도의 시간이 주어진다. 일단 본인 Github에 commit을 해두고, 미션 제출 가능 기간 동안 push & PR을 하면 되겠다. (물론 미션 제출 기간이 아니더라도 push는 해도 된다고 한다.) 물론, 마감일 이후 추가 push도 허용하지 않는다. 물론 리팩토링은 자유자재겠지만, 그보다 1주 차 미션을 복습하고 다음 미션을 하느라 정신이 없을 것 같다.

프리코스 커뮤니티

프리코스 커뮤니티란?

프리코스에서는 4천 여 명에 달하는 우아한테크코스 지원자들이 함께 커뮤니티 내에서 학습하고 성장할 수 있다. 디스코드 커뮤니티에서 권장하는 활동은 서로 리뷰하기(코드 리뷰), 토론하기, 함께 나누기(정보 공유), 다시 돌아보기(회고)이며 이 활동이 아니더라도 함께 학습하고 성장할 수 있는 스터디 개설 등 무엇이든 가능하다고 한다.

참고할 사항

단, 각자 스스로 학습할 기회를 위해 아직 마감되지 않은 미션에 관련된 질문과 답변은 마감일 이후로 공유하는 게 암묵적 원칙이다. 물론 커뮤니티 활동 기록은 최종 합격에 전혀 영향을 미치지 않는다고 우테코 측에서 입학설명회 등을 통해 공식적으로 이야기했다. 심지어 디스코드 채널에 들어오지 않아도 된다.

커뮤니티의 첫인상

디스코드에 접속했는데 프리코스 시작 전부터 커뮤니티 활동이 엄청 활발해서 놀랐다. 다들 열정이 대단하다고 느꼈지만, 미션을 스스로 하는데에 살짝 노이즈가 되는 면도 없지 않아 적절하게 활용하고자 한다. 함께 리뷰하고 토론하는 것도 프리코스에서 누릴 수 있는 혜택이기 때문.

미션에 앞서,

기능 목록 만들기

미션은 기능 요구 사항, 프로그래밍 요구 사항, 과제 진행 요구 사항 세 가지로 구성되어 있다. 특히 주목해야 할 점은 기능을 구현하기 전에 기능 목록을 만든다는 것. 기능 구현 시 목록을 만들어져 있는 것이 아니라, 직접 만들어야 하는 게 포인트다.

제출 전 테스트 필수

기능 구현을 모두 정상적으로 했더라도 요구 사항에 명시된 출력값 형식을 지키지 않을 경우 0점으로 처리된다. 기능 구현을 완료한 뒤 테스트를 실행했을 때 테스트가 실패할 경우 0점으로 처리되므로, 반드시 확인 후 제출한다. 필자는 Node 환경에서 프로그램이 잘 돌아가는지 확인하고, 테스트 통과까지 확인하며 작업했다. 그렇지 않으면 최종 제출 테스트에서 예상치 못한 에러가 날 수 있으니 미리미리 확인하자.

Node 버전 확인 필수

테스트 패키지 설치를 위해 Node.js 버전 18.17.1 이상이 필요하다. 본인 로컬의 Node 버전이 몇인지 꼭 확인 후 미션을 진행하자. 참고로 Node 버전 확인은 'node --version'으로 확인 가능하며, 업데이트는 Node.js 공식 사이트를 참고하거나, 아래와 같이 'nvm install'로 특정 Node 버전을 설치할 수 있다.
Node 버전 설정 화면

⚾️ 기능 요구 사항

숫자 야구 게임 설명

미션에 대한 자세한 내용은 우테코 프리코스 레포에 있으니 참고하길 바라며, 간단히 게임 설명을 하자면 기본적으로 1부터 9까지 서로 다른 수로 이루어진 3자리의 수를 맞히는 게임이다.
같은 수가 같은 자리에 있으면 스트라이크, 다른 자리에 있으면 볼, 같은 수가 전혀 없으면 낫싱이란 힌트를 얻고, 그 힌트를 이용해서 먼저 상대방(컴퓨터)의 수를 맞추면 승리한다. 즉, 제시한 숫자에 따라 다른 결과가 나오도록 하는 것이 핵심 골자인 듯하다.

숫자 야구 게임 예시

예) 상대방(컴퓨터)의 수가 425일 때
위 숫자 야구 게임에서 상대방의 역할을 컴퓨터가 한다. 컴퓨터는 1에서 9까지 서로 다른 임의의 수 3개를 선택한다. 게임 플레이어는 컴퓨터가 생각하고 있는 서로 다른 3개의 숫자를 입력하고, 컴퓨터는 입력한 숫자에 대한 결과를 출력한다.
이 같은 과정을 반복해 컴퓨터가 선택한 3개의 숫자를 모두 맞히면 게임이 종료된다. 따라서 위 과정을 반복하면서 → 컴퓨터가 생성했던 3개의 숫자를 모두 맞히거나 / 맞히지 않았을 때에 따라 적절한 결과가 핑퐁핑퐁 잘 나오도록 해야 한다.
게임을 종료한 후 게임을 다시 시작하거나 완전히 종료할 수 있다. 사용자가 잘못된 값을 입력한 경우 throw문을 사용해 예외를 발생시킨 후 애플리케이션은 종료되어야 한다. 만약 사용자가 예상되는 값 외의 값을 입력했을 경우 꼼꼼하게 예외처리까지 꼭 해주어야겠다.

프로그래밍 요구 사항 - 실행 환경

 Node.js 18.17.1 버전에서 실행 가능해야 한다.
→ Node 버전 체크 필수
 프로그램 실행의 시작점은 App.js의 play 메서드다.
→ play()로 프로그램 실행이 가능하니 이를 추가하자.
 package.json을 변경할 수 없고 외부 라이브러리(jQuery, Lodash 등)를 사용하지 않는다. 순수 Vanilla JS로만 구현한다.
→ 하지만 필자는 ESLint와 Prettier를 사용하고, 변경된 package.json 내역은 굳이 push 하지 않았다.
 JavaScript 코드 컨벤션을 지키면서 프로그래밍한다.
→ 아래에서 자세하게 이야기하겠지만, Airbnb 코드 컨벤션을 하나하나 보고 따르기에는 너무 번거로웠기에 ESLint와 Prettier를 설정해서 컨벤션을 맞추었다.
 프로그램 종료 시 process.exit()를 호출하지 않는다.
→ 보통 프로그램을 종료할 때 Console.Close()나 process.exit()를 사용하고는 했지만, 여기서는 메서드를 호출하지 않는다. 필자는 그냥 '게임 종료' 메시지를 출력했다. (참고: Node.js에서 종료하는 방법)
 프로그램 구현이 완료되면 ApplicationTest의 모든 테스트가 성공해야 한다. 테스트가 실패할 경우 0점 처리한다.
→ 위에서 말한 것처럼 두 가지 테스트를 모두 통과해야 한다.
 프로그래밍 요구 사항에서 달리 명시하지 않는 한 파일, 패키지 이름을 수정하거나 이동하지 않는다.
→ 디폴트로 있던 파일, 코드, 폴더는 굳이 건들지 않았다.

프로그래밍 요구 사항 - 라이브러리

 @woowacourse/mission-utils의 Random 및 Console API를 사용하여 구현해야 한다.
 Random 값 추출은 Random.pickNumberInRange()를 활용한다.
 사용자의 값을 입력받고 출력하기 위해서는 Console.readLineAsync, Console.print를 활용한다.
→ 굳이 따로 만들지 않고 우테코에서 제공하는 utils들을 사용해 입력 및 출력을 하면 된다.

미션을 통해 배운 점

기능 명세서

전체적인 플로우를 텍스트와 플로우 차트로 정리
시간이 조금 걸리더라도 기능 구현 플로우를 텍스트와 플로우 차트를 통해 정리하니 해야 하는 것들이 명확하게 보였다. 특히 첫 번째 미션인 만큼 익숙하지 않을 터라 초반에 더 많이 고민해 보아야 한다고 생각했다.
또한, 기능 명세서는 꼼꼼하게 작성하면 할수록 (나에게) 좋다고 생각했다. 함수명, 변수명까지 모두 정해두면 훨씬 빠른 코딩이 가능했다. (물론 이것도 점차 익숙해지면 이렇게까지 클래스명, 메서드명까지 정의하며 자세하게 작성하진 않아도 된다.) 명세서를 작성하며 전체적인 로직 구상을 할 수 있으니 코드를 다 써두고 로직을 수정하거나 헷갈리는 일을 방지할 수 있었다.
기능 명세서를 작성하다 보니 플로우가 책의 목차처럼 전체적인 그림이 그려지도록 작성하는 것이 좋다고 생각했다. 물론 코드도 마찬가지. 코드를 보면 프로그램이 그려지도록 작성해야겠다고도 생각했다.
물론 중간에 트러블슈팅을 하거나 리팩토링을 하면서 로직을 변경하느라, 일단 코드 수정 → 기능 명세서 수정으로 거꾸로 하기도 했지만, 어쨌든 기능 명세 작성은 미션을 마주하는 막막함을 덜어줄 수 있을 것이라 생각한다.

함수 이름 짓기

함수 하나는 한 가지 일만 해야 하니 함수 명은 누가 봐도 직관적으로 짓는 것이 좋다. 예를 들어, checkPermission()는 승인 여부를 확인하고 그 결과를 반환하는 동작만 해야 한다. 승인여부를 보여주는 메시지를 띄우는 동작은 이 함수에 들어가지 않는 게 좋다. (참고: function 함수이름 짓기 (2) - javascript 0604)
함수 이름
의미
예시
show...
무언가를 보여주는 함수
showMessage()
get...
값을 반환하는 함수
getAge()
calc...
값을 반환하는 함수
calcSum()
create...
무언가를 생성하는 함수
createForm()
check...
무언가를 확인하고 불린값을 반환하는 함수
checkPermission()

Eslint와 Prettier 설정

Airbnb 스타일 가이드를 보면서 코딩하기에는 너무 비효율적이라고 생각했다. 컨벤션 하나하나 어떻게 다 신경 써서 하나 싶어 ESLint와 Prettier를 직접 설정해 보기로 했다. 협업 프로젝트 할 때는 누군가가 해줬는데, 이번에 처음으로 해봤고 도움이 많이 되었다. 설정에 매우 도움이 된 블로그 글을 첨부한다.
ESLint와 Prettier 설정으로 변경된 package.json
패키지 파일을 수정하지 말라는 요구사항이 있었지만, 걱정된다면 우선 코드를 작성할 때는 로컬 환경에서만 ESLint와 Prettier를 설치 및 설정해 두고, 커밋과 푸시를 하지 않으면 된다. (하지만 프리코스 1주 차 공통 피드백에서 가능하면 ESlint와 Prettier를 이용해 더욱 생산적으로 코드를 작성하자라고 기재된 것을 보니 적극적으로 사용해도 좋겠다.)

함수 vs. 클래스

함수만 작성하기보다 아래와 같이 클래스의 형태로 만들어 상태와 메서드를 클래스 내에 캡슐화하고자 했다. 함수로 작성해도 되지만 이김에 클래스 개념에 더 익숙해지고 싶기도 했고, 추후 함수 내의 값을 변경하거나 관리하는 데에 필요한 상태나 추가적인 기능을 추적하기 어려울 수 있기 때문이었다.
// Generator.jsclass Generator { constructor() { this.computerNumber = this.generateRandomNumber(); } generateNewCorrectNumber() { this.computerNumber = this.generateRandomNumber(); } generateRandomNumber() { // ... 생략 } export default Generator;
JavaScript
복사
클래스로 나타냈을 때 더 많은 코드와 구조가 필요할 수 있어 처음에는 조금 복잡하게 느껴질 수 있지만, 어쨌든 값의 상태와 메서드를 클래스 내에 캡슐화할 수 있을 뿐만 아니라, 인스턴스를 여러 번 생성해서 추후 추적에 유용하기도 하다.

비동기로 사용자의 입력 처리

async & await로 사용자의 입력을 받은 이유는 사용자가 입력을 완료할 때까지 다른 작업을 할 수 있기 때문이다.
// BaseballGame.js async startGame() { await this.getUserInput(); } async getUserInput() { const input = await Console.readLineAsync(LOG_MESSAGE.INPUT_NUMBER); this.handleUserInputDuringGame(input); } // ... 생략 handleUserInputDuringGame(input) { if (!isValidGameInputDuringGame(input)) { throwError(ERROR_MESSAGE.INCORRECT_VALUE); }
JavaScript
복사
위 로직을 설명하자면, 일단 게임이 시작하고 사용자로부터 입력을 받아 유효성 검사를 수행한다. 이때 await를 사용하여 비동기로 입력을 받도록 했으며, 유효성 검사 함수를 호출하여 입력된 숫자의 유효성을 검사한다. 유효한 숫자가 아닌 경우, 해당 함수에서 에러를 처리할 수 있다.     await를 사용하여 비동기로 입력을 받도록 한 이유는 입력을 기다려야 하는 동안 프로그램이 블로킹되지 않도록 하기 위함이었다. 비동기적인 처리를 통해 사용자가 입력을 완료할 때까지 다른 작업을 수행할 수 있으며, 입력 대기 시간 동안 다른 코드 또한 실행될 수 있기 때문이다.
일반적으로 사용자와의 상호작용은 시간이 불규칙하게 발생하기에, 사용자의 입력을 기다리는 동안 프로그램이 블로킹되면 사용성이 저하될 수 있기 때문이다. 따라서 await를 사용하여 사용자의 입력을 기다리며, 이벤트 루프에서 비동기로 처리될 수 있도록 했다.

throw Error문과 throw new Error문의 차이

결론부터 말하자면, 두 개가 큰 차이는 없으나, new Error문이 기술적으로 더 올바른 방법이라고 한다. throw Error문은 JavaScript에서 구식인 방식으로 예외를 발생시키는 방법이며, Error 객체를 직접 생성하는 것이 아니라 내장된 Error 생성자를 사용하는 방식이다. (참고: JavaScript ‘throw new Error’ vs ‘throw Error’ vs ‘throw something’)
반면, throw new Error 문은 더 현대적인 방식으로 예외를 발생시키는 방법이라고 한다. 이 방식은 예외 객체를 명시적으로 생성하는 것으로, 예외 객체에 더 많은 정보나 커스텀 속성을 추가하거나 다른 형태의 에러 객체를 만들 수 있다. (참고: Differences Between “throw” and “throw new Error” in Javascript / NodeJS)

자바스크립트 파일명과 폴더명 컨벤션

컨벤션은 습관적으로 알려하지 않으면 나중에 적용하려 할 때 헷갈린다. 그러니 이번 기회에 확실하게 짚고 넘어 가자. (참고: google.github.io30secondsofcode.org)
파일명은 camelCase를 사용한다. (e.g. myJavaScriptFile.js) 다만, 클래스 이름의 경우 PascalCase를 사용한다. 클래스 정의가 포함된 파일명 역시 PascalCase로 작성한다. (e.g. Generator.js) 그리고 폴더명은 kebab-case를 사용한다. (e.g. my-javascript-folder) 다만, 어떤 파일 시스템(예: NTFS, git, macOS의 기본 설정)은 대소문자를 무시하므로, 파일 및 폴더명이 대소문자를 구분하지 않는 환경에서는 kebab-case나 snake_case 중 어떤 것을 사용해도 상관없다.

require? import?

이번 미션을 하면서 외부 모듈이나 라이브러리를 불러올 때 require과 import 둘 중 어떤 것을 써야 하는지 고민했다. 둘 다 비슷한 거 아닌가? 생각했기 때문.
알고 보니 (참고: 자바스크립트 CommonJS 모듈 내보내기/불러오기) require는 Node.js에서 예전부터 사용되고 있는 CommonJS의 키워드이고, import는 ES6(ES2015)에서 새롭게 도입되어 현재 자바스크립트 생태계에서 표준이 되어가고 있는 키워드라고 한다. 두 개의 키워드 모두 하나의 파일에서 다른 파일의 코드를 불러온다는 동일한 목적을 가지고 있지만, 비슷한 듯 약간씩 다른 문법을 가지고 있다. 그래서 필자는 자바스크립트에서 표준이 되어가고 있다는 import문을 사용했다.
// Node.js에서 예전부터 사용되고 있는 CommonJS의 키워드const express = require("express"); const app = express(); // ES6(ES2015)에서 새롭게 도입되어 현재 자바스크립트 생태계에서 표준이 되어가고 있는 키워드import express from "express"; const app = express();
JavaScript
복사

트러블 슈팅

일반적인 에러는 따로 다른 글로 발행하고,본 미션에서 마주할 수 있는 몇 개만 기술합니다.

App.js 모듈을 찾을 수 없다니

에러 화면
위의 에러 메시지를 잘 읽어보면 'App.js 모듈을 찾을 수 없다'는 내용이기에, 두 가지 방법으로 해결을 시도했다. 첫 번째는 Node 환경에서 게임 실행 시 'node App.js' 명령어를 입력하는 위치가 루트가 아니라 '/src'에서 해보는 것이다.
App.js를 src에서 실행
또한, 위에서 기술한 것처럼 const & require문을 모두 import & export 문으로 수정했다. (물론 함수와 클래스에 맞게 사용하는 것도 중요) 사실 모듈을 찾을 수 없는 원인은 여러 가지가 있겠지만 우선 위와 같이 해결을 했다.

테스트 통과가 안된다니

테스트 통과가 되지 않는 화면
Node 환경에서는 게임 실행과 모든 기능이 잘 구현되었는데, 'npm test'를 했을 때의 테스트는 통과하지 못하고 있었다. 기껏 다 구현했는데 테스트 확인조차 못하니 굉장히 막막했다. 하지만 내 코드의 문제겠거니 해서 테스트 코드와 함께 새벽 내내 붙잡고 있었다. 그리고 다음날 아침, 감사하게도 굉장히 힌트가 되는 블로그에서 본 내용을 기반으로 코드를 다시 살펴보았다.
이게 웬걸! 비동기가 문제였다. async를 붙인 함수를 호출하고, await를 붙이지 않았기 때문이었다. await를 붙이지 않으면 play 함수는 에러를 뱉는 함수만 실행시켜 놓고 끝나기 때문이다. async & await를 사용하려면 게임 실행을 담당하는 메서드 내부에서 프로미스를 반환하거나 await 가능한 비동기 작업을 수행해야 한다. 그러면 게임 실행 메서드 단계(사용자가 숫자를 입력을 완료하는 등)가 완료될 때까지 App 클래스의 play 메서드가 기다릴 수 있다.
코드는 아래와 같이 사용자의 입력을 기다리는 모든 곳에 async & await를 붙여 주었다.
// App.jsimport BaseballGame from './BaseballGame.js'; import { Console } from '@woowacourse/mission-utils'; import { LOG_MESSAGE } from './constants.js'; import { printMessage } from './utils.js' class App { constructor() { this.game = new BaseballGame(); } async play() { printMessage(LOG_MESSAGE.START_GAME); await this.game.startGame();// 수정된 게임 시작 메서드 호출 } } const app = new App(); app.play(); export default App;
JavaScript
복사
App.js 뿐만 아니라 아래 BaseballGame.js에서도 게임 실행 메서드 startGame을 비동기 버전으로 수정했다. 예를 들어, Console.readLine를 Promise로 래핑 하거나 적절한 방식으로 비동기 처리하며, 이는 await와 함께 작동해야 한다는 것. 그러니 게임 실행 시 사용자의 입력을 다루는 모든 곳을 살펴보는 것이 좋겠다.
// BaseballGame.js// ... 생략class BaseballGame { constructor() { this.computer = new Generator(); } async startGame() { await this.getUserInput(); }// 수정된 게임 시작 메서드async getUserInput() { const input = await Console.readLineAsync(LOG_MESSAGE.INPUT_NUMBER); this.handleUserInputDuringGame(input); }// 수정된 사용자 입력 메서드async recommendRestart() { await printMessage(LOG_MESSAGE.CORRECT_END); const input = await Console.readLineAsync(`${LOG_MESSAGE.RESTART_INPUT}\n`); this.handleUserInputEndGame(input); }// 수정된 게임 재시작 입력 메서드 handleUserInputDuringGame(input) { // ... 생략 } async handleUserInputEndGame(input) { const isValidGameInputEndGame = [GAME_SELECT.RESTART, GAME_SELECT.END]; // ... 생략 } restartGame() { this.computer.generateNewCorrectNumber(); this.startGame(); } } export default BaseballGame;
JavaScript
복사
위 코드 하단의 게임 종료 입력을 담당하는 handleUserInputEndGame 메서드 내에서는 비동기 작업을 기다릴 필요가 없으므로 await를 사용하지 않아도 된다. 이 메서드는 동기적인 작업을 수행하기에, 입력값을 검사하고 처리하는 부분에서 추가적인 비동기 작업이 필요하지 않기 때문이다.

테스트 통과를 위해선 공백 하나도 중요

'n볼 n스트라이크' 문자열이 나오지 않는 에러
얼추 기능 구현은 다 끝난 것 같다고 생각해 테스트를 돌려 보았는데, 위와 같이 출력 값이 다르게 나온다는 에러 메시지를 보았다. 메시지를 자세히 보니 '1 볼 1 스트라이크'가 나와야 하는데 그렇지 않아서 에러가 나는 것 같았다.
어이없게도 n볼 n스트라이크라는 힌트를 출력할 때 볼과 스트라이크 사이의 공백이 없어서 생기는 문제였다. 아래와 같이 볼 개수 리터럴 바로 뒤에 공백을 추가해 주었다.
const convertNumberToString = (strikeNumber, ballNumber) => { let hintMessage = ''; if (ballNumber > 0) { hintMessage += `${ballNumber}${HINT_MESSAGE.BALL} `;// 공백 추가 } if (strikeNumber > 0) { hintMessage += `${strikeNumber}${HINT_MESSAGE.STRIKE}`; } if (hintMessage.length === 0) { hintMessage += HINT_MESSAGE.NOTING; } return hintMessage; };
JavaScript
복사

테스트는 통과되는데..

Jest의 노란색 메시지
위와 같이 테스트 통과도 되었고, Node 환경에서 잘 작동되는 것도 확인했다. 그런데 하단에 노란색 글자로 "Jest did not exit one second after the test run has completed."라는 메시지가 떴다. 무슨 의미인가 찾아보니 아래와 같다고 한다.
Jest의 이러한 에러는 주로 비동기 작업이 완료되지 않아 테스트가 종료되지 않았을 때 발생한다고 한다. 그래서 에러 메시지 알려주듯, 제안하는 방법 중 하나인 '--detectOpenHandles' 옵션을 사용하여 비동기 작업이 완료되지 않은 핸들(Handles)을 식별하고 해결해도 된다. 아래와 같이 package.json에 옵션을 추가하면 더 이상 노란색 메시지가 뜨지 않는다. (하지만 이 메시지가 있다고 테스트 통과가 되지 않는 건 아니니 걱정말자.)
"scripts": { "test": "jest --detectOpenHandles" }
JavaScript
복사

리팩토링

출력 메시지들은 constants로 동결

출력 메시지들은 오탈자가 생길 가능성이 높고, (테스트 통과를 위해서라면 더더욱) 이후 변경되어서도 안된다. 따라서 일명 '동결' 시켜놓아야 한다. Object.freeze()는 자바스크립트 내장 객체의 메서드 중 하나로, 객체의 속성을 변경 또는 수정할 수 없도록 만드는 불변성을 부여한다. 이 메서드를 사용하면 객체의 속성을 동결하고, 나중에 이 객체를 변경하려는 시도가 실패하도록 하기에 아래와 같이 적용했다.
const LOG_MESSAGE = Object.freeze({ START_GAME: '숫자 야구 게임을 시작합니다.', INPUT_NUMBER: '숫자를 입력해주세요 : ', CORRECT_END: '3개의 숫자를 모두 맞히셨습니다! 게임 종료', END_GAME: '게임 종료', RESTART_INPUT: '게임을 새로 시작하려면 1, 종료하려면 2를 입력하세요.', }); // ... 이하는 생략
JavaScript
복사

중복된 숫자가 없는지 확인하는 두 가지 방법

게임 흐름 상, 컴퓨터가 랜덤 한 3자리 숫자를 생성해야 하는데 이때 중요한 조건이 '중복이 없어야 한다는 것'이었다. 이를 구현하기 위해 처음에는 배열을 하나 만들어, includes()를 사용해 이미 숫자가 배열에 들어있는지 (중복된 숫자가 있는지) 확인하고, 없다면 해당 숫자를 push()로 넣어주었다.
그러다 내장 함수 중, set()을 발견하고 리팩토링 했다. Set의 가장 큰 특징은 '중복된 요소를 허용하지 않는다는 것'이기 때문에 이를 활용하면 되었다. array의 요소를 push()하는 것처럼, set()은 add()를 활용해 값을 추가할 수 있고 중복을 허용하지 않는다. 그리고 array의 요소 존재 여부를 확인하는 includes()처럼, set()은 has()로 확인할 수 있다.
아래와 같이 set & add를 사용해 수정하니 코드가 훨씬 깔끔해졌다.
// 수정 전 : includes & push 사용 generateRandomNumber() { const computerArray = []; while (computerArray.length < 3) { const newComputerNumber = Random.pickNumberInRange(1, 9); if (!computerArray.includes(newComputerNumber)) { computerArray.push(newComputerNumber); } } return computerArray.join(''); } // 수정 후 : set & add 사용 generateRandomNumber() { const generatedNumber = new Set(); while (generatedNumber.size < 3) { const newComputerNumber = generateNumberInRange(1, 9); generatedNumber.add(newComputerNumber); } return Array.from(generatedNumber).join(''); } }
JavaScript
복사

이름은 직관적으로

메서드명, 파일명, 클래스명을 전체적으로 살펴보며 아래와 같이 누구나 알아볼 수 있게 수정했다. 물론 주석을 사용해 각 메서드와 클래스가 어떤 역할을 하는지 써도 나쁘지 않지만, 사실 가장 좋은 건 이름 자체에서 무슨 역할을 하는지 알 수 있도록 하는 것이다.
// 1. 게임 진행중 입력 값이 유효한지 판단하는 메서드 이름 수정// 보통 boolean을 return하는 함수는 is ~ 이런 식으로 쓰기 때문 checkValidNumberDuringGame → isValidGameInput // 2. 입력 값의 유효성을 검사하는 파일 이름 수정// 동사도 좋지만 '역할'을 분명히 드러내면 더 좋다. checkValid.js → validator.js // 3. 컴퓨터가 랜덤한 숫자를 생성하는 클래스 이름 수정// 컴퓨터 라고 이름만 써놓는 것보다 컴퓨터가 하는 '역할'을 분명히 드러내면 더 좋다. Computer.js → Generator.js
JavaScript
복사
그리고 책 <ThoughtWorks Anthology>에 나오는 객체 지향 생활 체조 원칙에서는 이름을 무작정 줄이기보다 간략하고도 명확하게 하라고 말한다. "누구나 실은 클래스, 메서드, 또는 변수의 이름을 줄이려는 유혹에 곧잘 빠지곤 한다. 그런 유혹을 뿌리쳐라. 축약은 혼란을 야기하며, 더 큰 문제를 숨기는 경향이 있다. 클래스와 메서드 이름을 한 두 단어로 유지하려고 노력하고 문맥을 중복하는 이름을 자제하자."

비즈니스 로직이 아닌 함수들을 utils.js에 분리

흔히 프로그래밍을 하면서 '비즈니스 로직', '도메인 로직'에 대해 이야기한다. 들어는 봤지만 해당 개념에 잘 몰라서 공부해 보고 (참고: 비즈니스 로직, 도메인 로직이 도대체 뭐지?) 이번 미션 코드에도 적용했다. 아래와 같이 utils.js라는 파일을 따로 만들어 export 하고, 해당 함수가 사용되는 곳에 import 했다.
// utils.js// 사용자에게 메시지를 출력하는 함수const printMessage = (message) => Console.print(message); // 컴퓨터가 랜덤한 숫자를 생성하는 함수const generateNumberInRange = (min, max) => Random.pickNumberInRange(min, max); // 에러를 출력하는 함수const throwError = (message, condition = true) => { if (condition) throw new Error(message); }; // ... 생략export { printMessage, generateNumberInRange, throwError };
JavaScript
복사
참고한 글에 따르면, 도메인 로직은 현실 문제에 대한 의사결정을 하는 코드이며, 나머지 코드는 그 결정을 위한 입력값을 만들어주거나, 그 결정의 결과물을 해석하고 보여주고 전파하는 코드라고 한다. 이런 기준을 세워서 우리가 도메인 로직과 아닌 것을 나누는 이유는 명확한 관심사의 분리를 위해서다.
도메인 로직과 아닌 것을 잘 나누고 결합도를 낮추면, 개발자가 로직을 이해하기 쉬워진다. 이런 이유로 클린 아키텍처에서는 다른 계층들은 도메인 로직에게 입력을 전달하고, 변화를 외부로 전달하는 역할을 하도록 명확하게 분리한다고 한다. 앞으로의 미션에서도 관심사 분리에 더 신경 쓰면서 코딩해야겠다고 생각했다.

else보다 early return

우테코 클린코드 체크리스트에 따르면, else 예약어 사용을 지양한다고 한다. 물론 이번 미션에서는 이 원칙을 준수하라고 따로 말은 하지 않았지만 클린코드를 염두하며 코드를 짜려고 했다. 서칭해 보니 지난 기수에서는 else를 포함한 else if, switch문도 지양하면 좋다는 말이 있어서 되도록이면 early return을 하고자 했다. (물론 이 부분은 사람마다 갑론을박이 있을 수 있고, 팀마다 회사마다 다를 수 있으니 참고만 하자.)
// 수정 전 async handleUserInputEndGame(input) { const isValidGameInputEndGame = [GAME_SELECT.RESTART, GAME_SELECT.END]; if (!isValidGameInputEndGame.includes(input)) { throwError(ERROR_MESSAGE.INCORRECT_VALUE); } switch (input) {// switch문 사용case GAME_SELECT.RESTART: this.restartGame(); break; case GAME_SELECT.END: printMessage(LOG_MESSAGE.END_GAME); break; } } // 수정 후 async handleUserInputEndGame(input) { const isValidGameInputEndGame = [GAME_SELECT.RESTART, GAME_SELECT.END]; if (!isValidGameInputEndGame.includes(input)) { throwError(ERROR_MESSAGE.INCORRECT_VALUE); return;// early return } if (input === GAME_SELECT.RESTART) { this.restartGame(); return;// early return } if (input === GAME_SELECT.END) { printMessage(LOG_MESSAGE.END_GAME); } }
JavaScript
복사

☕️ 1주 차 소감

망설임 없이 가자

우아한테크코스를 지원한 이유는 스스로를 망설일 새 없이 부딪히고 성장할 수밖에 없는 환경에 두고 싶기 때문이었다. 다행히 프리코스를 통해 망설임과는 멀어지고 있는 것 같다. 이전에는 적극적으로 시도하지 않았던 코드 컨벤션, 클린 코딩, 관심사 분리, 테스트 코드 등을 망설임 없이 찾아보고 코드에 녹여내고자 했다. 이러한 시도들을 기반으로 앞으로의 미션에 더 자신감 있게 도전할 수 있을 것 같다.

묘미는 문제 해결

개발자는 많은 문제를 만날수록 성장한다고 믿는다. 하지만 믿는 것과는 다르게 실천하는 것은 어렵고, 즐겁게 하는 것은 더더욱 어려운 것 같다. 이번 미션 수행을 하면서 예상하지 못한 에러들을 마주하며 너무나 고되었지만, 해결한 뒤 성취감은 이루 말할 수 없었다. 역시 프로그래밍의 묘미는 문제를 해결하는 것이라는 말을 다시 한번 실감했다. 앞으로는 에러를 만나면 오히려 성장의 기회라 생각하고 신나게 반겨주어야겠다.

‘그렇구나’는 화살

그동안 개발을 강의나 책으로 학습할 때는 ‘그렇구나’ 하고 넘겼던 것들이 비로소 화살(?)이 되어 돌아왔다. 이번 미션을 하면서 사소한 실수가 에러의 원인이 되거나, 기본적인 개념을 확실하게 알지 못해 스스로 놀랐던 적이 있다. 하지만 이를 성장의 계기로 삼아 다시 공부하면서 부족한 것을 채울 수 있었다. 앞으로는 사소한 개념이라도 화살이 되지 않도록 반드시 예제 코드를 써보거나 작게라도 만드는 연습을 하며, 확실히 내 것으로 만들어야겠다.