본문 바로가기
우테코

[6기 프리코스 프론트] 4주차 크리스마스

by limew 2023. 11. 12.

 

이번주부터는 template repository를 사용하여 레포지토리를 생성하고 제출하는 방식으로 바뀌었다.

Template Repository 알아보기

 

먼저 저번주 공통피드백과 개인적으로 신경써야할 점을 정리했다.

클래스(객체)를 분리

  • 클래스의 역할과 책임을 생각
  • 클래스 작성 시 도메인 로직에 집중

UI는 도메인 로직과 분리

아래와 같이 작성하지 않는다

 

class Lotto {
   #numbers
   // 로또 숫자가 포함되어 있는지 확인하는 비즈니스 로직
   contains(numbers) {}
   // UI 로직
   print() {}      
}

 

함수는 공백 포함 15라인으로 제한

초과한다면 함수분리

예외 상황고민

else if 줄이기

 

필드 수를 최대한 줄인다

중복되거나 불필요한 필드는 줄인다.

class LottoResult {
   #result = new Map()
   #profitRate
   #totalPrize
}

 

위 객체의 profitRatetotalPrize는 등수 별 당첨 내역(result)만 있어도 모두 구할 수 있는 값이다. 따라서 위 객체는 다음과 같이 하나의 필드만으로 구현할 수 있다.

class LottoResult {
   #result = new Map()
   calculateProfitRate() { ... }
   calculateTotalPrize() { ... }
}

 

클래스의 필드는 private로 구현한다

class WinningLotto {
   #lotto
   #bonusNumber
   constructor(lotto, bonusNumber) {
       this.#lotto = lotto
       this.#bonusNumber = bonusNumber
   }
}

 

 

객체를 객체답게 사용하기

클래스에서 필드를 밖으로 꺼내서(get) 처리하지 말고, 필드와 관련된 로직은 클래스에서 메서드로 구현하고 밖에서 바로 이 메서드를 사용할 수 있도록 한다. (이 피드백을 보자 뜨끔했다..)

참고

 

getter를 사용하는 대신 객체에 메시지를 보내자

getter는 멤버변수의 값을 호출하는 메소드이고, setter는 멤버변수의 값을 변경시키는 메소드이다. 자바 빈 설계 규약에 따르면 자바 빈 클래스 설계 시, 클래스의 멤버변수의 접근제어자는 private

tecoble.techcourse.co.kr

 

(아래는 안 좋은 예)

 

class Lotto {
   #numbers
   constructor(numbers) {
       this.#numbers = numbers
   }
   getNumbers() {
       return this.#numbers
   }
}

class LottoGame {
   play() {
       const lotto = new Lotto(...)
       // 숫자가 포함되어 있는지 확인한다.
       lotto.getNumbers().contains(number)
       // 당첨 번호와 몇 개가 일치하는지 확인한다.
       lotto.getNumbers().stream()...
   }
}

 

이렇게 변경한다

 

class Lotto {
   #numbers
   constructor(numbers) {
       this.#numbers = numbers
   }
   contains(number) {}
   matchCount(other) {}
}

class LottoGame {
   play() {
       const lotto = new Lotto(...)
       lotto.contains(number)
       lotto.matchCount(...)
   }
}

 

객체의 상태를 외부에서 직접 접근하는 방식을 최소화 하는 이유

상태값을 갖는 객체에서는 상태값을 외부에서 직접 접근해 변경하지 못하도록 메서드만 노출시킨다.

이때, 상태값을 private으로 설정해 외부의 직접적인 접근을 막고, getter와 setter를 이용해서만 접근이 가능하도록 한다. 그러나 무조건적으로 모든 상태값을 호출하는 getter메소드를 생성하는 것이 맞을까? 

 

객체는 다른 객체와 메시지를 주고 받으며 협력한다. 객체는 메시지를 받으면 그에 따른 로직을 수행하고 스스로의 상태값도 변경한다. 간단히 말해서 객체지향 프로그래밍은 객체가 스스로 일을 하도록 하는 프로그래밍이다.

 

