IT일상

우아한테크코스 5기 프론트엔드 프리코스 회고 본문

우아한테크코스

우아한테크코스 5기 프론트엔드 프리코스 회고

solo5star 2022. 11. 22. 23:31
리뉴얼 된 블로그로 보기: https://solo5star.dev/posts/21/

 

어느덧 우아한테크코스 프리코스 기간이 끝나간다. 4주라는 기간이었는데 긴 시간이지만 너무 빠르게 지나간 듯 했다. 마지막 주 미션을 마무리하며 프리코스 기간에 대한 회고를 남겨보려고 한다.

 

프리코스란?

우아한테크코스의 지원 과정은 다른 부트캠프와 다르게 4주간 프리코스 기간을 가지며 각 주마다 주어진 과제를 해서 제출해야 한다. 프리코스 기간이 끝나고 중간 합격 발표가 나오며 프리코스를 통과한 사람은 최종 코딩테스트를 통해 최종 합격자가 결정되는 구조이다.

 

5기에서는 백엔드 100명, 프론트엔드 50명, 모바일 안드로이드 25명을 선발한다. 참고로 필자는 프론트엔드로 지원하였다. 우테코에 지원한 사람들은 모두 슬랙에 초대되는데 그 인원이 무려 3300명이나 된다.

 

프리코스 진행 방식

1주일씩 총 4번의 미션이 있으며 백엔드는 Java로, 프론트엔드는 Javascript로, 모바일 안드로이드는 Kotlin으로 과제를 진행하고 제출한다. 제출은 GitHub Pull Request로 하며 일찍 제출한 사람들의 코드를 볼 수도 있다. (...)

 

각 미션이 끝나고 일괄 피드백도 해주시는데 도움이 되는 내용이 많았다. 커뮤니티가 슬랙 외에도 GitHub discussions이 있는데 아고라, 피어 리뷰, 학습 컨텐츠, 회고록 게시판이 있으며 활성화가 매우 잘 되어있어 의아한 부분에 대해서 지원자끼리 토론을 하거나 서로 맞리뷰도 할 수 있다. (피어 리뷰는 끝난 미션에 한해서만 가능하다)

 

필자는 피어 리뷰 요청을 올렸었는데 정말 많은 사람들이 리뷰해줘서 놓치고 있었던 부분들을 꼼꼼히 체크할 수 있었던 것 같다. 물론 맞리뷰도 다니며 다른 사람들의 코드를 보고 배운 것도 많았다.

 

1주차 미션, 온보딩 (javascript-onboarding)

수 많은 지원자들이 처음 마주친 관문! 바로 온보딩이다. 7문제가 있었으며 백준이나 코딩테스트에서 볼 법한 문제들이었다. 다른 점이라면 일주일이라는 넉넉한 기한과 빈약한 테스트 케이스라는 점이다.

 

첫 주차엔 모르고 넘어갔었지만 프리코스에서 추구하는 역량이 무엇인지 알 수 있는 대목이라고 생각한다. 현실의 문제를 해결하는데엔 테스트 케이스를 충분히 준비해주지 않는다. 따라서 요구사항을 꼼꼼히 잘 읽으면서 기능을 구현해야 한다. 실무를 해본 적은 없지만 그 어떤 코딩 테스트보다도 실무에 가깝다는 느낌을 받았다.

 

7문제 중 5문제는 단순 구현, 문자열, 그리디 같은 쉬운 난이도의 문제였다. 백준으로 따진다면 브론즈 정도라고 생각한다. 문제 6, 7 또한 백준을 평소에 많이 풀었다면 크게 어려운 정도는 아니지만 앞의 문제보다는 어려운 편이었다. 문제 6번은 N이 10,000이며 (테스트 케이스는 10,000으로 제공하지 않는다!) 해시 맵을 사용해야 하고, 문제 7은 마찬가지로 N이 10,000이고 그래프 이론 문제이다. 난이도는 실버 4~5 정도 되는 것 같았다.

 

1주차 미션은 코드를 간단하게 만드면서도 확장에 열려있도록 노력하였다.

function getScore(pages, strategies = [addStrategy, multiplyStrategy]) {
  const candidates = pages.map(page => strategies.map(strategy => strategy(page))).flat();
  return Math.max(...candidates);
}

function addStrategy(page) {
  return String(page).split('').map(Number).reduce((a, b) => a + b);
}

