티스토리 뷰

안녕하세요. 이번 글에서는 10월 초부터 4주간 진행한 우아한테크코스의 프리코스에 대해 후기를 자세히! 남겨보고자 합니다. 우아한테크코스에 관심이 있고, 교육에 지원할지 고민하시는 분들에게 도움이 되었으면 합니다.

 

2024 우아한테크코스 6기 모집 포스터

목차

소개
 우아한테크코스란?
 프리코스는 무엇인가?

우테코 지원 전에 생긴 궁금증
매주 미션을 진행한 과정 & 배운 점
  1주차: 좀 낯설지만 시작은 가볍게
    README 요구사항
    Class로 구현하기
    현직자의 코드

  2주차: 너 테스트 코드 작성할 수 있니?
    함수 분리하기
    구현한 함수 테스트하기

  3주차: 좀 더 잘 해보도록 해
    Class vs. Object
    이제 던진 예외를 캐치해보자
    테스트를 하는 이유

  4주차: 모든 걸 총 동원하기
    복잡하다, 요구사항 분석하기
    클래스 다이어그램 만들기
    이벤트 재사용하기
    Class vs. Object

 


소개

우아한테크코스란?

우아한테크코스는 우아한형제들에서 진행하는 개발자 교육 프로그램입니다. 줄여서 우테코라고 많이 부릅니다. 내년 2024년에 진행할 교육은 웹 백엔드, 웹 프론트엔드, 안드로이드로 나눠서 진행되기 때문에 서류 지원도 3가지 분야가 열렸습니다. 저는 그중 웹 프론트엔드로 지원했습니다. 자세한 교육과정은 홈페이지와 우아한테크코스 2024 입학설명회에서 확인해보세요! 저는 입학설명회에서 코치들이 어떤 것을 중요하게 여기는지 파악해 두면, 지원서 작성할 때 어떤 경험을 위주로 적을지 도움이 되더라고요. 물론 지금은 합불 여부가 나오지 않아서 지원서를 잘 작성했다고 말씀드릴 수 없습니다 😅

프리코스는 무엇인가?

프리코스는 우아한테크코스의 교육 방식을 미리 경험해 볼 수 있는 단계입니다. 프리코스를 해보면서 우테코의 교육이 자신과 맞는지 판단해 볼 수 있는 시간이라고도 합니다. 2023년 기준 서류를 낸 모든 지원자가 참여할 수 있습니다. (막말로 지원서를 한 줄만 적어도 참여할 수 있지 않을까요..?) 지원서를 제출하면 프리코스 기간에 매주 이메일을 받게 됩니다. 프리코스에 참여하면 4주간 매주 미션을 하나씩 수행해야 하는데요. 이메일로 안내받은 우테코의 github 레포지토리에서 미션을 포크하여 과제를 진행하고, pull request와 홈페이지에 과제를 제출하는 방식으로 진행됐습니다. 

 

아래는 제가 3주 동안 미션을 진행한 레포지토리입니다. main 브랜치에서 프리코스가 미션을 내는 방식을 확인하실 수 있습니다.

우테코 지원 전에 생긴 궁금증

우테코 6기 선발 과정이 10월 초부터 서류 모집을 하고, 현재 11월 중순까지 프리코스를 진행합니다. 학생들은 과제와 시험을 보면서, 직장인이라면 회사를 다니면서 프리코스를 해야 합니다. 취직을 준비하고 있다면, 우테코에 붙지 못하더라도 프리코스를 통해 성장하고 싶을 수 있습니다. 그렇다 보니 시간을 할애한 만큼 

프리코스에서 무엇을 배울 수 있을까?

 

라는 궁금증이 생기더라고요. 입학 설명회에서 프리코스는 최소한의 커트라인을 넘는지만 확인하는 용도라고 하는데, 그게 어느 정도 수준인지는 해보기 전에 모르겠더라고요. 제가 최소한의 커트라인을 지켰는지 모르겠지만 무엇을 신경 써서 미션을 진행했는지, 그리고 미션만 던지며 가르쳐 주는 게 없다던 프리코스가 저에게 알려준 것이 무엇인지 여기에 남기겠습니다.

매주 미션을 진행한 과정 & 배운 점

1주차: 좀 낯설지만 시작은 가볍게

README 요구사항