하지만 모든 상태값에 getter을 생성하고 그에 따른 로직을 객체 외부에서 수행한다면, 객체는 단순히 값을 갖고 있을 뿐 스스로 변경도 못 하고, 로직도 없고 외부에서 상태값을 변경할 수 있는 위험성도 생긴다.

이는 객체가 객체스럽지 못한것이다.

 

또한 getter를 남발하면 가독성이 떨어진다 

아래는 예로 getter가 줄줄이 이어진 모습이다.

object.getChild().getContent().getItem().getTitle();

 

즉 요점은 아래와 같다

 

상태를 가지는 객체를 추가했다면 객체가 제대로 된 역할을 하도록 구현해야 한다.
객체가 로직을 구현하도록 해야한다.
상태 데이터를 꺼내 로직을 처리하도록 구현하지 말고 객체에 메시지를 보내 일을 하도록 리팩토링한다.

 

테스트 코드 중복 줄이기

 

파라미터만 다른 테스트를 진행하여 여러 번 같은 코드를 적어야 할 경우

test.each를 사용해서 비슷한 테스트를 여러 데이터 세트에 대해 수행할 수 있다. 

 

const INPUTS_TO_END = ["1", "해산물파스타-2"];
describe("예외 테스트", () => {
  test.each([[["0", ...INPUTS_TO_END]], [["32", ...INPUTS_TO_END]]])(
    "날짜 1미만, 31초과 입력시 테스트",
    async (input) => {
      // given
      const INVALID_DATE_MESSAGE =
        "[ERROR] 유효하지 않은 날짜입니다. 다시 입력해 주세요.";
      const logSpy = getLogSpy();
      mockQuestions(input);

      // when
      const app = new App();
      await app.run();

      // then
      expect(logSpy).toHaveBeenCalledWith(
        expect.stringContaining(INVALID_DATE_MESSAGE)
      );
    }
  );
});

 


 

이제 본격적으로 이번주 미션과정에서 배운 점 느낀 점을 소개하겠다.

 

toThrow 테스트를 통과 못 할 때 해결

분명 node index를 통해 테스트를 할 때는 [ERROR]를 출력하는데 npm test만 돌리면 동일한 테스트가 통과 안 됐다. 테스트 코드를 잘 짰는지 확인도 했는데 초반부터 테스트를 못 넘어가니 막막했다.

그러다 코드를 따라서 상위 메서드로 확인하다 보니 entry인 start메서드에 async를 안 적은 것을 발견했다. async처리를 안 해줘서 실질적으로 throw Error가 안 됐던 것이었다. 다행히 수정하고 나니 테스트가 통과했다.

이런 쉽게 까먹을 수 있는 부분을 잡기 위해 초반부터 테스트가 필요하구나라는 것을 느꼈다. 

 

메뉴-개수 형식인지 체크하기

찾아보니 한글도 regex로 검사가 가능했다

  REGEX_KOREAN: /[ㄱ-ㅎ|ㅏ-ㅣ|가-힣]+\-[1-9]\d*/,

 

ㄱ-ㅎ : ㄱ부터 ㅎ까지 자음

ㅏ-ㅣ :ㅏ부터 ㅣ까지 모음

가-힣 : 가 부터 힣까지의 글자

\- : 메뉴와 개수 사이 -

[1-9]\d*: 0을 포함하지 않는 자연수

 

Input이 valid 한 지 확인하는 공통 메서드

날짜와 주문을 받는 getValidDate, getValidOrder를 작성하면서 while안의 try catch구문이 중복되었다.

그래서 이 부분을 getValidInput이라는 메서드로 분리시키고 input을 물어보는 함수와, validation을 확인하는 메서드를 파라미터로 전달하는 식으로 만들었다. 메서드 타입별 리턴하는 방식이 다른 것은 type 파리미터를 추가해 메서드의 타입을 구분했다.

 