function multiplyStrategy(page) {
  return String(page).split('').map(Number).reduce((a, b) => a * b);
}

위 코드는 1번 문제 코드의 일부이다. 요구사항에서 점수를 계산하는 방법은 덧셈과 곱셈 두 가지 였지만 요구사항이 변경될 수 있다는 상황을 가정하여 default parameter를 통해 다양한 방법을 지원하도록 해주었다.

 

그리고 필요하다면 클래스 또한 적극 활용하였다. 관리할 상태가 발생하고 함수만으론 코드가 지저분해질 것 같은 경우 클래스를 사용하였다.

class SocialNetwork {
  #friendRelations = new Map()

  #addFriend(username, friend) {  }

  addFriendRelation(friendA, friendB) {  }

  getFriends(username) {  }

  getFriendRecommendations(user) {  }
}

문제 7번의 코드 중 일부이다. 친구 관계라는 상태를 지속적으로 유지할 필요가 생겨 SocialNetwork라는 클래스를 만들었다.

 

피어 리뷰 요청

 

PR에 달린 수 많은 댓글들...

최선을 다해 좋은 코드를 만들었다고 생각했지만 필자가 모르는 좋지 못한 습관이나 좋은 팁들이 있지 않을까 싶어 피어 리뷰를 요청했는데, 정말 많은 사람들이 피어 리뷰를 해주셨다!!

 

* 메세지를 별도로 분리하여 향후 i18n 지원에 대비

* let 대신 const를 사용

* 구조 분해 할당 파라미터 적극 활용

* 읽기 힘든 고차 함수를 별도 함수로 분리

* 성능 문제로 delete 사용 지양

* i와 같은 의미 없는 변수명 지양

 

이와 같은 많은 피드백을 얻을 수 있었고 다음 미션을 진행하는데 소중한 거름이 되었다.

 

2주차 미션, 숫자 야구 게임(javascript-baseball)

이번 미션부터 과제 형식으로 나온다. 기능 요구 사항을 꼼꼼히 읽고 기능을 구현하면 된다.

다음 미션이 될 때 마다 요구사항이 추가되는데, 2주차에 추가된 요구사항은 아래와 같다.

 

* 구현할 기능 목록 작성하기 (docs/README.md)

* airbnb 코드 스타일 적용하기

* indent의 depth는 최대 2까지

* Jest를 활용하여 기능 정상 작동확인하기

 

구현은 아래와 같이 실행되는 숫자 야구 게임을 만들면 된다.

숫자 야구 게임을 시작합니다.
숫자를 입력해주세요 : 123
1볼 1스트라이크
숫자를 입력해주세요 : 145
1볼
숫자를 입력해주세요 : 671
2볼
숫자를 입력해주세요 : 216
1스트라이크
숫자를 입력해주세요 : 713
3스트라이크
3개의 숫자를 모두 맞히셨습니다! 게임 종료
게임을 새로 시작하려면 1, 종료하려면 2를 입력하세요.
1
숫자를 입력해주세요 : 123
1볼
...

 

스파게티 코드가 되지 않도록 설계를 먼저했다. 그 첫 번째 단계로, 가장 중요한 도메인 로직을 분리하였다.

 

도메인 로직을 분리하기 위해 이 프로그램에서 사용되는 객체를 정의해보았다. 공(숫자 3개)와 상대방(컴퓨터) 두 개로 정의할 수 있었다. 상대방이라는 이름은 Enemy, Opponent 같이 쉽게 지을 수 있었으나 공의 이름을 짓기가 쉽지 않았다. Ball로 사용하기엔 볼의 갯수, 스트라이크의 갯수와 충돌되어 balls라는 변수명이 등장했을 때 볼의 갯수인지 숫자 3개를 추상화한 클래스인지 알기 어려울 것이다. 이에 떠오른 방안이 고유명사이며 한국어 발음인 Gong을 그대로 쓰기로 했다.

 

이 외에는 constants(상수), game(게임 한 판), messages(메세지)으로 비교적 간단하게 분리할 수 있었다.

 

결과, 아래와 같은 디렉토리 및 파일 구조가 나올 수 있었다.

