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

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


🎯 주제

이번 2주차 미션의 주제는 레이싱 게임 이었다.

1주차 숫자 야구 게임 보다는 확실히 고려해야할 사항이 많았지만 해결하지 못할 정도의 어려움이 있는 주제는 아니었던 것 같았다. 최대한 기능 구현을 빠르게하고 사용해보지 못한 기술을 접목시키거나 리팩토링에 많은 힘을 쏟고 싶었다.

🎯 회고를 통한 수정

👍🏻 중복을 최소화 하자!

시작하기에 앞서 1주차 미션에 대한 공통 피드백을 읽어보았다. 유익한 정보들이 많았으나 가장 눈에 띄었던 것은 중복을 최소화 하자였다.

어쩌면 당연한 설명이었지만 변수명을 명확하게 작성하기 위해 구체적으로 적으려 하다보니 저런 오류가 발생했던 것 같다.

// bad
const RandomNumberGenerator = {
  generateRandomNumber(size) {...}
}

// good
const RandomNumberGenerator = {
  generate(size) {...}
}

1주차에서 작성했던 랜덤 숫자를 생성하는 메서드명을 의미가 중복되지 않게 generate()로 변경하였다. 훨씬 깔끔해진 느낌이 든다.

👍🏻 기능을 분리하자!

사실 1주차 미션에서 애를 먹었던 부분이 바로 상수화한 파일을 어디로 분리를 해야하는가였다. 주변 친구들한테도 많이 물어보고 인터넷을 검색했을 때에는 상수화한 파일은 domain 에서 관리가 되어야 한다고 했고, 실제로도 domain 안에 작성하였다. 하지만 상수를 관리하는 파일이 domain 안에 있다보니 파일을 관리하기 힘들어서 이번 2주차에서는 static이라는 파일로 관리하였다.

또한 Input의 유효성 검사를 controller에서 진행했지만 유효성 검사는 충분히 View 혹은 domain에서 가능할 것 같다고 판단했다. 너무 많은 기능을 controller에 위임하는 것 같아서 InputView에서 입력값을 받는 즉시 유효성 검사를 하도록 변경하였다.

  async readCarsName(callback) {
    try {
      const input = await Console.readLineAsync(PRINT_MESSAGE.inputNames);
      InputValidator.validateCarName(input);
      callback(input);
    } catch {...}
  }

👍🏻 if()보다는 try…catch 사용하기

위에 있는 코드를 보더라도 유효성 검사를 할 때 try...catch문을 사용했다. try...catch문은 예외 처리를 효과적으로 할 수 있고 예외를 적절히 처리할 수 있게 해준다. if()문을 사용하면 예외를 적절하게 처리하지 못하고 예외가 그대로 전파될 수 있으므로 예외 처리를 제대로 하지 않는 경우에는 예기치 않은 동작을 유발할 수 있다.

try...catch문을 비동기에서 사용한다면 Promise 내부에서 발생한 예외를 즉시 처리할 수 있기 때문에 더 좋다.

🎯 문제 요구 사항

2주차 미션부터 추가된 요구 사항이 새로 생겼다. 무엇보다 가장 눈에 띄었던 부분은 Jest를 이용한 테스트 코드 작성이었다.

최근들어서 TDD의 중요성이 강조되기 때문에, 이번 기회에 테스트 케이스를 직접 구현해보며 질적인 측면에서 내 코드를 개선해보고 싶었다. 코딩테스트 문제를 풀더라도 나는 항상 코드를 제출만 하는 입장이었지, 테스트하는 것은 나와 거리가 멀다고 생각했었기 때문에, 조금 능동적으로 코드를 작성할 수 있을 것 같았다.

🎯 기능 목록 작성

## 기능 목록

- [x] 자동차 이름 입력 기능
- [x] 자동차 이름 입력값 유효성 검사 기능
- [x] 시도할 횟수 입력 기능
- [x] 시도할 횟수 입력값 유효성 검사 기능
- [x] 랜덤값을 이용한 각 횟수마다 전진여부 출력 기능
- [x] 최종 우승자 계산 기능

이번 기능 목록을 작성할 때에는 전보다 데이터를 어떻게 이용할 것인가에 대해 고민을 많이 했다. 아래에서 조금 더 자세히 설명하겠다.

🎯 고난 그리고 배움

❌ 기능 구현의 방향성 수정