1주차 미션은 숫자 야구 게임입니다. README에 작성된 요구 사항은 기능 요구 사항, 프로그래밍 요구 사항, 과제 진행 요구사항으로 구성되어 있습니다. 전공생이라면 수업 시간에 교수님이 꼭 예시로 들 것 같은 게임이었는데요. 상대방이 생각하고 있는 서로 다른 3개의 숫자로 구성된 세 자릿수를 맞추는 게임입니다. 게임은 친숙했지만 누군가 저에게 이렇게 상세한 요구 사항을 준 게 처음이었습니다. 살짝 감동적이다 싶다가도 정보가 많아서 정신 바짝 차리고 읽어야 했습니다. 

 

README 일부

Class로 구현하기

처음에 받은 파일은 App.js와 test 파일뿐이었습니다. 이때, App.js가 클래스로 만들어져 있어서 '아, 클래스로 구현해야 하는구나'하고 단순하게 생각했습니다. 하지만 JS로 클래스를 사용한 적이 별로 없어서 공식 문서를 다시 보며 클래스 사용법부터 살펴보고 미션을 진행해야 했습니다. 

 

이때까지만 해도 '무엇을 클래스로 만들까'를 감으로 나눈 것 같습니다. 우선 App.js에 모든 구현을 한 다음, 함수를 분리하고, 그 함수를 또 비슷한 성격으로 묶어서 클래스로 분리했습니다.

 

'숫자 야구 게임' 폴더 구조

 

  • App.js - 다른 클래스를 사용하여 숫자 야구 게임을 실행한다.
  • Computer.js - 서로 다른 숫자 3개를 뽑는다.
  • Player.js - 사용자가 입력한 숫자 3개를 확인하고 저장한다.
  • Score.js - 컴퓨터와 플레이어의 숫자를 비교하고 그 결과를 반환한다.
  • Message.js - 에러 메시지를 출력한다.

현직자의 코드

미션 중에 우테코에서 제공하는 라이브러리에서 1.사용자의 입력을 받는 함수와 2.콘솔에 출력하는 함수를 가져다가 사용하라는 조건이 있었습니다. '이것이 현직자가 적은 코드?!'라는 생각으로 신기해하며, 라이브러리 함수가 어떻게 동작하는지 꼼꼼하게 읽어보곤 했습니다.

또한, 테스트도 처음 실행해 봤는데요. 웹 프론트엔드에서는 주로 Jest라는 프레임워크를 사용해서 단위 테스트를 진행한다고 합니다. 이전부터 "테스트 코드부터 작성하세요", "테스트 코드를 작성해야 ~~ 것들이 좋습니다."라는 말을 주위에서 들어서 '테스트 코드 작성하는 것도 언젠가 공부해 봐야지'라는 생각만 갖고 있었는데, 이번 1주차 미션에서 테스트 코드를 살펴볼 수 있어서 좋았습니다. 하지만 1주차까지는 테스트 코드를 직접 작성하지 않았습니다.

2주차: 너 테스트 코드 작성할 수 있니?

2주차 자동차 경주 미션이 도착했습니다. 이번 목표는 함수를 분리하고 각 함수별로 테스트를 작성하는 것이었습니다.

함수 분리하기

흔히 함수는 하나의 기능을 하도록 작성하라는데요. 저는 '하나'라는 기준부터 정의하고 미션을 시작했습니다. 만들어야 하는 프로그램이 자동차 경주이니, 자동차 경주가 동작하는 단계를 기준으로 하나의 기능을 정했습니다. 예를 들어, 자동차가 전진하기 위해 랜덤으로 숫자를 뽑고, 이 숫자가 4 이상일 때 차가 전진을 하게 되는데, 이 과정을 '랜덤으로 숫자를 뽑는다' → '4 이상인지 확인한다.'보다는 이를 묶어서 '차가 전진할지 판단한다'라는 기능을 만들었습니다. 이렇게 함수를 나누다 보니 요구 사항 파악도 빨라지고, 함수도 직관적이면서 적당히(?) 나눌 수 있었습니다. 이미 존재하는 라이브러리 함수를 다시 한번 함수로 감싸는 건 비효율적이라고 생각했습니다.

// 수정 전: 코드 기준
function getRandomNumber() {
	return Random.pickNumberInRange(0, 9);
}