+ src
  + models  --------- 엔티티를 모아둔 폴더
    └ gong.js  ------ 서로 다른 숫자 3개를 하나의 개념으로 취급하기 위한 엔티티
    └ opponent.js  -- 상대방(컴퓨터)을 정의한 엔티티
  └ App.js  --------- 프로그램의 진입점(entrypoint)이며 각 게임 실행의 책임을 맡음
  └ constants.js  --- 런타임 중 사용되는 상수들을 관리
  └ game.js  -------- 한 차례의 게임 중 사용되는 로직을 책임짐
  └ messages.js  ---- 런타임 중 사용되는 메세지들을 관리

 

먼저 설계를 해놓으니 코드를 작성하다가 엎을 일이 거의 없어졌다. 물론 100% 설계대로 되는 일은 거의 없지만 갈아 엎으면서 낭비되는 시간을 크게 줄여줬던 것 같다.

 

한 가지 아쉬운 점이 있다면, 도메인 분리는 충분히 만족스러웠지만 도메인을 테스트하는 테스트 케이스 작성이 없었다는 점이 아쉬웠다. 최종 실행에서의 확인만을 통해 정상 동작을 확인하였는데 기껏 분리해놓고 아까운 짓이었다.

 

이 주차의 피어 리뷰는 올리진 않았지만 고마우신 분께서 피어 리뷰를 남겨주시고 갔다. 덕분에 좋은 피드백을 많이 얻을 수 있었다..!

 

요약하자면 아래와 같다.

 

* npm install --save-dev eslint-config-airbnb eslint-plugin-import eslint-plugin-react eslint-plugin-react-hooks eslint-plugin-jsx-a11y 대신 npx install-peerdeps --dev eslint-config-airbnb 으로 간단하게 airbnb config를 설치할 수 있다.

* 파일과 클래스 명을 동일하게 하는 것이 좋다. (airbnb 가이드에도 관련 내용 有)

* constructor(opponent = null) 대신 constructor(opponent = new Opponent()) 사용

 

이 외에도 많은 의견을 나누었으며 다른 개발자들이 나의 코드를 어떻게 보는지 알 수 있었던 것 같다. 좋은 부분은 최대한 살리고 지적받은 내용은 개선하려고 노력할 것이다.

 

3주차 미션, 로또(javascript-lotto)

주차가 지날 수록 미션이 어려워진다는 것을 느끼게 된다. 코드를 조여오는(...) 요구사항도 추가되고 구현할 기능도 많아졌다. 아무튼, 이번 주차에 추가된 프로그래밍 요구사항은 아래와 같다.

 

* 함수의 길이가 15라인을 넘어가지 않도록 구현한다.

* else를 지양한다.

* 도메인 로직에 단위 테스트를 구현해야 한다.

 

사실 15라인을 넘을 일은 정말 많지 않으나 가~끔씩 15라인을 넘을 때가 있는데 이때 머리가 터진다...

 

기능 구현은 아래와 같이 프로그램이 수행되도록 하면 된다.

구입금액을 입력해 주세요.
8000

8개를 구매했습니다.
[8, 21, 23, 41, 42, 43]
[3, 5, 11, 16, 32, 38]
[7, 11, 16, 35, 36, 44]
[1, 8, 11, 31, 41, 42]
[13, 14, 16, 38, 42, 45]
[7, 11, 30, 40, 42, 43]
[2, 13, 22, 32, 38, 45]
[1, 3, 5, 14, 22, 45]

당첨 번호를 입력해 주세요.
1,2,3,4,5,6

보너스 번호를 입력해 주세요.
7

당첨 통계
---
3개 일치 (5,000원) - 1개
4개 일치 (50,000원) - 0개
5개 일치 (1,500,000원) - 0개
5개 일치, 보너스 볼 일치 (30,000,000원) - 0개
6개 일치 (2,000,000,000원) - 0개
총 수익률은 62.5%입니다.

 

이번 주차에 추가된 요구사항인 "도메인 로직에 단위 테스트를 구현해야 한다" 를 지키려면 도메인 분리는 필수다! 필자는 필수든 선택이든 분리는 하겠지만, 지금까지 분리하지 않고 구현하던 사람도 이번 주차부터는 필수로 분리해야 된다는 뜻이다.

 

지난 주차에서 분리했던 방식을 그대로 하되, 좀 더 탄탄한 프로그램을 위해 이러한 것들을 도입하였다.

 

* MVC 구조 채택 (Model, View 분리. Controller는 App.js가 수행)

