[우아한 테크 코스 준비] - 숫자야구

jiny·2023년 7월 13일
1
post-thumbnail

숫자 야구

https://github.com/woowacourse-precourse/javascript-baseball

간단한 설명과 함께 기능 요구 사항, 프로그래밍 요구 사항, 입 출력 요구 사항, 과제 진행 요구 사항, 추가 요구 사항 등과 함께 제출 전 체크리스트와 미션 제출 방법 등이 기술 되어 있다.

기능 요구 사항 설계

초반에는 docs에 기능 요구 사항만 작성하였지만, 다른 분 들이 작성한 것들을 보며 클래스 다이어그램, 폴더 구조, 프로젝트 환경 및 코드 스타일 등을 추가 했다.

따로 추가하진 않았지만, 기능 요구 사항에 step 별로 작성, git 컨벤션 등이 들어가도 좋을거 같다고 느꼈다.

클래스 다이어 그램

개발 입문 당시 소프트웨어 설계 공학 수업 때 클래스 다이어 그램을 작성했던 이후로 정말 오랜만에 작성해서 정말 어색했고 잘못된 부분들이 존재할 거 같다. (앞으로 적응 해야 할 거 같다.)

최대한 MVC 패턴 으로 설계해보려고 했으며, 큰 범주로 컨트롤러, 모델, 뷰로 구성하였다.

컨트롤러

컨트롤러인 GameController 객체는 모델인 User, Computer, BallComparator의 데이터를 변경 하며 때론 새로운 데이터를 받아와 뷰를 그리도록 조종하는 클래스로써 설계하였다.

전체적인 숫자 야구 게임의 시작 ~ 종료의 라이프 사이클을 함께 하는 객체이며 동작 흐름은 다음과 같이 설계 했다.

  1. View를 통해 게임 시작을 알린다.
  2. Computer가 숫자를 선택 한다.
  3. User가 숫자를 선택 한다.
  4. 올바르게 선택했다면 컨트롤러가 이 모델들에게 접근하여 해당 값들을 가져온다.
  5. 가져온 값들을 바탕으로 BallComparator(받은 숫자들을 비교하여 컨트롤러에게 결과를 알려주는 역할)에게 두 값을 비교하여 ball과 strike 결과를 알려달라고 한다.
  6. BallComparator 에게 결과 값을 받으면 result 메서드를 통해 결과를 확인할 수 있으며, 3strike가 아니라면 다시 2-4을 반복한다.
  7. 3strike라면 View를 통해 결과 종료를 알리며, 유저가 입력한 값에 따라 다시 1-6를 실행하거나 완전히 종료 한다.

핵심은 사용자의 행동과 객체의 특성에 맞게 메서드를 구현하려고 했으며 모델에 변화를 가하여 데이터를 변경하고 그 데이터를 통해 뷰를 새롭게 변경하는 것을 중점으로 두었다.

모델

User, Computer, BallComparator의 경우 모델 객체로써, 내부 상태를 가지며 상태를 변경 시키기 위한 메서드 들로 구성하였다. 또한, 모델 객체 들은 컨트롤러나 뷰에 접근하지 않도록 신경 쓰기 위해 노력했다.

핵심은 구현과 관련된 내부 상태를 가지는 것이며, 모델은 오직 상태를 변경하거나 상태 값을 주는 것에 집중해야 한다. 또한, 컨트롤러와 뷰에 관련된 로직을 가질 수 없으며 접근할 수도 없다.

InputView와 OutputView는 컨트롤러로 부터 받은 모델의 데이터를 적합한 형태로 렌더링하는 로직들로 구성되어 있다.

핵심은 오직 렌더링 하는것에 집중해야 하며, 모델이나 컨트롤러에 접근할 수 없다.

etc

컨트롤러, 뷰, 모델 이외에 Computer와 User가 설정한 값이 요구 사항에 유효한지 검사하기 위한 Validator를 구성하였다.

폴더 구조