function checkIsFourOrMore(number) {
	if (number >= 4) return true;
	return false;
}

// 수정 후: 프로그램 동작 기준
function isMovingForward() {
	const randomNum = Random.pickNumberInRange(0, 9);

	if (randomNum >= 4) return true;
	return false;
}

구현한 함수 테스트하기

다음으로 앞서 만든 함수의 테스트 코드를 작성해야 했습니다. 테스트 코드를 처음 작성해 봐서, 미션에 기본으로 있던 StringTest.js와 ApplicationTest.js를 읽어보고 공식 문서를 참고하여, 테스트에 사용되는 함수를 파악했습니다.

// Car.js
class Car {
  constructor(name) {
    this.name = name;
    this.distance = "";
  }

  move() {
    this.distance += "-";
  }
}

export default Car;

위 Car 클래스에 자동차 이름을 넘기면 차가 생성됩니다. 이때 달린 거리(distance)는 빈 문자열로 초기화되고, move() 함수를 실행하면 '-' 문자가 거리에 추가됩니다. 이 함수가 잘 동작하는지 확인하기 위해, 'Car 인스턴스를 생성하여 처음 move() 함수를 실행하면, distance는 '-'이고, 문자의 길이는 1이 돼야 한다'를 테스트해보겠습니다. 단순히 값이 일치하는지 확인하는 테스트 코드의 기본 구조는 다음과 같습니다.

test("테스트 목적/내용을 적습니다.", () => {
  // 테스트를 위해 실행되어야 할 함수를 작성합니다.

  expect(테스트 대상).toEqual(원하는 결과);
});

이 코드를 사용하여 차가 전진하는 테스트 코드를 아래처럼 작성했습니다.

// CatTest.js
import Car from "../src/RacingCar/Car";

test("차가 전진하면 거리가 증가한다.", () => {
  const car = new Car("myCar");
  car.move();

  expect(car.distance).toEqual("-"); // car.distance 값이 '-'와 같은지 확인한다.
  expect(car.distance.length).toEqual(1); // car.distance.length 값이 1과 같은지 확인한다.
});

테스트 목적은 차가 전진하면 실제로 거리가 잘 증가하는지 확인하는 것입니다. 이때 목적은 개발자 외의 사람이 봐도 이해할 수 있도록 작성하면 좋습니다. 테스트를 하려면 Car 인스턴스를 생성하고 move() 함수를 실행해야 하고, 그 결과로 distance는 '-', distance의 길이(length)는 1이 되어야 합니다. expect에 값을 확인할 car.distance와 car.distance.length를 넘기고, toEqual에 각각 '-'와 1을 입력하여 해당 값이 맞는지 확인합니다.

 

이번에는 배열을 반환하는 함수가 올바른 값을 포함하고 있는지 확인해 보겠습니다. 사용자에게 경주할 자동차의 이름을 입력받은 상황입니다. 이름은 쉼표(,)로 구분되고, 쉼표를 기준으로 이름을 분리하여 배열에 저장합니다.

test("좌우 공백 없이 쉼표(,)로 문자열을 구분한다.", () => {
    const input = "쏘나타,그랜저,제네시스";
    const names = input.split(",");

    expect(names).toContain("쏘나타", "그랜저", "제네시스");
});

names 변수는 자동차 이름을 갖는 배열이므로, toContain을 사용하여 배열이 포함해야 하는 element를 적어줍니다. 만약 특정 element를 포함하는 것뿐만 아니라 순서도 동일해야 한다면, 전진 테스트처럼 toEqual을 사용하면 됩니다.

test("좌우 공백 없이 쉼표(,)로 문자열을 구분한다.", () => {
    const input = "쏘나타,그랜저,제네시스";
    const names = input.split(",");

    expect(names).toEqual(["쏘나타", "그랜저", "제네시스"]);
});

3주차: 좀 더 잘 해보도록 해

3주차 미션 로또의 목표는 클래스(객체)를 분리하고, 도메인 로직에 대한 단위 테스트를 작성하는 거였습니다.

Class vs. Object

클래스로 구현된 App.js를 보고 열심히 클래스를 사용하고 있었는데 공통 피드백으로 콕 집어주시더라고요.객체를 생성하는 방법은 클래스만 있는 게 아니라 Javascript 객체 기본 문법을 사용하는 방법도 있으니 다양한 방법을 이해하고 사용해 보라는 피드백이었습니다. 그래서 객체와 클래스의 차이점부터 찾아봤습니다.

 