* 커스텀 에러 클래스 구현

* 제너레이터 문법을 사용하여 콜백 지옥에서 탈출

 

MVC는 기본중의 기본이라 할 수 있을 만큼 많은 사람들이 적용하였다. 필자 또한 MVC 분리가 필요하다 생각하여 이번 미션에서 적극 도입하였다.

src/
  constants/
    Messages.js  ------ 프로그램에서 출력되는 메세지를 여기에서 관리
  domains/
    Lotto.js  --------- 번호 6개를 가지는 로또 클래스
    WinningLotto.js  -- 번호 6개+보너스 번호를 가지는 당첨 로또 클래스
    Reward.js  -------- 당첨의 조건 및 보상을 담는 클래스
  errors/
    AppError.js  ------ 프로그램에서 발생하는 에러 클래스
    LottoError.js  ---- 로또의 로직과 관련된 에러 클래스
    PromptError.js  --- 프롬프트 입력과 관련된 에러 클래스
  views/
    Prompt.js  -------- 입력 및 출력의 책임을 담당하는 클래스
  App.js  ------------- 도메인과 뷰를 사용하여 프로그램을 처음부터 끝까지 수행
  Lotto.js  ----------- domains/Lotto.js 리다이렉션을 위한 파일

domains 폴더는 MVC의 Model, views 폴더는 MVC의 View 이다. 입, 출력을 위한 함수는 모두 Prompt.js에만 있다. App.js에서는 Prompt를 사용하여 입, 출력을 하게 된다.

 

커스텀 에러는 요구사항 중 "모든 에러 메세지는 [ERROR]로 시작해야된다" 를 보고 도입하게 되었다. 커스텀 에러가 알아서 [ERROR] 접두사를 붙여주면 에러를 생성하는 측에선 메세지만 넘겨주면 되니까.

 

// 작동은 하지만 테스트를 통과하지 못합니다
const input = await new Promise((resolve) => Console.readLine('Input your text: ', resolve));

콜백 지옥을 탈출하려면 사실 모두가 잘 아는 async/await 문법을 쓰면 된다. 하지만 이 문법을 쓰면 테스트가 통과되지 않는다... 테스트에서 비동기를 사용하지 않기 때문에 async함수를 await해주지 않고 그냥 끝나버린다. 따라서 테스트에서의 동기/실제 실행에서의 비동기를 모두 지원할 수 있도록 제너레이터 문법을 도입하였다.

 

아래와 같은 유스케이스를 고려하여 Prompt라는 클래스를 만들었다.

 new Prompt(function*(prompt) {
   prompt.print('반갑습니다! 나이가 어떻게 되시나요?');
   yield '당신의 입력 : ';
   prompt.print(`당신은 ${prompt.read()}살 이군요!`);
   prompt.print('당신의 거주지가 어떻게 되시나요?');
   yield '당신의 입력 : ';
   prompt.print(`${prompt.read()}! 좋은 곳이네요.`);
 }).start();

yield 키워드는 호출한 측으로 값을 넘겨주고 함수가 일시정지된다는 특성이 있다. 이를 이용하여 비동기든 동기든 callback이 실행되었을 때 일시정지된 함수를 다시 실행시키면 async/await 비슷한 구현이 된다.

 

설계대로 잘 구현할 수 있었고 아래와 같이 사용할 수 있는 코드로 만들 수 있었다.

// Lotto 객체는 로또 한 장 이라는 개념을 구현했습니다!
//
const lotto1 = new Lotto([1, 3, 7, 21, 23, 37]); // 로또 한 장 생성
const lotto2 = Lotto.fromRandom(); // 랜덤으로 로또 한 장 생성
const lotto3 = Lotto.fromString("1,3,7,21,23,37"); // 문자열로부터 로또 한 장 생성

// WinningLotto 객체는 당첨 번호를 가진 로또라는 개념을 구현했습니다!
//
const winningLotto = new WinningLotto(Lotto.fromRandom(), bonusNumber);
const matchCount = winningLotto.countMatchNumber(lotto1); // 로또 번호 몇 개 맞췄는지
const isMatchBonusNumber = winningLotto.isMatchBonusNumber(lotto1); // 보너스 번호를 맞췄는지

