💻 주제
드디어 프리코스 1주차가 시작되었다. 1주차는 숫자 야구 미션을 받았다. 요구 사항이 많아서 약간 걱정이 되었지만 앞으로의 성장에 있어 큰 도움이 될 거라고 생각되어 즐거운 마음으로 구현을 시작하게 되었다.
🤯 시행착오
📄 기능 목록 작성
평소 코딩할 때 손부터 움직이는 버릇이 있다. 처음 부여된 요구사항을 본 순간 어떻게 기능 목록을 작성해야 할지 갈피를 잡지 못했다. 어떻게 하면 효율적으로 접근하여 상세하게 목록화할 수 있는지 고민해보았다. 그러나 고민할수록 정답이 없다는 것을 깨달았다. 따라서 일단 작성해보기로 결정하고 어떠한 기준으로 세분화하는 것이 좋을지 구상해보았다.
기능 목록에서는 요구사항에서 주어진 프로그램의 입력과 출력, 그리고 핵심 기능으로 구성하였다. 예외 상황에서는 더 많은 경우의 수가 있더라도 일단 생각나는 대로 작성하였고 추후에 구현하면서 필요한 부분은 추가해나가자는 생각으로 접근했다. 이를 바탕으로 다음과 같이 README.md에 작성하였다.
# 📄 기능 목록
- 입력 기능
- [ ] 사용자로부터 숫자를 입력받는 기능
- [ ] 사용자로부터 재시작 여부를 입력받는 기능
- 출력 기능
- [ ] 게임시작 문구를 출력하는 기능
- [ ] 사용자로부터 입력한 숫자의 결과 문구를 출력하는 기능
- [ ] 3개의 숫자 모두 맞을 경우 결과 문구를 출력하는 기능
- 추가 기능
- [ ] 상대방(컴퓨터) 숫자를 랜덤으로 생성하는 기능
# 🎯 예외 상황
- 숫자 입력
- [ ] 숫자가 3글자 이상인 경우
- [ ] 숫자가 아닌 다른 문자일 경우
- [ ] 중복되는 숫자가 입력있을 경우
- 재시작 여부 입력
- [ ] 1, 2가 아닌 다른 입력을 받을 경우
기능 목록을 작성한 후에 느낀 장점들이 많았다. 그 중에서도 크게 두 가지를 느꼈다.
첫 번째는 기능 단위로 커밋이 가능해졌다. 기존에도 이미 알고 있었지만, 실제로 구현하게 되면 여러 기능을 동시에 처리하게 되고, 커밋하려고 보면 너무 많은 기능을 담당하게 되어 작성한 코드를 다시 지우는 경우가 많았다. 그러나 기능 목록을 작성하고 난 후, 해당 기능을 구현 완료한 후 README에 체크하는 과정은 자연스럽게 기능 단위로 커밋할 수 있도록 도움을 주었다.
두 번째는 구현 시간이 단축된 것이다. 기존에는 어떤 것부터 시작해야 할지 갈피를 잡지 못하였다. 이런 상황에서는 필요한 기능이 추가되었을 때 기존의 코드와 충돌하면서 수정해야 하는 문제가 발생하곤 했다. 하지만 기능 목록을 작성하면서 자연스럽게 프로그램의 흐름을 파악할 수 있게 되었고, 그 결과 코드를 순차적으로 작성할 수 있게 되었다.
🛠️ 배열과 문자열 처리
배열과 문자열을 다양한 형태로 추출하고 판단하는 과정에서 자바스크립트의 기본 내장 함수에 대해 깊이 알게되었다. 내장 함수들의 역할과 어떤 상황에서 자주 사용되며, 어떻게 활용되는지에 대해 알게 되었다.
1️⃣ 사용자 입력에서 중복 요소 판단하기
InputView에서는 사용자로부터 입력을 받는 부분과 입력에 대한 에러 처리와 관련한 함수들을 작성하였다. 에러 처리를 할 때, 사용자의 입력 값 중에 중복이 있는지 확인하기 위해 Set 객체를 활용하였다. Set은 배열의 값 중 중복되는 요소들을 제거한다. 이를 이용해서 기존의 사용자의 입력 배열의 length와 Set에 할당했을 때의 size 값이 다르면 중복되는 값이 있다는 것을 의미하므로 이를 활용하여 에러 처리를하였다. 중요한 점은 배열의 길이를 구하기 위해선 length를, Set의 길이를 구하기 위해선 size를 사용한다는 것이다.
validateUserNumber: userNumber => {
const userNumberArray = userNumber.split('');
if (userNumber.length !== new Set(userNumberArray).size)
throw new Error(ErrorString.ERROR_USER_DUPLICATED_NUMBER);
}
사용자의 입력 값의 각 문자를 배열에 할당하기 위해 split 함수를 사용하였다. 입력 값에 중복되는 요소가 있는 경우, 관련된 에러 문구를 constants의 ErrorString의 정의되어있는 메시지로 에러를 출력하도록 하였다.
2️⃣ 사용자 입력에서 숫자가 아닌 요소 판단하기
사용자의 입력이 숫자인지 아닌지 판단하기 위해 정규 표현식을 사용해야 한다는 것을 알게 되었다.
validateUserNumber: userNumber => {
if (userNumber.replace(/[1-9]/g, '').length > 0)
throw new Error(ErrorString.ERROR_USER_NOT_NUMBER);
}
g는 global match를 의미하므로, 문장 전체에서 정규 표현식에 매치하는 것을 찾는다. 숫자에 해당하는 요소는 ' '로 치환하여 숫자가 아닌 요소만 남도록 문자열을 조정하였다. 해당 문자열의 길이가 0보다 크면 숫자가 아닌 요소가 하나라도 있다는 의미이므로, 예외 처리하였다.
추가로, flags 즉, modifier는 다음과 같이 설정할 수 있다.
3️⃣ 볼, 스트라이크 출력하기
사용자가 입력한 값의 볼 개수와 스트라이크 개수를 인자로 받아 해당 값들에 대한 문구를 출력하는 로직을 작성하였다. 각 문구들을 result 배열에 추가하고, 배열의 요소를 공백을 기준으로 문자열로 출력하기 위해 join 함수를 사용하였다. 코드는 다음과 같이 작성하였다.
printResultString: (ballCount, strikeCount) => {
const result = []
if(ballCount !== 0) hint.push(`${ballCount}볼`)
if(strikeCount !== 0) hint.push(`${strikeCount}볼`)
if(ballCount === 0 && strikeCount === 0) hint.push('낫싱')
MissionUtils.Console.print(hint.join(' '))
}
배열에 각 카운트에 해당하는 문구들을 push하고, 각 구분자를 공백 ' '을 기준으로 문자열로 합쳐 출력하도록 하였다.
😳 요구사항은 꼼꼼히
1️⃣ 프로그램 종료 오류
사용자로부터 게임 재시작 여부를 입력받을 때 '2'를 입력받으면 process.exit()을 통해 프로그램을 종료하고 '1'을 입력받으면 게임을 재시작하고 다시 입력을 받도록 작성하였다.
inputRestartNumber = async () => {
const restartNumber = await InputView.readRestartNumber();
if (restartNumber === '1') {
this.gameReset();
return this.inputUserNumber();
}
if (restartNumber === '2') process.exit()
process.exit()
};
그러나 이렇게 작성한 후 테스트를 돌려보았을 때, 예상치 못한 에러가 발생하였다.
에러 메시지를 보아도 이해가 되지 않았다. 코드 상으로는 문제가 없어 보였기 때문에, 혹시 프로그램 종료와 관련한 다른 조건들이 있는지 요구사항을 다시 한번 확인하였다.
다시 확인해보니 요구사항에 프로그램 종료와 관련한 요구사항이 명시되어있는 것을 확인할 수 있었다. 요구사항을 꼼꼼히 읽지 않은 나의 실수였다. 이를 확인한 후에 사용자로부터 '2'를 입력받으면 빈 Promise를 반환하여 함수를 종료하도록 코드를 수정하였다.
inputRestartNumber = async () => {
const restartNumber = await InputView.readRestartNumber();
if (restartNumber === '1') {
this.gameReset();
return this.inputUserNumber();
}
if (restartNumber === '2') return Promise.resolve();
return Promise.resolve();
};
2️⃣ 게임 재시작 오류
처음 게임이 시작된 이후, 사용자로부터 3개의 숫자를 입력받고 힌트를 출력하는 부분은 완벽하게 작동하였다. 그러나 사용자가 게임을 재시작 후 입력을 받으면 하나를 입력하면 여러 개를 받는 오류가 발생하였다.
해당 오류를 발견하고 나서 입력과 출력에는 문제가 없지만, 여러 기능이 동시에 수행되고 있다는 느낌을 받았다. 따라서 컨트롤러에 문제가 있다는 것을 파악하고 컨트롤러의 코드를 다시 살펴보았다.
inputRestartCommand = () => {
const restartCommand = await InputView.readRestartCommand()
if (restartCommand === '1') {
this.resetGame()
return this.inputUserNumber()
}
if (restartCommand === '2') return OutputView.printEndString()
}
handleInputOrEnd = (strikeCount, ballCount) => {
OutputView.printHintString(strikeCount, ballCount)
if (strikeCount === NUMBER_SIZE) {
OutputView.printResultString()
return this.inputRestartCommand()
}
return this.inputUserNumber()
}
resetGame() {
this.#computer.reset()
this.inputUserNumber()
}
문제는 바로 inputRestartCommand에서 발생하였다. 사용자로부터 '1'을 입력받으면 resetGame()을 호출하고, 사용자로부터 입력을 받는 inputUserNumber() 함수를 호출한다. 그러나 이미 resetGame() 내부에서 사용자로부터 입력을 받는 inputUserNumber() 함수가 호출되고 있었기 때문에, 동시에 입력을 받는 오류가 발생했던 것이다. 따라서 두 함수 중 하나의 호출을 제거해야 했다. 그래서 resetGame()이 초기화의 역할만 수행하는 것이 바람직하다고 판단하여 이를 제거하기로 결정했다. 수정한 코드는 다음과 같다.
inputRestartNumber = async () => {
const restartNumber = await InputView.readRestartNumber();
if (restartNumber === '1') {
this.gameReset();
return this.inputUserNumber();
}
if (restartNumber === '2') return process.exit(0)
return process.exit(0)
};
handleInputOrEnd = (ballCount, strikeCount) => {
OutputView.printResultString(ballCount, strikeCount);
if (strikeCount === NUMBER_SIZE) {
OutputView.printEndString();
return this.inputRestartNumber();
}
return this.inputUserNumber();
};
gameReset = () => {
this.#computer.reset();
};
이 두 문제를 해결하는 데에 반나절 정도의 시간을 쏟은 것 같다. 만약 요구사항을 꼼꼼히 읽었다면 이렇게 오래 걸릴 필요는 없었을 것이다. 다음 미션에서는 개발 시간을 효율적으로 활용하기 위해 요구사항을 꼼꼼히 읽고, 기능 목록 작성과 함께 프로그램에 대한 플로우 차트를 작성하는 것이 좋을 것이라고 생각하였다. 다음 미션에서는 반드시 이를 적용해보려고한다.
👨🏻💻 공들인 부분
🗂️ MVC 패턴 적용
백엔드 개발 분야에서 MVC 패턴과 관련한 이야기를 자주 듣곤 했다. 로직의 재사용성과 유지보수를 용이하게 하는 아키텍처 패턴으로 알려져 있지만, 정확한 개념과 프론트엔드에서 어떻게 적용되며, 이 미션에 적용하는 것이 적절한지에 대해 궁금하였다.
1️⃣ Model
Model은 데이터와 비즈니스 로직을 모아놓은 곳이다. 이번 미션에서 컴퓨터가 랜덤으로 생성하는 값들을 할당하고, 이를 기반으로 사용자가 입력한 숫자와 비교하여 볼의 개수와 스트라이크 개수를 얻을 수 있도록 데이터와 비즈니스 로직을 따로 분리하면 관리하기 편리하다고 생각했다.
2️⃣ View
View는 UI에 해당하는 로직들을 모아놓은 곳이다. 따라서 정의된 상수(문구)들을 기반으로 입/출력 로직을 따로 분리하면 될 것 같다는 생각이 들었다.
3️⃣ Controller
Controller는 Model과 View를 명령하여 프로그램의 흐름을 제어하는 로직들을 모아놓은 곳이다. 이처럼 숫자 야구 게임을 하나의 Controller로 관리하고 App에서 한 번 인스턴스를 생성해서 시작 지점의 함수를 호출하면 훨씬 간결한 코드를 작성할 수 있을 것이라고 생각했다.
이처럼 MVC패턴은 로직들을 역할에 맞게 분리하고 재사용성을 높일 수 있다는 점에서 큰 장점을 느꼈고, 이를 해당 미션에 적용하기 적합하다고 생각했다.
constants
모든 상수들을 index.js에 정의하여 관리하였다. 다른 파일에서 import해서 사용할 수 있고, 상수화하지 않으면 하나의 파일에서 값이 변경되면 모든 파일들을 검색해야 하는 번거로움을 해소할 수 있다.
model
컴퓨터가 랜덤으로 생성하는 값을 저장하고 볼, 스트라이크 개수의 값을 반환하는 비즈니스 로직들을 작성하여 관리하였다.
view
사용자의 입력과 예외 처리를 수행하는 InputView와 각 출력문과 숫자에 대한 볼, 스트라이크의 결과를 나타내는 출력문들은 OutputView에 관리하였다.
utils
핵심 기능을 수행하는 로직들을 모아두어 관리하였다. 여기서는 숫자를 랜덤으로 생성하는 로직을 따로 분리하여 관리하였다.
controller
숫자 야구 게임을 컨트롤 하는 NumberBaseballGameController을 생성하여 모든 과정을 수행하는 Controller를 만들어 관리하였다.
처음에는 디자인 패턴을 적용하는 것이 어색했다. 특히 로직을 올바른 역할에 따라 분리하고 있는지에 대한 확신이 없어 개발 속도가 조금 느렸다. 하지만 각 모듈 간의 의존성이 낮아져 코드가 더 깔끔해지고, 오류를 디버깅하거나 수정하는 과정에서 효율적이라는 것을 느꼈다. 다음 미션에서는 역할을 미리 정의하고 그에 따른 기능을 분리하여 개발하면 훨씬 빠르게 개발할 수 있을 것이라고 판단하였다.
🖌️ prettier & eslint을 곁들인 Airbnb 코드 스타일 적용
자바스크립트 컨벤션 중에서도 가장 많이 쓰이는 Airbnb 코드 스타일을 적용하는 것이 요구사항에 명시되어 있었다. 이전에 팀 프로젝트를 진행하면서 eslint, prettier 그리고 자동화를 쉽게 설정해주는 husky 라이브러리를 사용해본 경험이 있다. package.json을 수정하면 안된다는 제약사항이 있었기 때문에 eslint-config-airbnb와 eslint, prettier을 설치하여 적용하고 package.json와 설정 파일들을 제외하고 커밋하는 방식을 생각해냈다.
1️⃣ 설치
npm install -D eslint eslint-config-airbnb-base eslint-config-prettier eslint-plugin-import eslint-plugin-jsx-a11y eslint-plugin-prettier prettier
설치를 완료한 후에는 .prettierrc, .eslintrc 파일을 생성하여 규칙을 설정하였다. .eslintrc의 extends에 airbnb를 할당하여 코드 스타일을 적용하였다. 추가적으로 prettier와 eslint 모두 기본적인 코드 스타일을 적용하므로, 하나의 rule을 무시해주어야 충돌이 발생하지 않는다. 이를 prettier에게 맡기고, eslint는 airbnb 컨벤션에 맞게 문법 검사를 실행하도록 설정하여 충돌을 방지하였다. 추가로, 현재 프로젝트의 type이 module인 만큼 es6를 true로 설정하고, private field의 #은 Ecma 2022 이후의 문법이므로 parserOption을 따로 설정하였다.
2️⃣ .eslintrc
{
"env": {
"browser": true,
"es6": true,
"node": true
},
"extends": ["eslint:recommended", "airbnb-base", "prettier"],
"plugins": ["prettier"],
"parserOptions": {
"ecmaVersion": 2022
},
"rules": {
"import/prefer-default-export": "off",
"import/extensions": ["off"],
"class-methods-use-this": "off",
"no-alert": "off",
"no-undef": "off",
"no-new": "off"
}
}
3️⃣ .prettierrc.js
{
"singleQuote": true,
"semi": true,
"useTabs": false,
"tabWidth": 2,
"trailingComma": "all",
"printWidth": 100,
"bracketSpacing": true,
"arrowParens": "avoid"
}
prettier 설정은 구글링을 통해 가장 많이 사용하는 설정을 참고하여 설정하였다.
4️⃣ FormatOnSave 설정
vscode에서 파일 저장 시 자동으로 코드 포맷팅이 적용되도록 설정하고 싶었다. 찾아본 결과 vscode 설정에 FormatOnSave라는 옵션이 있었고, 이를 체크하여 editor.formatOnSave를 true로 설정하면 적용할 수 있었다.
이후 settings.json을 편집모드에 들어가 editor.formatOnSave가 true인지를 확인한다.
이후 파일을 저장하면 prettier에 설정된 규칙들이 다음과 같이 자동적으로 적용되는 것을 확인할 수 있다.
💡 회고
1주차 미션을 완료하며, 예상보다 많은 어려움을 겪었지만, 그만큼 새로운 것을 배우며 미션을 수행하는 데 큰 즐거움을 느꼈다. 처음 요구사항을 접하고 어떻게 접근해야 할지에 대해 많이 고민하였다. 하지만 문제를 하나씩 해결하고, 모르는 부분을 찾아가며 공부하면서 개인적으로 부족한 부분이 많다는 것을 깨달았다. 만약 이번 우테코 프리코스에 참여하지 않았다면, 내 자신이 얼마나 부족한지를 인지하지 못하고 있었을지도 모르겠다는 생각이 들었다. 이번에 느낀 점들을 다음 미션에 잘 반영하여, 더 나은 코드를 작성하기 위해 앞으로의 미션에 많은 시간을 투자해야겠다는 생각이 들었다.
⛳️ 다음 목표
- 효과적인 기능 목록 작성에 대한 나만의 방식 찾기
- 프로그램 플로우 차트 작성하기
- MVC 패턴에 익숙해 지기
🏃♂️ 구현 코드 보러가기
[숫자 야구 게임] 강병현 미션 제출합니다. by llbllhllk · Pull Request #28 · woowacourse-precourse/javascript-ba
github.com
'🚀 우아한테크코스 6기 프리코스' 카테고리의 다른 글
[우아한테크코스 6기] 최종 코딩 테스트 - 온콜 리팩토링 회고록 (0) | 2023.12.21 |
---|---|
[우아한테크코스 6기] 1차 합격 및 최종 코딩 테스트 후기 (4) | 2023.12.17 |
[우아한테크코스 6기] 프리코스 4주차 - 크리스마스 프로모션 회고록 (0) | 2023.11.20 |
[우아한테크코스 6기] 프리코스 3주차 - 로또 회고록 (0) | 2023.11.10 |
[우아한테크코스 6기] 프리코스 2주차 - 자동차 경주 회고록 (0) | 2023.11.02 |