[우아한테크코스 2022] 최종 코딩테스트 풀어보기

재오·2023년 9월 3일
31
post-thumbnail

이 포스트는 우아한테크코스 6기를 준비하며 풀어본 우아한테크코스 5기 문제입니다.


🤷🏻 문제의 이해

작년 우아한테크코스 5기의 최종 코딩테스트 문제는 점심 메뉴 추천이었다. 그동안의 프리코스 문제들은 일주일의 시간동안 해결하면 됐었는데 다른 블로그의 후기들을 살펴보니 현장에서 5시간 안에 해결해야만 한다고 했다. 실제 현장의 압박감 등을 고려하면 이렇게 연습차원에서 문제를 해결하는 것도 빠른 시간안에 가능해야할 것 같다. 해당 문제는 기능구현을 최대한 빠르게 해보고자 노력을 했고 이후에 클린코드를 접목시켰다.

📑 기능구현 목록 작성

프리코스 문제를 처음 풀 때 나를 제일 힘들게 했던 것은 기능구현 목록 작성이다. 한달동안 우아한테크코스 문제를 풀면서 찾은 꿀팁 아닌 꿀팁이 있는데 내가 최종적으로 코드를 작성하고 실행이 되는 console 창을 이용하면 기능구현 목록을 작성하는 데 조금이나마 도움을 받을 수 있다.

이번 문제를 예를 들어 설명을 해보자.

점심 메뉴 추천을 시작합니다.

코치의 이름을 입력해 주세요. (, 로 구분)
토미,제임스,포코

토미(이)가 못 먹는 메뉴를 입력해 주세요.
우동,스시

제임스(이)가 못 먹는 메뉴를 입력해 주세요.
뇨끼,월남쌈

포코(이)가 못 먹는 메뉴를 입력해 주세요.
마파두부,고추잡채

메뉴 추천 결과입니다.
[ 구분 | 월요일 | 화요일 | 수요일 | 목요일 | 금요일 ]
[ 카테고리 | 한식 | 한식 | 일식 | 중식 | 아시안 ]
[ 토미 | 쌈밥 | 김치찌개 | 미소시루 | 짜장면 | 팟타이 ]
[ 제임스 | 된장찌개 | 비빔밥 | 가츠동 | 토마토 달걀볶음 | 파인애플 볶음밥 ]
[ 포코 | 된장찌개 | 불고기 | 하이라이스 | 탕수육 | 나시고렝 ]

추천을 완료했습니다.

위 마크다운 파일은 우아한테크코스_메뉴추천에서 가져왔다.

매 줄마다 해당 출력값을 얻기 위해서는 값이 어떠한 형태로 저장이 되어야 하는지, 해당 기능을 하기위해서 별도의 데이터를 관리하는 곳이 필요한지 (MVC 패턴에 적용하기 위함) 등을 파악하며 큰 흐름을 살펴보는 것이 중요하다. 직접 끄적거리는 과정에서 기능구현 목록과 MVC 패턴 구현을 모두 만족시킬 수 있다. 이 과정은 실제로 아래 이미지와 같다.

예외처리와 구현할 기능 목록을 나누어 별도로 작성하였는데 데이터(값)를 중심으로 작성하는 것이 중요하다. 쓸데없이 많은 기능을 세세하게 적을 필요가 없다. 쓸데없이 많은 파일만 생성될 뿐이다...

앞서 우아한테크코스 프리코스 3주차에서도 말했지만 이 기능 목록을 작성하는 것이 제일 중요하다. 모든 로직의 흐름이 여기서 결정되기 때문에 이 큰 흐름이 코드를 작성하다가 무너진다면 회복하기에 꽤 많은 시간이 잡아먹힐 것이고 코드도 많이 꼬일 것이다.

🧱 Static 상수화

본 문제에서는 App.js 파일에 메뉴 카테고리와 해당 카테고리에 해당하는 메뉴들이 배열로 담겨져 있었다. 처음에는 App.js에 적혀있는 파일들을 이동시키면 안되는건가 싶었지만 문제 요구사항에 별 말이 없었기에 static.js파일로 이동시켰다. 객체로 구성하여 value값을 배열의 형태로 저장하여 필요할 때마다 뽑아서 쓸 예정이었다.

그리고 앞에서 진행한 프리코스와 마찬가지로 Model과 View에서 숫자 사용을 최대한 지양하기 위해서 숫자를 모두 static.js 파일에 상수로 저장하였다. 중간중간 코드를 작성할 때에도 예상치 못한 숫자가 등장할 때가 있어서 바로바로 상수화 해주었다.