클래스는 객체를 생성하기 위한 템플릿입니다. 즉, 클래스는 객체를 생성하는 방법 중 하나이고, 객체는 클래스 외에도 JS의 기본 문법을 사용하여 생성할 수 있습니다. 클래스는 객체가 가질 속성과 메소드를 정의하고, 틀로 쿠키를 찍어내듯 동일한 구조를 가진 여러 개의 객체를 생성합니다. 그렇게 생성한 객체는 고유한 값을 가지게 됩니다. 그래서 저는 로또 게임이 한 번 진행되는 동안 동일한 구조로 여러 번 생성해야 하는 객체는 Class로, 그 외 객체는 기본 문법을 사용하여 생성했습니다.

 

로또 모델

 

이번 미션을 크게 추첨과 로또, 구매, 수익 4가지로 나눴는데요. 여기서 사용자가 구매한 만큼 여러 번 생성하는 로또는 클래스로, 나머지 추첨과 구매, 수익은 객체 기본 문법으로 구현했습니다. 로또 클래스에 6개의 숫자로 이루어진 배열을 넘기면, 클래스는 그 숫자 배열을 가진 로또 인스턴스를 생성합니다.

class Lotto {
  #numbers;

  constructor(numbers) {
    this.#validate(numbers);
    this.#numbers = numbers; // 파라미터로 넘긴 숫자로 로또를 생성한다.
  }
  // ...
}

export default Lotto;

그 외 한 번만 사용하고 고유한 값만 갖는 객체는 다음과 같은 구조로 구현했습니다.

const purchase = {
  // 사용자가 구매한 로또의 개수를 구한다.
  countMoney: function (money) {
    this.validate(money);
    return parseInt(money / lottoInfo.PRICE);
  },

  validate: function (money) {
    // ...
  },
  // ...
};

export default purchase;

이제 던진 예외를 캐치해보자

이전까지는 사용자의 입력이 잘못되면 예외를 던지기만 하면 됐습니다. 하지만 3주차부터는 예외를 발생시키고 다시 입력을 받으라는 요구 사항이 있었습니다. '그러면 try-catch를 사용하면 되겠다'라는 생각이 들었지만, 어디에서 예외 처리를 할지 고민되었습니다. 원래는 각 파일마다 예외 처리를 하려고 했습니다.

const purchase = {
  // 사용자가 구매한 로또의 개수를 구한다.
  countMoney: function (money) {
  try{
    	this.validate(money)
  } catch(error) {
    	Console.print(error.message);
        // 다시 입력받기????
  }
  	return parseInt(money / lottoInfo.PRICE);
  },
  // ...
};

export default purchase;

하지만 위의 코드처럼 예외 처리를 하면 사용자의 입력을 다시 받기 어려웠습니다. 각 기능이 독립적으로 구분되어 있었고, 애써 구분해 놓은 객체에 다른 객체의 함수를 사용하려니 코드가 다시 복잡해졌습니다. 그래서 이 모든 객체를 한 곳에 실행할 LottoGame 객체를 생성하고 그곳에서 모든 예외 처리를 했습니다. 예외 처리를 한 곳에서 하니 예외가 발생했을 때 원하는 지점으로 돌아가기 수월했습니다. 아래는 LottoGame.js 파일 일부입니다.

import { Console } from "@woowacourse/mission-utils";
import purchase from "../models/Purchase.js";
import inputView from "../views/Input.js";

const lottoGame = {
  start: async function () {
    const { purchaseAmount, lottoCount } = await this.purchase();
    // ...
  },

  /* 사용자에게 구매 금액을 입력받고
   * 구매 금액과 구매한 로또 개수를 반환한다.
  */
  purchase: async function () {
    try {
      const purchaseAmount = await inputView.enterPurchaseAmount();
      const lottoCount = purchase.countMoney(purchaseAmount);

      return { purchaseAmount, lottoCount };
    } catch (error) {
      Console.print(error.message);
      return await this.purchase(); // 다시 입력받기
    }
  },
  // ...
};

export default lottoGame;

테스트를 하는 이유