기존에는 자동차 이름을 담은 배열 하나, 이름의 개수만큼 랜덤의 숫자가 생성되어 있는 배열 하나 이렇게 생성하려고 했다. 하지만 이렇게 큰 배열 두개를 모두 관리할 수 있는 방법에 대해 다시 고민하게 되었다. 못하지는 않을 것 같긴 한데 흠…로직이 너무 꼬일 것 같았다.

➡️ 처음 입력값을 받는 자동차 이름을 하나의 인스턴스로 생성하고 반복문을 통해 직접 랜덤으로 생성된 숫자와 비교해가며 실시간으로 거리를 증가시키고 출력했다. 훨씬 로직이 전보다 간결해졌다.

처음 기능 목록을 작성하고 로직을 작성하는 것의 중요성을 다시한번 느낄 수 있었다. 틀을 구체화하여 시간을 많이 투자하는 것이 전혀 아깝지 않은 부분이 바로 이 로직을 작성하는 부분 같다.

❌ 왜 최종 우승자 이름이 안나오지?

이번에는 index.js에서 디버깅을 할 수 있었다. 하지만 이상하게 최종 우승자 출력이 되지 않았다. 그 어떤 에러도 발생하지 않고 그냥 출력값이 없었다. 코드에서 undefined를 받는 것이 분명했다.

천천히 우승자를 출력하는코드를 하나하나 분석해보니 아래에서 문제를 발견할 수 있었다.

// 최종 리팩토링을 하기 전 코드

// before
let finalDistance = [];
finalDistance = this.#cars.map((car) => {
  car.getCurrentDistance();
});
const maxDistance = Math.max(...finalDistance);

// after
let finalDistance = [];
this.#cars.map((car) => {
  finalDistance.push(car.getCurrentDistance());
});
const maxDistance = Math.max(...finalDistance);

사실 처음에 왜 저렇게 작성한 지 이해가 안간다…(졸면서 코드를 작성했나..?) 물론 최종 코드와 위 코드는 많이 다르지만 배열을 이미 선언해두고 push를 하는게 아닌 배열에 forEach로 순회한 값을 통째로 넣어버렸다. 많은 시간을 쓰며 고민한 부분은 아니었지만 아주 좋은 경험이었다.

❌ 테스트 케이스에서의 오류… 난 분명 예외 처리까지 했는데..?

기능 구현을 마치고 테스트를 한번 돌렸는데 에러가 발생했다. 지난주 에러때문에 고생했던 장면이 스쳐지나갔다. 분명 지난주에 완벽히 에러에 대해 파악했다고 생각했는데 무슨 에러인지 너무 궁금했다. 발생한 에러가 모두 예외 처리와 관련되어있음을 확인하고 InputValidator 코드를 살펴봤다. 무조건 이번에 처음 사용한 try...catch와 관련이 있을 것이라고 확신했다.

async readCarsName(callback) {
    try {
      const input = await Console.readLineAsync(PrintMessage.INPUT_NAMES);
      InputValidator.validateCarName(input);
      callback(input);
    } catch (error) {
      Console.print(error);
    }
}

Console.print(error) 를 주의깊게 봤다. 흠… 그냥 에러를 출력만 해주고 에러를 처리한다는 느낌이 들지 않았다. 그래서 에러가 발생하면 해당 에러를 던지는 throw new Error(error) 를 적어주었다.

둘의 차이는 전자는 콘솔창에 찍어보기는 하지만 프로그램을 종료시키는 역할을 하는 것은 아니다. 하지만 후자는 에러가 발견된 즉시 바로 프로그램을 중단시킨다. 프로그램을 보다 안정적으로 종료하는데 사용이 된다고 한다…메모!

❌ Jest에서는 그냥 toEqual만 쓰면 안되나..?

뒤에 🎯 새로 배운 내용에서 설명을 하겠지만 Jest를 공부하면서 계속 의문이 들었던 점은 ‘그 많은 matcher를 사용할 일이 있을까?‘였다.

toEqul, toBe만 쓴다면 기대하는 값과 결과값만 비교해서 테스트 코드를 작성할 수 있을 것 같았다. 하지만 여기서 위기가 발생했다.

혹시나 해서 Jest 공식문서와 블로그 글을 찾아봤다.

“만약 **toBe**나 **toEqual**과 같은 일반적인 Jest 매처를 사용하면 스파이 함수의 호출 여부나 호출된 매개변수를 확인할 수 없습니다. 이러한 일반적인 매처는 주로 반환값이나 객체의 값에 대한 비교에 사용됩니다.”