📦src
 ┣ 📂domain
 ┃ ┣ 📜BallComparator.js  --- 컴퓨터와 유저가 설정한 숫자를 비교하여 볼, 스트라이크 갯수를 전달하기 하도록 정의한 클래스 모듈 
 ┃ ┣ 📜Computer.js  --- 상대방을 정의한 클래스 모듈
 ┃ ┗ 📜User.js  --- 컴퓨터에 맞서는 유저를 정의한 클래스 모듈
 ┣ 📂constants
 ┃ ┗ 📜messages.js  --- InputView와 OutputView에 출력할 메시지들이 있는 모듈
 ┣ 📂utils
 ┃ ┣ 📜runGenerator.js  --- 제너레이터 모듈을 처리하기 위한 유틸 함수가 있는 모듈
 ┃ ┗ 📜validate.js  --- Validator와 관련된 유틸 함수들이 있는 모듈
 ┣ 📂validator
 ┃ ┣ 📜index.js  --- 유저 입력에 대한 검증 함수들 정의하는 클래스 모듈
 ┣ 📂view
 ┃ ┣ 📜InputView.js  --- 입력에 대한 뷰
 ┃ ┗ 📜OutputView.js  --- 출력에 대한 뷰
 ┣ 📜App.js  --- 컨트롤러를 통해 앱을 실행하는 모듈
 ┗ 📜GameController.js  --- 실질적인 숫자 야구 실행을 담당하는 컨트롤러가 있는 모듈

개발 셋팅

이미 개발 셋팅은 우테코 담당자 분들이 해준 상태였기 때문에 fork -> clone만 하면 개발 시작이 가능하다. 하지만, 프로그래밍 요구 사항에 AirBnB 컨벤션과, 추가 요구 사항들에 대응하기 위해 eslint와 prettier를 세팅했다.

eslintrc.js

module.exports = {
  env: {
    node: true,
    commonjs: true,
    jest: true,
  },
  parserOptions: {
    ecmaVersion: 2022,
  },
  extends: ['airbnb-base', 'prettier'],
  rules: { 'max-depth': ['error', 2] }, // 최대 deps를 2로 설정
};

prettier

{
  // 쌍따옴표 대신 홑따옴표 사용
  "singleQuote": true,
  // 모든 구문 끝에 세미콜론 출력
  "semi": true,
  // 탭 대신 공백으로 들여쓰기
  "useTabs": false,
  // 들여쓰기 공백 수
  "tabWidth": 2,
  // 가능하면 후행 쉼표 사용
  "trailingComma": "all",
  // 줄 바꿈할 길이
  "printWidth": 80,
  // 객체 괄호에 공백 삽입
  "bracketSpacing": true,
  // 항상 화살표 함수의 매개 변수를 괄호로 감쌈
  "arrowParens": "always",
  // OS에 따른 코드라인 끝 처리 방식 사용
  "endOfLine": "auto"
}

더 자세한 내용은 알아두면 쓸데있는 ESLint & Prettier 설정 방법 (feat.우아한테크코스)
을 통해 확인해 볼 수 있다.

최종 소스 코드

https://github.com/jinyoung234/javascript-baseball/tree/jinyoung

어려웠던 점

기능 요구 사항 설계

이 부분은 할 때마다 느끼지만 적절한 선을 찾지 못하고 있는거 같다.

너무 오랫동안 고민하게 되면 만약 기능 요구 사항 대로 로직이 흘러가지 않았을 때 드는 박탈감과 허무함 때문에 간략하게 설계 후 구현하면서 조금씩 변경하는 쪽으로 진행했는데 초반에 내가 작성한 README와 리팩토링을 위해 참고했던 다른 분들의 README를 비교해보니 너무 대충 작성했나 싶은 생각이 들었고 적당히 리팩토링을 했다.