2주차 미션을 마치고 코드 리뷰를 하기 위해 다른 지원자의 코드를 읽어보았는데요. 제 테스트 케이스로는 기능에 에러가 없다는 걸 증명하기에는 부족하다는 걸 깨닫고, 3주차 때는 더 꼼꼼히 테스트를 했습니다. 그러면서 왜 테스트 코드를 작성해야 하는지 몇 가지 알게 되었습니다.

 

1️⃣ 혹시 매번 코드를 실행해서 잘 동작하는지 확인하나요?

네, 제 얘깁니다. 이전에는 제가 짠 코드가 잘 동작하는지 F5키를 눌러 프로그램을 실행해서 확인했습니다. 그러다 입력을 잘못하면 중지하고 처음부터 다시 실행했죠. 하지만 테스트 코드를 작성하면 본인이 테스트하고 싶은 케이스를 한 번에 확인할 수 있습니다. 예시로, 사용자에게 구매 금액을 입력받는다고 해봅시다. 구매 금액을 입력할 때는 다음과 같은 상황이 발생할 수 있습니다.

 

  1. 구매 금액을 정상적으로 잘 입력한다.
  2. 값을 입력하지 않는다.
  3. 숫자가 아닌 문자를 입력한다.
  4. 1000원 단위의 금액이 아니다. (로또는 1000원 단위로만 구매할 수 있다는 조건이 있습니다)

1번 상황을 제외하고는 모두 에러가 발생하는 케이스입니다. 만약 테스트 코드를 작성하지 않는다면 적어도 4번 코드를 실행해서 위 상황에서도 코드가 중단되지 않고 잘 돌아가는지 확인해야 합니다. 하지만 테스트 코드를 작성하면 테스트 코드만 한 번 실행해서 모든 상황을 확인할 수 있습니다.

// InputView.js
const inputView = {
  async enterPurchaseAmount() {
    // 구매 금액을 입력받는다.
    const amount = await Console.readLineAsync(message.ENTER_PURCHASE_AMOUNT);
    this.validate(inputType.PURCHASE, amount); // 입력값이 유효한지 검사

    return Number(amount);
  }
  
  validate(type, value) {
    // ...
  }
};
export default inputView;

InputView.js에서 사용자의 구매 금액을 입력받는 함수를 구현했습니다. readLineAync 함수를 사용해서 비동기적으로 작동하고, 입력값이 유효한지 검증(validate)합니다. 입력값이 올바르면 문자를 숫자로 변환하여 반환합니다. 이 함수의 테스트 코드는 다음과 같습니다.

import { MissionUtils } from "@woowacourse/mission-utils";
import inputView from "../src/views/Input";

/*
 * 사용자의 입력을 받는 readLineAsync 함수를 모킹한다.
 * inputs을 앞에서부터 순서대로 반환한다.
*/
const mockQuestions = (inputs) => {
  MissionUtils.Console.readLineAsync = jest.fn();

  MissionUtils.Console.readLineAsync.mockImplementation(() => {
    const input = inputs.shift();

    return Promise.resolve(input);
  });
};

describe("구매금액 입력 테스트", () => 
  // 테스트하기 전에 한 번만 실행한다.
  beforeAll(() => {
    const inputs = ["1000", "", "abc", "1500"]; // 입력 케이스
    mockQuestions(inputs);
  });
 
  // 1번. 정상 입력
  test("구매금액을 입력하면 숫자로 반환한다.", async () => {
    const amount = await inputView.enterPurchaseAmount();
    expect(amount).toEqual(1000);
  });

  // 2번. 입력하지 않음
  test("구매금액을 입력하지 않으면 예외가 발생한다.", async () => {
    await expect(inputView.enterPurchaseAmount()).rejects.toThrow("[ERROR]");
  });

  // 3번. 숫자가 아님
  test("구매금액이 숫자인 문자가 아니면 예외가 발생한다", async () => {
    await expect(inputView.enterPurchaseAmount()).rejects.toThrow("[ERROR]");
  });
  
  // 4번. 1000원 단위가 아님
  test("구매금액이 1000원 단위가 아니면 예외가 발생한다", async () => {
    await expect(inputView.enterPurchaseAmount()).rejects.toThrow("[ERROR]");
  });
});