// Reward 객체는 당첨(예: 3개 일치 (5,000원) - 1개) 개념을 구현했습니다!
//
const myReward = new Reward('4개 일치 + 보너스 번호', 25_000, (numberCount, bonusNumber) => numberCount === 4 && bonusNumber);
const eligible = myReward.isEligible(winningLotto, lotto1); // 당첨에 적격한지 확인

// 6개 일치, 5개 일치 + 보너스 번호, 4개 일치, 3개 일치는 미리 만들어놨어요.
//
const reward = Reward.DEFAULT_REWARDS.find((reward) => reward.isEligible(winningLotto, lotto1));
Console.print(reward.toString()); // 3개 일치 (5,000원)
// Prompt 객체를 사용.
// yield는 사용자로 부터 입력을 받을 때 사용
// prompt.read(), prompt.readNumber(), prompt.print() API 를 사용하여 상호작용
new Prompt(function*(prompt) {
  yield '로또 번호를 입력하세요! : '; // 이 때 이 함수는 일시정지되며 Console.readLine 콜백에서 next()가 호출되면 다음 라인부터 실행됨
  const input = prompt.read(); // 라인 하나를 입력받음
  const lotto = Lotto.fromString(input); // 문자열로부터 Lotto를 생성. 포맷이 잘못된 경우 LottoError 발생

  yield '당첨 번호를 입력하세요! : '; // 입력이 버퍼에 저장됨
  yield '보너스 번호를 입력하세요! : '
  const winningLotto = new WinningLotto(Lotto.fromString(prompt.read()), prompt.readNumber());

  // 6개, 5개+보너스, 5개, 4개, 3개에 해당되는 Reward 중 자격이 있는
  // 객체를 달라고 요청 (null이면 해당 없음 ㅜㅜ)
  const reward = winningLotto.getRewardFor(lotto);
  if (rewawrd === null) {
    prompt.print('꽝입니다!!');
    return;
  }
  prompt.print(`${reward.toString()} 에 당첨되었습니다!!`);
});

 

테스트도 충실히 작성해주었다! 확실히 테스트가 있으니 코드를 수정하더라도 안심할 수 있었다. 매 번 실행해서 동작을 확인했는데 이게 자동으로 된다니 정말 편했다.

 

다만 Jest를 잘 사용할 줄 아는게 아니라서 테스트 코드는 엉망으로 작성되었다. ㅜㅜ 아무튼 신뢰할 수 있는 코드의 기반을 만들었다는 데에 의의를 두자.

 

 

3주차 미션 피어 리뷰 요청을 했는데 정말 많은 분이 리뷰를 해주셨다. 물론 내 댓글도 있겠지만 96개라는 아주 많은 댓글이 달렸다!

 

실수했던 부분도 발견하고.. ㅜㅜ 평소 생각없이 sort에서 사용하던 a, b 라는 변수 이름도 고찰해보는 시간을 가질 수 있었다.

 

class WinningLotto {
  constructor(lotto, bonusNumber, availableRewards) {
    // 프로퍼티에 값 할당
    this.validate();
  }

  validate() {
    // 유효성 검사
  }
}

추가로, 이번 미션에서는 유효성 검사가 아쉬웠던 것 같다. 도메인 클래스 내에서 validate 함수를 호출하여 유효성을 검사하는데 로직이 사실 다른 도메인이랑 겹치는 부분이 많다. 예를 들면 로또 번호와 보너스 번호는 똑같이 1에서 45까지의 범위를 가지는...

 

다음 미션에서는 이번 미션에서 부족했던 validation 부분을 잘 신경써서 해봐야겠다는 생각이 들었다.

 

4주차 미션, 다리 건너기(javascript-bridge)

https://youtu.be/JUrErvCiPDc

 

 

 

 

우아한테크코스 슬랙에 어떤 분이 이 영상을 올려주셨는데 이걸 보고 어떤 게임인지 한 번에 이해가 되었다. 다리는 왼쪽에서 오른쪽으로 갈 수 있으며, 위/아래 중 하나를 골라 이동할 수 있다. (잘못 고르면 다시...)

 

이번에 추가된 프로그래밍 요구 사항은 더욱 타이트한데, 다음과 같다.

 

* 함수의 길이가 10라인을 넘어가지 않도록 구현한다.

* 함수의 파라미터 개수는 최대 3개까지만 허용한다.

 

이 외에도 기본으로 제공되는 InputView, OutputView, BridgeGame, BridgeMaker 를 사용하여 구현하되 일부분 수정이 불가능한 제약사항이 있다.

 