다음 주제 때 다시 설계 한다면 다음과 같은 과정으로 진행할 것이다.

  1. step 별로 기능 요구 사항 들을 세부적으로 작성하기
  2. 클래스 다이어 그램에 들어갈 클래스 들에 대해 틀만 잡아 놓기
  3. 코드 스타일 작성하기
  4. 구현하면서 조금씩 수정하기
  5. 구현이 끝나면 폴더 구조와 클래스 다이어그램 마무리 하기

MVC 패턴

자바로 MVC 구현은 해봤었지만 자바스크립트로 클래스를 통해 MVC 패턴으로 구현해보려고 하니 어디서 부터 시작해야 할지 감이 오질 않았다.

컨트롤러가 모델과 뷰에 접근해서 데이터를 변경 하고 모델로 부터 가져온 데이터를 뷰에 접근하여 렌더링 하는 과정에 대한 이해를 하기 까지 생각 보다 많은 시간이 소모 되었다.

또한, 각자 객체에 대한 책임을 어떻게 구성해야 할지에 대해 나름대로 고민 했지만 eslint로 인한 무분별한 static 키워드로 인해 다른 클래스들 어디서든 접근이 가능하단 점이 구현하는 과정에서 아쉬움을 가지게 되었다. 적절한 책임과 역할에 대해 좀 더 생각하고 고민할 필요가 있음을 이번 구현 과정을 통해 느끼게 되었다.

readLine 처리

초반 구현 과정에서 readLine 처리에 대해 어려움을 겪었다. readLine이 비동기적으로 처리되는데 이를 인지하지 못해서 아래의 오류 사항에 대해 이해하는데 많은 시간이 잡아 먹혔다.

 userSelectNumber() {
  InputView.input("숫자를 입력해주세요 : ", (inputvalue) => {
    this.userNumbers = String(inputvalue).split('').map(Number)
  });
  console.log(this.userNumbers)
 }

이 로직의 경우 유저로 부터 값을 받아서 유저가 입력한 값을 userNumbers 필드에 추가 한 후 userNumbers의 값을 확인하는 것인데, 자꾸만 userNumbers가 먼저 출력되어서 이해하는데 많은 시간이 소모 되었다.

결과적으로 readLine의 경우 2번째 인자의 콜백 함수가 비동기적으로 처리(콜백 큐 -> 이벤트 루프 -> 콜 스택)되기 때문에 console.log가 먼저 출력되는 것을 알고 있었음에도 불구하고 실전에서는 어려움을 겪었다.

class Game {
 constructor() {
  this.computerNumbers = [];
  this.userNumbers = [];
 }

 // 숫자 야구 실행 (run) 
 start() {}

 // 유저가 입력한 값을 저장 후 compare 로직 실행
 userSelectNumber() {}
 
 // 컴퓨터 값과 유저의 값을 비교하는 로직 
 compare() {}
 
 // 3 스트라이크 이후 로직을 담당
 end() {}

module.exports = Game;

하지만, 끝이 나질 않았는데 리팩토링 하는 과정에서 Game 클래스의 로직이 너무 역할과 책임의 분리가 이루어지지 않은 절차지향적으로 구현되어 User 클래스와 BallComparator로 분리하여 관련 로직들을 추가하려고 했다.

// view/inputView.js
class InputView {
 static input(query, callback) {
  Console.readLine(query, callback);
 }
}

// util/index.js
const handleInputValue = (param, callback) => function (inputValue) {
  callback({ ...param, inputValue });
 };

const assignUserNumber = ({ inputValue, userNumbers, callback }) => {
 validatedValue(inputValue);
 userNumbers.splice(0, 3, ...inputValue.split("").map(Number));
 callback();
};

// Game.js
userSelectNumber(callback) {
  const param = { callback, userNumbers: this.userNumbers };
  InputView.input("숫자를 입력해주세요 : ", handleInputValue(param, assignUserNumber));
}

유저가 값을 입력 후 비교하는 로직의 메서드 흐름

userSelectNumber -> input -> handleInputValue -> assignUserNumber -> compare

콜백이 콜백을 호출하는 형태가 너무 많아서 실행 흐름을 명확히 파악하는 것이 어려웠고 분리 시 어떻게 분리해야 할지 결정하는 것이 쉽지 않았다.

흔히 말하는 "콜백 지옥" 이었다.

결국 제대로 리팩토링에 실패한 채 다시 구현하기로 하였다.

어떻게 다시 구현해야 이런 사태가 벌어지지 않을까..?!

사실 async/await를 통해 비동기 처리가 가능했지만, 테스트 코드에서 문제가 발생해서 다른 해결책이 필요 했다.

다른 대안으로 제너레이터를 활용하는 방법이 있었다.

runGenerator.js

function isTask(thing) {
  return typeof thing === 'function';
}

function whileGenerates(gen, prevGenResult) {
  if (isTask(prevGenResult.value)) {
    const task = prevGenResult.value;
    const resolve = (...args) => whileGenerates(gen, gen.next(...args));
    task(resolve); // run callback
  } else if (!prevGenResult.done) {
    whileGenerates(gen, gen.next(prevGenResult.value));
  }
}

function runGenerator(generator) {
  const gen = generator();
  whileGenerates(gen, gen.next());
}

module.exports = runGenerator;

App.js

class App {
  constructor() {
    this.controller = new GameController();
  }