위와 같이 테스트 코드를 작성하고 테스트를 실행하면 확인하고 싶은 모든 케이스가 잘 동작하는지 확인할 수 있습니다. 1번 케이스는 2주차에서 작서한 것처럼 toEqual를 사용하여 값이 예상과 일치하는지 확인합니다. 나머지 2~4번 케이스는 정상적인 상황이 아니므로 예외를 던져야 합니다. 이때 입력받는 함수가 비동기 함수이고, 이 상황에서 예외를 던지는지 확인하려면 expect에 비동기 함수를 넘기고 rejects.toThrow() 함수를 사용하면 됩니다. 만약에 비동기 함수가 아니라면 그냥 toThrow() 함수만 사용하면 됩니다.

// 비동기 함수 테스트
await expect(readLineAsync()).rejects.toThrow();

// 일반 함수
expect(readLine).toThrow();
// OR
expect(() => {
  readLine()
}).toThrow();

 

2️⃣ 테스트 코드를 작성하기 불편하다면 함수 리팩토링!

테스트 코드를 작성하다가 '함수를 테스트하기 힘들다'라고 느낀다면, 함수가 두 가지 이상의 기능을 수행하거나, 서로 연관이 없는 기능이 한 객체에 만들어진 걸 수도 있습니다.

 

  • 서로 연관이 없는 기능이 한 객체에 있는 경우

아래는 로또 번호와 관련된 테스트 코드입니다.

test("로또번호를 입력하지 않으면 예외가 발생한다.", async () => {
    await expect(inputView.enterWinningNumber()).rejects.toThrow("[ERROR]");
  });

test("로또번호가 숫자인 문자가 아니면 예외가 발생한다.", async () => {
	await expect(inputView.enterWinningNumber()).rejects.toThrow("[ERROR]");
});

test("로또 번호가 범위를 넘어가면 예외가 발생한다.", () => {
    await expect(inputView.enterWinningNumber()).rejects.toThrow("[ERROR]");
});

원래는 InputView에서 로또 번호를 입력받을 때, 입력값이 로또 번호가 지켜야 하는 모든 조건을 만족하는지 확인하려고 했습니다. 그러다 보니 enterWinningNumber 함수에서 처리해야 하는 예외가 많아졌습니다. 입력값을 숫자로 바꾸기 전에는 값이 입력되었는지, 쉼표로 번호를 구분했는지 확인하고, 입력값을 숫자로 바꾼 후에는 중복된 숫자가 있는지, 숫자가 범위를 만족하는지 등 테스트를 작성할 때도 문자일 때와 숫자일 때의 예외 처리가 한 곳에 작성되었습니다. 

 

함수와 테스트 코드가 복잡해지는 것을 막고자, 문자일 때의 예외 상황은 InputView에, 숫자일 때의 예외 상황은 Lotto 클래스에 작성했습니다. 테스트 코드를 작성할 때도 입력 테스트는 StringTest.js, 로또 번호 테스트는 LottoTest.js로 분리했습니다.

// StringTest.js
describe("로또번호 입력 테스트", () => {
  // ...

  test("로또번호를 입력하지 않으면 예외가 발생한다.", async () => {
    await expect(inputView.enterWinningNumber()).rejects.toThrow("[ERROR]");
  });

  test("로또번호가 숫자인 문자가 아니면 예외가 발생한다.", async () => {
    await expect(inputView.enterWinningNumber()).rejects.toThrow("[ERROR]");
  });
});

// LottoTest.js
// ...
describe("로또 클래스 테스트", () => {
  test("로또 번호가 범위를 넘어가면 예외가 발생한다.", () => {
    expect(() => {
      new Lotto([1, 2, 3, 4, 5, 66]);
    }).toThrow("[ERROR]");
  });

  // ...
});

 

  • 한 함수가 두 가지 이상의 기능을 수행하는 경우

아래 코드는 구매한 로또 개수만큼 로또 배열을 반환하는 함수입니다.

import { Random } from "@woowacourse/mission-utils";
import { lottoInfo } from "../constants.js";
import Lotto from "./Lotto.js";