예외처리와 출력메시지는 이제 앞에서 너무 많이 설명한 바 있어 여기서는 생략하겠다.

📁 Util 클래스 적용

Util 클래스도 앞에 프리코스에서 많이 설명한 바 있어 구체적인 설명은 생략하고자 한다. RandomGenerator.jsInputValidators.js를 적용하였고 추가적으로 메뉴가 중복되는 경우 false를 반환하는 기능을 여기서 구현하여 써먹어보려 했지만 실제로 코드에서 사용되는 부분은 한번밖에 없어서 굳이 Util 클래스에서 구현하지는 않았다.

  // 카테코리의 번호 (1 ~ 5) 중 하나를 반환
  makeRandomCategory() {
    const pickedCategory = Random.pickNumberInRange(
      StaticString.CATEGORY_START_NUMBER,
      StaticString.CATEGORY_END_NUMBER
    );
    return pickedCategory;
  },
}

🗃️ MVC 패턴 적용

처음 MVC 패턴을 적용할 때 정말 갑갑했었다. ViewContoller까지는 이해가 쉽게 됐었지만 대체 어떤 값들을 Model에 분류를 해야할 지가 정말 막막했다. 하지만 위에서 기능구현 목록을 살펴보며 필요한 데이터를 정리하는 과정에서 Model을 쉽게 구분할 수 있었다.

📕 Model

Model은 View와 Controller에 대해 몰라야만 한다. 조금더 쉽게 이해를 하자면 Model에는 다른 곳에서는 담을 수 없는 데이터들이 담겨야만 하고 이는 필요할 때 수정이 가능한 데이터여야 한다.

또한 Model 안에는 getvalidate가 주로 사용된다. 그리고 나머지 계산적인 기능은 controller에서 이루어지는 것이라고 생각하면 편하다. 그렇다면 점심 메뉴 추천 기능에서 Model 기능은 어떠한 것이 있을까?

  • Users : 입력되는 사용자의 정보에 따라 출력 및 메뉴 데이터가 변경되기 때문에 매우 중요한 정보이다.
  • Category : 카테고리가 랜덤으로 결정되고 이에 따라 추천 메뉴 데이터가 변경되기 때문에 매우 중요한 정보이다.
  • RecommendMenus : 사용자에 따라 랜덤으로 변경되는 추천 메뉴 역시 중요한 정보이다.
  • CannotEatMenus : 추천 메뉴는 못 먹는 메뉴에 따라 결정 되므로 매우 중요한 정보이다.
class Users {
  #users;

  // 입력값에 대한 예외처리 작업
  constructor(name) {
    InputValidators.validateNotProperName(name);
    InputValidators.validateOverRangeName(name);
    this.#users = name.split(",");
    InputValidators.validateOverRangeCount(this.#users);
  }

  // 이름 입력값이 ","를 기준으로 들어왔을 때 자동적으로 배열로 변환해주고 이를 리턴해주는 작업
  getUsersName() {
    return this.#users;
  }
}

📗 View

📥 InputView

여기서는 역시 콜백함수를 이용하여 각 함수에 대한 가독성을 좋게 하고자 하였다. 자세한 설명은 앞에 프리코스 2주차, 3주차를 참고하길 바란다.

  // 못 먹는 음식을 받아오기 => callback은 ,로 구분된 문자열(메뉴)을 인수로 가진 함수
  readNotEatingMenu(user, callback) {
    this.getUserInput(
      `\n${user}${GuideMessage.PERSON_NOT_EATING_MENU}`,
      callback
    );
  },

  getUserInput(guide, callback) {
    Console.readLine(guide, (input) => {
      callback(input);
    });
  },

📤 OutputView

이번 OutputView는 신경을 많이 썼다. 사용자의 입력에 따라 최종적인 출력 메뉴 배열이 달라지기 때문이다. 앞에는 사용자의 이름도 추가해서 가독성있는 배열을 출력해야 하기 때문에 OutputView에서 인자를 받아 최종 배열 문자열을 출력하고자 하였다.