10라인이라 아주 타이트하다. 몇몇 함수는 10라인이 넘을 수 밖에 없는데 이걸 또 분리하느라 고생했다.

 

프로그램은 아래와 같이 동작해야 한다.

다리 건너기 게임을 시작합니다.

다리의 길이를 입력해주세요.
3

이동할 칸을 선택해주세요. (위: U, 아래: D)
U
[ O ]
[   ]

이동할 칸을 선택해주세요. (위: U, 아래: D)
U
[ O | X ]
[   |   ]

게임을 다시 시도할지 여부를 입력해주세요. (재시도: R, 종료: Q)
R
이동할 칸을 선택해주세요. (위: U, 아래: D)
U
[ O ]
[   ]

이동할 칸을 선택해주세요. (위: U, 아래: D)
D
[ O |   ]
[   | O ]

이동할 칸을 선택해주세요. (위: U, 아래: D)
D
[ O |   |   ]
[   | O | O ]

최종 게임 결과
[ O |   |   ]
[   | O | O ]

게임 성공 여부: 성공
총 시도한 횟수: 2

 

디렉토리와 파일을 먼저 설계했다. 사실 최종본이긴 한데 처음에 설계했던 거랑 크게 차이나진 않는다.

📦src
 ┣ 📂domains
 ┃ ┣ 📜Bridge.js  --- 다리를 정의한 클래스
 ┃ ┣ 📜BridgeGame.js  --- 다리 건너기 게임을 정의한 클래스
 ┃ ┣ 📜Moving.js  --- 플레이어의 이동 흔적을 정의한 클래스
 ┃ ┗ 📜Player.js  --- 플레이어를 정의한 클래스
 ┣ 📂errors
 ┃ ┣ 📜AppError.js  --- 프로그램 내에서 발생할 수 있는 모든 에러
 ┃ ┣ 📜BridgeError.js  --- 다리와 관련된 로직에서 발생한 에러
 ┃ ┗ 📜ValidationError.js  --- 값 검증 중 발생한 에러
 ┣ 📂intl
 ┃ ┗ 📜Messages.js  --- 프로그램에서 사용되는 모든 메세지
 ┣ 📂utils
 ┃ ┣ 📜deepFreeze.js  --- Object.freeze의 nested 버전
 ┃ ┗ 📜Routine.js  --- 콜백 기반의 비동기 함수를 제너레이터 문법을 이용하여 async/await처럼 사용하게 해주는 유틸
 ┣ 📂validators
 ┃ ┣ 📜index.js  --- 각종 입력(다리 길이, 게임 재시작 또는 종료 등)에 대한 검증 함수들 정의
 ┃ ┣ 📜ArrayValidator.js  --- 배열 값의 검증을 수행
 ┃ ┣ 📜NumberValidator.js  --- 숫자 값의 검증을 수행
 ┃ ┣ 📜StringValidator.js  --- 문자열 값의 검증을 수행
 ┃ ┗ 📜Validator.js  --- 타입이 특별히 지정되지 않은 값의 검증을 수행
 ┣ 📂views
 ┃ ┣ 📜InputView.js  --- 입력에 대한 뷰
 ┃ ┗ 📜OutputView.js  --- 출력에 대한 뷰
 ┣ 📜App.js  --- 도메인, 뷰들을 사용하여 프로그램 기능 수행
 ┣ 📜BridgeMaker.js  --- 다리에 사용되는 타일들을 생성
 ┣ 📜BridgeRandomNumberGenerator.js  --- 0, 1 숫자를 랜덤으로 생성
 ┗ 📜constants.js  --- 프로그램에서 사용되는 상수들 정의

3주차 미션의 구조(domains, errors, views)에서 intl, utils, validators를 추가했다. intl은 그냥 메세지 저장해두는 곳이고, utils는 유틸리티 성격을 띄는 함수나 클래스가 있는 폴더다.

 

가장 많은 공을 들였던 것이 바로 validators인데, validation 특성 상 중복되는 부분이 많은 점에서 착안하여 메서드 체이닝 방식으로 validation을 수행하도록 설계했다. 예를 들면 아래처럼 말이다.

function validateLuckyNumber(value) {
  return validateNumber(value).shouldInteger().shouldPositive().shouldRangeInclusive(1, 45);
}