(추가 저번에는 catch구문 err를 바로 콘솔 했지만 찾아보니 err.message가 더 정확한 표현이라는 것을 배웠다.)

  async getValidInput(inputFunc, validationFunc, type) {
    while (true) {
      try {
        const input = await inputFunc();
        validationFunc(input);
        return type === Constants.INPUT_TYPES.DATE ? Number(input) : input;
      } catch (err) {
        OutputView.display(err.message);
      }
    }
  }
  
async getValidDate() {
    return this.getValidInput(
      InputView.readDate,
      InputValidator.date,
      Constants.INPUT_TYPES.DATE
    );
}

 

날짜를 통해 무슨 요일인지 찾기

Date생성자를 통해 목표날짜를 구하고 getDay메서드를 통해 요일을 찾을 수 있었다.

0은 일요일, 1은 월요일, ..., 6은 토요일을 나타낸다

getDayOfWeek(date) {
    const targetDate = new Date(`2023-12-${date}`); // Sat Dec 11 2023 09:00:00 GMT+0900
    const dayOfWeek = targetDate.getDay(); // 0~6
    return dayOfWeek;
  },

 

key값을 한글로 정의하는게 맞는가?

주어진 메뉴를 객체로 정리할 때 탐색을 편리하기위해 key를 한글 메뉴이름으로 작성했는데 

영어로만 key를 써봐서 궁금증이 생겨 찾아봤다.

const obj = {
    타파스: { cost: 5500, type: "APPETIZER" },
    시저샐러드: { cost: 8000, type: "APPETIZER" },
}

 

 

 

 

 

링크1, 링크2

 

요약하자면 우리가 어떤 걸 써도 JS는 자동으로 key를 문자열 형태로 변형하기에 써도 괜찮은 것 같다. 하지만 느낌적으로 영어로 key를 만드는걸 더 선호할 것 같긴하다ㅎㅎ 실제 이런 상황이 오면 현업에서 어떻게 하나 궁금하다.

 

1000단위로 숫자 콤마로 나누기

number.toLocaleString("ko-KR")

 

 

메서드 단위 테스트

이번에는 아래와 처럼 하나의 메서드를 만들 때마다 단위테스트를 작성해 메서드의 기능을 확인하였다.