const purchase = {
  // ...

  getLottos(count) {
    let lottos = [];

    for (let i = 0; i < count; i++) {
      // 1부터 45사이의 6개의 서로 다른 숫자를 뽑는다.
      const numbers = Random.pickUniqueNumbersInRange(
      lottoInfo.START_INCLUSIVE,
      lottoInfo.END_INCLUSIVE,
      lottoInfo.COUNT
    );
      lottos.push(new Lotto(numbers));
    }

    return lottos;
  },
};

export default purchase;

만약 getLottos 함수를 테스트한다면, 다음 두 가지를 살펴봐야 합니다.

 

  1. 각 로또 번호가 6개의 서로 다른 숫자로 이루어져 있는가?
  2. 구매한 개수만큼 로또 인스턴스가 생성되었는가?

2번 케이스는 반환된 배열의 길이가 count와 동일한지 확인하면 되므로 복잡한 테스트가 아닙니다. 하지만 1번 케이스는 배열의 요소를 꺼내서 각 로또가 올바른 번호를 가졌는지 모두 확인해야 합니다. 즉, 함수가 임의의 로또 번호를 뽑고, 그 번호로 로또를 생성하는 두 가지 기능을 수행하고 있습니다. 그래서 이 함수를 다음과 같이 분리했습니다.

import { Random } from "@woowacourse/mission-utils";
import { lottoInfo } from "../constants.js";
import Lotto from "./Lotto.js";

const purchase = {
  // ...

  getLottos: function (count) {
    let lottos = [];

    for (let i = 0; i < count; i++) {
      const numbers = this.pickLottoNumbers();
      lottos.push(new Lotto(numbers));
    }

    return lottos;
  },

  /* 1과 45사이의 6개의 랜덤 번호를 반환한다 */
  pickLottoNumbers: function () {
    const numbers = Random.pickUniqueNumbersInRange(
      lottoInfo.START_INCLUSIVE,
      lottoInfo.END_INCLUSIVE,
      lottoInfo.COUNT
    );

    return numbers;
  },
};

export default purchase;

이렇게 하면 구매 객체를 테스트할 때, 숫자 6개가 잘 생성되는지는 pickLottoNumbers 함수로 확인하면 되고, 이 테스트가 잘 동작한다면, getLottos 함수가 구매 개수만큼 로또 인스턴스를 생성하는지 확인하면 됩니다.

4주차: 모든 걸 총 동원하기

와, 드디어 마지막 미션입니다. 이것만 하면 밀린 과제를 할 수 있겠다며 레포지토리를 확인했는데, 미션이 평소와 좀 결이 다른 것 같았습니다.

 

README 일부

복잡하다, 요구 사항 분석하기

이메일 형식의 리드미를 읽을 때는 신기했지만, 아래 길게 나열되어 있는 12월 이벤트를 보며 든 첫 번째 생각은 복잡하다는 거였습니다. 평소처럼 줄줄 읽으면서 기능 목록을 쉽게 적을 수 없을 거 같아서, 기능 목록을 적을 때 무엇을 객체로 만들지 생각하며 작성했습니다. 

먼저 12월 이벤트가 진행되는 과정을 크게 생각해 봤습니다. 고객이 주문을 하면, 주문에 따라 이벤트가 적용되고, 고객은 할인된 금액만큼 결제를 합니다. 그리고 프로그램이 실행되려면 입력출력이 있어야 합니다.

 

  • 주문
  • 이벤트
  • 결제
  • 입력
  • 출력

그다음 가장 기능이 많은 이벤트를 더 세세하게 나눴습니다. 이벤트는 디데이 할인, 주말 할인, 평일 할인, 특별 할인, 증정 이벤트로 나뉩니다. 이 모든 이벤트를 적용한 후, 할인 금액에 따라 배지를 부여합니다. 또한, 사용자가 주문을 하려면 먼저 메뉴가 제공되어야 합니다. 그래서 최종적으로 추출한 객체는 다음과 같습니다.

 

  • 주문
  • 메뉴
  • 이벤트: 디데이 할인, 주말 할인, 평일 할인, 특별 할인, 증정 이벤트
  • 배지
  • 결제
  • 입력
  • 출력

정리한 기능 목록은 여기에서 확인하실 수 있습니다.

클래스 다이어그램 만들기

추출한 객체의 기능을 구현하기 위해 함수와 필드를 정리하고자 클래스 다이어그램을 작성했습니다. 다이어그램을 만들면서 필요한 객체를 더 추가했습니다.

 