function validateSuperLuckyNumber(value) {
  // 기존의 validation에서 확장
  return validateLuckyNumber(value).shouldRangeInclusive(1, 10);
}

함수 이름은 대충 썼지만 강조하고 싶은 건 validation을 확장할 수 있다는 점이다.

 

// U 또는 D인지
function validateTile(value) {
  return new StringValidator(value).shouldOneOf(['U', 'D']);
}

function validateTiles(value) {
  return new ArrayValidator(value).each((validator) => validator.pipe(validateTile));
}

function validateBridgeSize(value) {
  return new StringValidator(value)
    .shouldNumeric()
    .as(NumberValidator)
    .shouldInteger()
    .shouldRangeInclusive(3, 20);
}

다리 건너기에서 사용하기 위해 Validator를 구현했고 이를 활용한 유스케이스는 이와 같다. validateTile은 하나의 타일에 대해 검증하며, validateTiles는 배열로 된 타일에 대해 검증한다. 이 때 validateTile을 재사용할 수 있게 된다.

 

class Player {
  // ...
  move(tile) {
    validate(this.isArrived()).shouldBe(false, () => new BridgeError('이미 도착했습니다!'));

    const survived = this.#bridge.getTileAt(this.getNextPosition()) === tile;
    this.#movingHistory.push(new Moving(tile, survived));
    return survived;
  }
  // ...
}

플레이어가 이미 도착한 상황에서 더 이상 움직이면 안되는 상황에서도, 위와 같이 한 줄로 간결하게 작성할 수 있다. 별 거 아닌 것 같지만 코드 라인 수를 절약하는 데 큰 도움이 되었다. 10라인이라는 제한이 있다 보니...

 

new Routine(function*(routine) {
  Console.print('반갑습니다! 나이가 어떻게 되시나요?')
  yield (resolve) => InputView.readAge('당신의 입력 : ', resolve);
  prompt.print(`당신은 ${routine.withdrawReturn()}살 이군요!`);
  prompt.print('당신의 거주지가 어떻게 되시나요?');
  yield (resolve) => InputView.readCity('당신의 입력 : ', resolve);
  prompt.print(`${routine.withdrawReturn()}! 좋은 곳이네요.`);
});

3주차 로또 미션과 마찬가지로 제너레이터를 활용한 비동기 처리 클래스를 만들어주었다. 다만 이번 요구사항에서 Console.readLine은 InputView에서만 사용할 수 있다는 조건이 추가되어 이전과 같은 코드를 그대로 사용할 순 없었다.

 

이왕 더 많은 유스케이스를 수용하기 위해 Promise 생성자에 넣는 함수처럼 yield에서 resolve function을 받도록 구현해주었다. 이에 입력 뿐만 아니라 모든 콜백 기반 비동기 함수를 수용할 수 있게 되었다.

 

도메인을 구현하면서 도메인에 대한 테스트도 틈틈히 작성해주었다. 저번 주 보다 테스트 케이스를 늘렸으며 더더욱.. 신뢰할 수 있는 코드가 되었다!

 

mermaid로 만든 클래스 다이어그램

구현을 모두 완료하고 클래스 다이어그램을 만들어보았다. 처음 설계를 클래스 다이어그램으로 하는 건 익숙치 않지만 이렇게 자꾸 그려보면서 적응해보려고 한다.

 

결과적으로 아쉬운 것 하나 없는 완벽에 가까운? 코드가 완성되었다! 미션을 진행하면서 콜백 지옥에 대한 아쉬움, error 처리에 대한 아쉬움, validation 처리에 대한 아쉬움을 다음 미션을 진행하면서 고민하고 해결하고, 4주차가 되어 결국 고민했던 모든 걸 해결했던 것 같다.

 

프리코스, 빠르게 지나갔다... 빠르게 업그레이드 되었다!

4주는 분명 한 달인데 너무 빠르게 지나갔다. 매 주 미션이 나올 때 마다 고민하고 시도해보고 구현하고 반복하다보니 일주일이 하루처럼, 한 달이 일주일처럼 지나갔다...

 

그래도 내 인생에서 이렇게 집중적으로 개선을 궁리하고 시행착오하는 일은 처음인 것 같다. 다양한 사람들과 리뷰를 주고 받으면서 좋은 경험을 할 수 있었던 것 같다.

Comments