  play() {
    runGenerator(this.controller.run.bind(this.controller));
  }
}

GameController.js

*run() {
  OutputView.print(GAME.RUN);
  this.computer.defineValue();
  while (true) {
    this.comparator.setReset();
    const userValue = yield User.userSelectValue;
    Validator.validateNumber(userValue);
    this.comparator.compare(this.computer.getValue(), this.user.getValue());
    const [ball, strike] = this.comparator.getResult();
    const isSuccess = GameController.result(ball, strike);
    if (isSuccess) break;
  }
  const userInputValue = yield GameController.end;
  if (userInputValue === GAME.RESTART) {
    yield* this.run();
    return;
  }
  GameController.exit();
}

runGenerator.js 의 소스 코드와 [FE] 콜백 지옥, 해결하고 싶으시죠? Generator 구경 해 보실래요? 글을 통해 제너레이터 적용에 대해 학습했고, async-await 처럼 동기적인 소스 코드 흐름을 만들어낼 수 있었다.

동기적인 소스 코드 흐름을 만들어냈을 때 장점은 다음과 같았다.

1. 콜백 지옥으로 부터 벗어나 쉬운 소스 코드 이해가 가능 하다.

이전 코드의 경우 하나의 변경을 위해 여러 유틸 모듈에 대해 클릭 해가며 로직 흐름과 소스 코드 이해를 해야 했고, 제대로 동작하는지 확인하기 위해 정말 여러번 왔다갔다 해야 했다. 그리고 여러 모듈로 분리 되어 있었기 때문에 한 모듈을 바꿀 때 마다 확인해야 해서 정말 불편 했다.

하지만 제너레이터를 통해 동기적으로 이해할 수 있도록 한 모듈에 필요한 소스 코드들을 추가 하여 한결 가독성이 개선 되었다.

2. 변경에 유연한 로직 생성이 가능 하다.

가독성이 개선되다 보니 빠르게 소스 흐름에 대해 파악할 수 있었고, 메서드 내 의존성이 많지 않았기 때문에 변경에도 이전 콜백 로직 보다 유연하게 대처할 수 있었다.

테스팅

별도의 테스트 코드가 있었지만 더 자세히 파악하기 위해선 추가적인 테스트 코드를 직접 작성해야 했다. 하지만 테스팅 할 때 에러가 발생하면 아직까지 "어디서 문제가 발생했고, 어떻게 해야 문제가 해결되겠다." 라는 확신이 생기지 않았다. 생각하고 고민하다 보니 TDD 하는 과정에서 많은 시간이 소모 되었다.

아직 테스트 하는 것이 익숙하지 않은 시점에서 TDD를 하는 것이 맞을까? 라는 생각이 들었고, 추가적인 학습과 연습이 필요하다고 느꼈다.

레퍼런스

0개의 댓글