한마디로 함수를 호출하는 것이 아닌 일반적인 값을 비교할 때에만 toBetoEqual을 사용할 수 있는 것이다.

const logSpy = jest.spyOn(Console, 'print');

test('우승자 출력 테스트', () => {
  const winners = [['pobi'], ['pobi', 'june'], ['pobi', 'june', 'wang']];
  const expectedResults = [
    '최종 우승자 : pobi',
    '최종 우승자 : pobi, june',
    '최종 우승자 : pobi, june, wang',
  ];

  winners.forEach((winners, index) => {
    OutputView.printWinnerMessage(winners);
    expect(logSpy).toHaveBeenCalledWith(expectedResults[index]);
  });
});

위 코드를 예시로 한번 살펴보면 forEach()함수로 출력함수를 호출한다. logSpy는 코드 내에서 OutputView.printWinnerMessage(winners)함수 내에서 print 에 사용되는 함수의 호출을 가로채는 스파이이다. expect(logSpy).toHaveBeenCalledWith(expectedResults[index])logSpyexpectedResults 배열에서 index에 해당하는 위치에 있는 예상 결과 메세지와 함께 호출되었는지 확인한다.

위와 같이 정해진 값이 아닌 함수의 호출여부를 파악하기 위해서는 toEqaul()을 사용할 수 없는 것이다.

여기서 또 하나의 의문이 들었다. 공식문서를 살펴보면 toHaveBeenCalledWith()는 예상 메시지의 결과가 호출되었는지를 확인한다고 쓰여져있다. 호출이 되는 것을 당연스럽게 ‘어떤 인자와 함께 호출이 된다’로 착각을 하고 예상 결과가 인자로 들어가야만 하는 줄 알았다. 여기서 호출은 결과값을 의미하기 때문에 expectedResults배열에 있는 값을 출력하느냐를 확인하는 것을 의미한다.

🎯 새로 배운 내용

🔖 Jest

우선 우아한테크코스에서 제공해준 ApplicationTest.js 파일을 분석하며 작동원리에 대해 알고싶었다. mock, jest.fn(), spyOn() 등 처음 보는 함수가 많았지만 describe , test , expect와 같은 키워드와 우아한테크코스 API를 비교하면서 기본적인 작동 원리에 대해서는 파악할 수 있었다.

처음 보는 함수에 대해서는 Jest 공식 문서를 살펴보면서 기능에 대해 이해하였습니다. 공식 문서에는 상당히 많은 양의 내장함수들이 존재하였으나 주어진 함수에 대해서만 우선적으로 공부하고, 직접 Input값과 Output에 관한 테스트 케이스에 대해서는 필요한 함수를 공식 문서에서 찾아가면서 적용해보았습니다.

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

👉 Jest를 활용하여 테스트코드 작성하기

🎯 리팩토링

✏️ airbnb styling 적용

같이 프리코스를 준비하는 다른 분들의 회고나 블로그 글을 보면 airbnb styling을 많이 적용하셨다. 사실 airbnb style guide에 대해서는 프리코스가 시작되기 직전에 개설된 디스코드에서 처음 봤다.

뭔가 흥미로운 정보가 많을 것 같아서 리팩토링을 하는 과정에서 참고했다. 많은 내용을 담고 있지만 가장 눈에 띄는 부분이 있었다.

상수가 객체인 경우 내부 프로퍼티 까지 상수로 적용하는 것은 불필요하며, 이렇게 하는 것이 아무 의미 없기 때문에 소문자로 사용하는 것을 지향한다

지금까지는 유지, 보수를 하기 위해서는 상수를 대문자로 눈에 띄게 표현하는 것이 좋다고 잘못 알았던 것이다. 생각해보니 이미 객체명을 대문자로 표현했으면 프로퍼티까지 대문자로 표현할 이유가 없었다. 고개를 끄덕거리며 모든 상수 파일 프로퍼티를 수정하였다.

// bad
const StaticNumber = Object.freeze({
  NAME_LENGTH_LIMIT: 5,
  CAN_MOVE_CONDITION: 4,
});

// good
const STATIC_NUMBER = Object.freeze({
  nameLengthLimit: 5,
  canMoveCondition: 4,
});

✏️ 자바스크립트 고차함수 적용

지난번과 마찬가지로 최대한 for() 반복문 사용을 지양하고 forEach()를 사용하려고 노력했다. 특히 배열로 두개의 값을 받아올 때 보통 인덱스로 값을 받아왔는데, 인덱스 내용은 나만 알 수 있고, 처음 코드를 보는 사람은 예측이 힘들 것 같았다.