 // 추천메뉴 출력
  printRecommendMenus(user, menus) {
    menus.unshift(`${user}`);
    Console.print(`[ ${menus.join(" | ")} ]`);
  },

📘 Controller

역시 시간이 은근 많이 쓰이는 곳이 Controller인 것 같다. 함수화를 하여 프로그램을 구현하는 것이 아직은 적응이 안된 것 같다. 많은 함수로 쪼개고 그 함수를 꼬리물기 하듯이 엮는 과정이 아직 복잡하게 느껴지는 것 같다. 특히 이번 Controller에 연산 기능을 몰아 넣었기 때문에 더욱 그랬다.

그나마 꿀팁은 앞에서 설명한 입출력 화면을 보면서 시간 순서대로 함수를 꼬리물기 하는 것과 같이 작성하면 된다.

// 카테고리의 종류에 따라 랜덤으로 메뉴를 배열에 넣는 코드 : array = 카테고리 배열, NotArray = 못 먹는 음식 배열 
setMenuCategory(array, NotArray) {
  this.#recommendMenu = [];
  for (let i = 0; i <= StaticString.DATE_LENGTH; i++) {
    if (MenuCategory.includes(array[i]))
      this.#recommendMenu.push(
        this.isMenuDuplicate(AllMenus[array[i]], NotArray)
      );
  }
  return this.#recommendMenu;
}

// 인수는 카테고리별 메뉴를 담은 배열이다. -> output: 카테고리별 랜덤 메뉴 하나 (재귀 함수)
isMenuDuplicate(menuArray, NotEatingArray) {
  let Random = RandomNumberGenerators.makeRandomMenus(menuArray);
  if (this.#recommendMenu.includes(Random) || NotEatingArray.includes(Random))
    return this.isMenuDuplicate(menuArray, NotEatingArray);
  return Random;
}

🔎 느낀점 : 클래스 private 잘 이용하자!

클래스 private에 관해서는 바로 전 주차에서 설명하였지만 이번 주에 이것 때문에 여러모로 애를 먹었다. 최종 출력과정에서 분명히 에러가 발생할 만한 곳을 찾기 어려웠는데 최종 메뉴추천 배열이 아예 출력되지 않는 문제가 발생했디 때문이었다. 에러라도 발생하면 어디서 문제가 발생하였는지라도 알 수 있었을텐데 그러지도 못해서 정말 1시간 반정도를 이곳에 투자했던 것 같다.

결과적으로는 private 필드선언 그리고 Model에서 정의해놓은 함수에서 오류가 발생한 것이었다.

class Users {
  #users;

  // 입력값에 대한 예외처리 작업
  constructor(name) {
    InputValidators.validateNotProperName(name);
    InputValidators.validateOverRangeName(name);
    this.#users = name.split(",");
    InputValidators.validateOverRangeCount(this.#users);
  }

  // 이름 입력값이 ","를 기준으로 들어왔을 때 자동적으로 배열로 변환해주고 이를 리턴해주는 작업
  getUsersName() {
    return this.#users;
  }
}
const MenuController {
  #users
  .
  .
  .
  this.#users = new Users(input);
  .
  .
  .
  // 최종적인 결과를 출력하는 함수
  resultCommend() {
    OutputView.printResultRecommend();
    OutputView.printDays();
    OutputView.printCategory(this.getCategoryMenus());
    for (let i = 0; i < this.#users.getUsersName().length; i++) {
      this.getPrintRecommendMenus(i);
    }
    OutputView.printCompleteRecommend();
  }

최종 Controller에서 private필드로 #users를 생성하였고 생성자 함수를 이용하여 선언도 해주었다. 그리고 해당 클래스는 기존에는 getUsersname()이라는 함수를 이용해야만 최종적인 배열도 출력하고 이용이 가능해지는 것이다. 하지만 내가 이 #users가 배열 그 자체라고 착각을 하였고 이것의 길이를 구하고자 length 프로퍼티를 이용하니 원하는 값이 나올 수가 없었던 것이다. 따라서 기존 클래스에서 정의한 this.#users.getUsersName()까지 코드를 작성하여야 원하는 배열이 나올 수 있는 것이다.

앞으로는 Clean Code 책을 읽으면서, 그리고 Jest에 대해 조금 더 공부를 하면서 지금까지 작성하였던 코드를 수정하고자 한다.
한달이라는 짧은 기간동안 우아한테크코스의 문제들을 풀어보았지만 이 짧은 시간 안에 자바스크립트라는 문법에 대해 코드로 직접 작성하면서 머리에 많이 중요한 개념들을 각인 시킬 수 있었던 것 같다. 이렇게 짧은 기간동안 많은 것을 배울 수가 있었는데 1년이라는 시간동안 이 코스에 합류해서 노력하여 배운다면 1년 뒤 나의 모습이 상상이 되지 않을 정도로 성장해있을 것 같아 설렌다. 부디 그 꿈을 이룰 수 있게끔 지금 이 순간에 노력이 헛되지 않게 공부하고자 한다.

자료출처:

profile
블로그 이전했습니다

2개의 댓글

comment-user-thumbnail
2023년 9월 11일

잘 보고 갑니다 ㅎㅎ 공부하는데 큰 도움이 됐어요

1개의 답글