12월 프로모션 클래스 다이어그램

EventController: 각 이벤트의 할인 금액을 가져오고, 총 할인 금액을 계산한다.

Duration: 각 이벤트가 적용되는 기간을 계산한다.

이벤트 재사용하기

원래는 각 이벤트 기간을 계산할 때 올해 2023년 12월 달력에만 초점을 두었는데, 2023년 이후에도 크리스마스 이벤트를 진행할 수 있도록 Duration 객체를 만들었습니다.

import { DAY, day } from "../constants/date.js";

class Duration {
  #year = new Date().getFullYear();
  #month = 12;
  #date;
  #day;
  #weekday = [DAY.SUNDAY, DAY.MONDAY, DAY.TUESDAY, DAY.WEDNESDAY, DAY.THURSDAY];
  #weekend = [DAY.FRIDAY, DAY.SATURDAY];

  constructor(date) {
    this.#date = date;
    this.#day = this.#getDay(date);
  }

  #getDay(date) {
    const index = new Date(`${this.#year}-${this.#month}-${date}`).getDay();
    return day[index];
  }
}

필드에서 month가 12월로 설정되어 있고, year는 올해 연도를 가져오도록 했습니다. weekday와 weekend에는 요구사항을 기준으로 평일, 주말인 요일이 저장되어 있습니다. 객체 인스턴스를 생성하면, 파라미터로 넘어온 날짜의 요일을 구하게 됩니다.

Duration 객체는 각 이벤트에서 이벤트 기간을 판별할 때 사용했습니다. 이벤트에 따라 주말인지, 평일인지, 크리스마스 전인지, 일요일인지 판단해야 해서, Duration에 다음 함수를 추가했습니다.

import { DAY, day } from "../constants/date.js";

class Duration {
  // ...
  // 크리스마스를 지나지 않았는지 확인한다.
  isUntilChristmas() {
    return this.#date <= 25;
  }

  isWeekday() {
    return this.#weekday.includes(this.#day);
  }

  isWeekend() {
    return this.#weekend.includes(this.#day);
  }

  isSunday() {
    return this.#day === DAY.SUNDAY;
  }

  isChristmas() {
    return this.#date === 25;
  }
}

평일 이벤트 객체는 다음처럼 Duration을 사용하여 날짜를 판별합니다.

import { MENU_TYPE } from "../constants/menu.js";
import Duration from "./Duration.js";

const WeekdayDiscount = Object.freeze({
  amount: 2023,
  menuType: MENU_TYPE.DESSERT,

  giveIf(date, menuCountPerType) {
    // 평일에 이벤트를 적용한다.
    if (new Duration(date).isWeekday()) {
      return menuCountPerType[this.menuType] * this.amount;
    }
    return 0;
  },
});

Class vs. Object

저번 미션에서 class로 객체를 생성할지 기본 문법으로 객체를 생성할지 결정할 때, 동일한 구조의 객체가 여러 번 사용되면 Class, 아니면 기본 문법을 사용했습니다. 이번에는 다음 조건도 추가하여 객체를 생성했습니다.

 

  • 객체 내부의 여러 함수가 외부에서 받는 변수를 공통으로 사용하면 Class로 구현했습니다. 필드에 변수를 생성하여 매번 함수에 파라미터를 전달해야 하는 수고를 줄이고자 했습니다.
  • 고유한 값만 사용하는 객체는 기본 문법으로 생성했고, 이 객체는 클래스의 private 필드처럼 정보를 은닉할 수 없는 대신 Object.freeze() 함수를 적용하여 외부에서 객체를 수정할 수 없도록 했습니다.
  • 클래스는 외부에서 사용해야 하는 함수를 제외하고 private 하게 만들어서, 클래스 코드를 변경했을 때의 영향을 최소화하고자 했습니다.

위의 모든 내용이 프리코스를 하는 4주 동안 배운 것입니다. 직접적으로 알려주는 게 없다지만, 리드미에 적힌 요구사항과 공통 피드백, 기본으로 전달받는 코드만 봐도 배울 게 많았던 시간이었습니다. 이제 곧 학교도 졸업하니 해보고 싶었던 활동에 열심히 참여하며 개발 실력을 기르고자 합니다. 끝까지 읽어주셔서 감사합니다 🫡

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/09   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30
글 보관함