이것을 해결할 수 있는 방법이 바로 구조분해 할당 이었고, 실제로 이를 적용해보니 가독성이 매우 좋아진 것을 확인할 수 있었다.

// bad
printMoveMarking(carMoveState) {
  carMoveState.map((car) => {
    Console.print(`${car[0]} : ${PRINT_MESSAGE.moveMarking.repeat(car[1])}`);
  });
}

// good
printMoveMarking(carMoveState) {
  carMoveState.forEach(([carName, currentPosition]) => {
    Console.print(
      `${carName} : ${PRINT_MESSAGE.moveMarking.repeat(currentPosition)}`
    );
  });
}

✏️ controller에게 너무 과분한 것을 맡기지 말자

이번 주차에서는 MVC 패턴, 특히 Model 작성에 큰 시간을 할애했다. 코드를 작성하는 동안 구현하고자 하는 기능이 domain에 들어가야 하는가? controller에 들어가야 하는가?에 대해 오랫동안 고민했다. 어떤 글을 보더라도 ‘Model은 데이터 관련 값을, View는 입력값과 출력값을, 그리고 Controller는그 둘을 연결해주는 역할’ 이라고만 명시되어 있을 뿐, 로직을 구현하는 것에 있어서 정답은 찾을 수 없었다.

처음에는 데이터의 속성만 가져오는 작업을 domain에서 했지만 이렇게 하니, controller에서는 domainview를 연결하는 작업 외에도 데이터를 가공까지 하게되면서 클래스가 복잡해졌다. 정확한 정답은 없지만 controller의 부담을 최대한 덜어주고 데이터와 관련된 모든 작업을 domain에서 하고, 대신 domain을 최대한 세분화하였다.

기존의 데이터의 속성을 가져오는 클래스를 그대로 두고, 추가적으로 그 속성을 가공하여 얻을 수 있는 데이터 값들의 집합인 클래스를 하나 더 생성하였다. 이미 domaincontroller 코드가 작성된 상태여서 기능을 분리하는 과정이 복잡하였지만 결과적으로는 훨씬 가독성도 좋고 controller의 코드도 간결해졌다.

✏️ 데이터라고 다 도메인은 아니야

이 부분은 거의 제출 마감날에 코드 리팩토링하는 과정에서 발견하였다.

tries라는 변수를 원래 도메인에서 관리하고자 하였지만 domain 명이 Cars인데 시도 횟수를 여기서 관리하는 것이 맞는가에 대한 의문이 들었다. 사실 반복문 한번에만 쓰일 변수이기 때문에 domain에서 쓰일 이유가 없는 것 같아 controller private 필드로 위치를 옮겼다.

🎯 개선해야할 점

우연히 이번 우아한테크코스를 통해서 알게된 분이 작성한 클래스 관련 글을 보았는데, domain이나 클래스 내부에서는 getter() setter() 사용을 최대한 지양해야 한다고 쓰여져 있었다. 다른 사람의 글을 읽어봐도 getter() setter()은 컨트롤러에 의해 타의적으로만 이용이 가능하며, 이는 협업을 추구하는 객체지향과 거리가 멀어 결국에는 캡슐화를 어긴다고 쓰여져 있었다. 지금까지 매번 get()set()으로 데이터를 받아왔기 때문에 이 점은 상당히 충격이었다. 아직은 낯선 방법이고, 지금 바로 완벽하게 이해해서 사용할 수는 없겠지만 앞으로 캡슐화와 객체 지향에 신경써서 코드를 작성해야할 것 같다.

🎯 마무리

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

의문을 갖고 공부를 하자

이번 주차에서는 스스로에게 물음표를 많이 던졌던 것 같다. ‘이건 왜 여기서 쓰는거지?’ ‘이게 왜 이 파일에 들어가야 하지?’ 스스로 생각을 많이 하다보니 궁금한게 많아지고 그것을 서칭하면서 코드를 작성하다보니 시간도 많이 소요됐다. 하지만 그만큼 배울 수 있는 것이 많아졌고, 무엇보다 안주하지 않으려는 모습을 볼 수 있어서 값졌다.

프리코스의 절반이 지났다. 힘들어도 조금만 힘내서 한다면 지금보다 더 많이 성장할 수 있을 것이다.

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

🧑🏻‍💻 내가 작성한 코드

👉 우아한테크코스 2주차 미션_자동차 경주

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