describe("구입 금액 입력 테스트", () => {
  test("구입금액에 숫자가 아닌 것이 입력된 경우 예외가 발생한다", () => {
    expect(() => {
      Validator.isNum("123f");
    }).toThrow(ErrorMsg.IS_NAN);
  });
)

test("1000단위 끊기", () => {
    expect(formatNumberWithComma(10000)).toBe("10,000");
  });

 

TDD란

만드려고 하는 기능의 테스트 코드를 먼저 만들고 실제 기능 구현코드를 만드는 방식

 

예제 테스트 결과

 

 

소감

저번주에 기능구현은 완성했지만 테스트 코드를 통과 못해 제출 당시 아차했다. 그래서 이번에는 테스트코드를 먼저 이해한 뒤 메서드를 만들 때마다 테스트를 실행했다. 처음에는 생각나는 로직을 쭉 써 내려가지 못해 답답한 감이 있었지만 중반부터 오히려 주어진 테스트에서 힌트를 얻어 여러 방법 중에 테스트에 적합한 코드를 짤 수 있었다. 

 

가장 고민한 부분은 메뉴를 어떤 형식으로 정의하는 게 주어진 기능들을 구현할 때 가장 편리하게 할 수 있을까였다.

처음에는 {애피타이저 : {양송이수프: 6000,...}, {메인: {티본스테이크: ...} ....}} 이런 식으로 에피타이저, 메인, 디저트, 음료를 key로 두고 각각 value에 해당 메뉴들을 넣는 객체를 생각했지만, 사용자가 입력한 것은 메뉴이름이기에 메뉴에 대한 정보를 이런 객체에서 매번 구하기에는 번거롭다고 생각이 들었다.

그래서 제일 간단하게 {메뉴이름: 가격} 형태의 객체로 만들었다. 이를 통해 중복된 메뉴입력 시 예외처리하는 것은 가능했지만 음료만 주문하는 예외경우, 날짜에 따라 디저트나 메인메뉴에 대한 할인을 계산하기 위한 type을 알 수가 없었다. 그래서 {메뉴이름: {cost: 가격, type: 메뉴타입} 객체로 변형하고 사용자로부터 valid 한 주문을 입력받을 시에 map에 메뉴이름: {개수, 가격, 타입} 이런 형태로 변형했다. 이를 통해 타입별 메뉴 갯수 구하기, 특정타입의 메뉴 유무 판단하기 등과 같이 많은 부분에서 편리하게 사용할 수 있었다.

 

또한 함수분리, 객체가 로직을 구현하는데 중점을 맞췄다.

전에는 클래스 안에 필드를 넣고 외부에서 이를 get한뒤 관련 메서드를 클래스 외부에 작성했지만 이번에는 피드백을 참고해 최대한 필드와 관련된 메서드는 클래스 내부에 작성했다. 외부에서 메서드를 바로 클래스로부터 가져와 쓸 수 있어서 주요 흐름에 관한 코드만 볼 수 있게 외부 controller가 더 단정해졌다. 전에는 코드를 찾을 때 여러 곳을 둘러봐야 했지만 객체대로 메서드를 분리해 놓으니 훨씬 빨리 관련 코드를 찾을 수 있었다.

 

어느새 벌써 4주 차 한 달이 다 됐다는 게 참 시간이 너무 빠른 것 같다. 진짜 이 정도로 농축된 생활을 해본 게 대학교 이후로 오랜만이라 프리코스가 끝나는 게 정말 아쉽다. 혼자 공부할 때는 외롭고 내가 하는 이게 맞나라는 생각이 나를 방해했지만, 프리코스를 하며 매주 안에 끝내야 하는 목표와 오픈채팅방 안 응원의 분위기에 잡생각 없이 온전히 목표에만 집중해 하루하루를 활기차게 보낼 수 있어 너무 좋았다.

 

이제는 요구사항을 보면 어떤 기능들을 만들어야 하고, 고려해야 될 예외사항과 공통적이거나 베이스가 될 수 있는 메서드는 어떤 게 있는지를 정리하고 개발할 수 있게 되었다. 아직도 보충할 부분이 많지만 전엔 놓치던 컨벤션을 신경 쓰고  리펙토링 하면서 프로젝트 전체가 이쁜지 읽기 좋은 코드인지를 고민하는 내 모습을 보니 한 달안에 많은 것을 배울 수 있고 성장할 수 있구나라는 것을 깨달았다. 특히 코드리뷰로 다양한 분들의 코드를 보면서 새로운 방식을 찾고 그를 실험해 볼 때는 모르는 사람의 일기장을 본 것처럼 너무 재밌었다. 아, 물론 나의 일기장도 남들에게 보이니 더 신경 쓰게 작성할 수 있었다

 

 

코드리뷰

이번에는 Model 클래스에서 display하는 메서드를 만들었는데

▶ 다음에는 리뷰어분 의견대로 다음에는 Model에서 View를 제외하고 Controller 중심으로 Model View를 이어주는 방식(Controller 내부에서 model로부터 받은 데이터를 View로 전달하는 식)으로 시도해보기

 

isNaN보단 에어비앤비에서 Number.isNaN을 권장

 

Array.every 메서드는 배열의 모든 요소가 제공된 함수로 구현된 테스트를 통과하는지 boolean을 반환한다

const isBelowThreshold = (currentValue) => currentValue < 40;
const array1 = [1, 30, 39, 29, 10, 13];
console.log(array1.every(isBelowThreshold)); // true

 

'우테코' 카테고리의 다른 글

[6기 프리코스 프론트] 1주차 회고 - 숫자야구  (0) 2023.10.26