Javascript

우아한테크코스 - 6기 프리코스 후기

oyatplum 2023. 11. 30. 09:10

Week 1 - 숫자 야구

 

Week 1 과제는 숫자 야구 게임이다.

순수 Vanila JS를 사용해 조건대로 로직을 구현한 뒤 테스트 케이스까지 통과하면 성공!

 

세오스 첫 주차 과제로 순수 Vanlia JS를 사용해 본 적은 있지만 html 파일을 사용한다던가 react, next를 사용해 웹 페이지를 구현했지 이렇게 코테처럼 정말 코드만 짜서 돌리는 건 처음 해봤다..

 

물론 학교 다닐 땐.. 파이썬, C, C++, 어셈블리 등.. 아주 잠깐 비슷하게 로직만 짜본 것 같긴 한데 기억도 안 난다^^;;

 

아무튼 단순히 웹만 구현할 때보다 자바스크립트에 대해 더 자세하게 공부할 수 있었다.

첫 주차라 그런지 과제는 어렵지 않았는데 node로 프로그램을 돌리는 방법이나 테스트 케이스를 실행시키는 등의 과정이 더 어려웠다..

 

 

 

 

항상 어떤 과제든 시작만 하면 체계 없이 중구난방으로 휘뚜루 마뚜루 해결하기 급급했던 나였는지라.. 여기서 하라는대로 진행 방식에 맞춰 딱딱 진행했더니 굉장히 만족도가 높았다!!!

냅다 코드부터 달리지 않고 어떻게 구현하는 게 좋을지 충분히 생각을 해본 뒤, README.md에 기능 목록을 정리하고 그 순서에 맞춰 코드를 작성하는 것이 좋았다. 테스트 케이스를 돌려보기 전에는 '아 완벽하다!' 라고 생각했지만 막상 테스트 케이스를 돌려보면 내가 생각하지도 못 한 부분에서 에러를 쏟아내는 게.. 슬펐지만 기뻤다. pr 날린 뒤 우테코 사이트에서 따로 나의 소감도 작성할 수 있어서 처음부터 끝까지 원하지 않아도 체계적으로 할 수 밖에 없구나... 나의 부족한 부분을 정말 많이 볼 수 있었다.

 


 

https://github.com/oyatplum/javascript-baseball-6/tree/oyatplum

 

1주차 끝

 

 


Week 2 - 자동차 경주

 

한 번 과제를 해봐서 그런지 확실히 익숙해졌다.

고 생각하고 큰 코 다쳤다.

 

처음엔 로직이 그리 어렵지 않아 보였는데 나의 시야가 굉장히 좁구나..를 느꼈다.

 

시간이 오래 걸린 부분은 딱 두 곳이다.

 


1.

constructor() {
    this.results = {}; // 각 자동차의 결과를 저장하는 객체
  }
racingCar(carArr, trialNum) {
    for (let i = 0; i < trialNum; i++) {
      carArr.forEach((car) => {
        this.calcResult(car);
      });
      Console.print("");
      this.printResults();
    }
  }

printResults() {
    for (const car in this.results) {
      Console.print(`${car} : ${this.results[car]}`);
    }
  }

calcResult(car) {
    const condition = this.racingCondition();

    let carResult = this.results[car] || ""; // 자동차의 이전 결과 가져오기
    if (condition === "전진") {
      carResult += "-";
    }
    this.results[car] = carResult;
  }

 

결과를 저장하기 위해 객체를 선언하고 그 안에 this.results[car]로 carResult를 넣어줬다.

이건 단순히 결과를 print 해야하는 부분에만 포커스를 뒀기 때문인데 이렇게 로직을 구현하고 자동차마다 결과값을 비교한 뒤 우승자를 출력하려고 봤더니 개노답이 되어버렸다는 것.

 

그래서

constructor() {
    this.results = []; // 각 자동차의 결과를 저장하는 객체
  }
addObj(carArr) {
    carArr.forEach((car) => {
      this.results.push({ car, state: [] });
    });
  }

 

결과를 저장하는 객체를 배열로 만들었고 그 내부에 car마다 state 배열을 가지고 있는 하나의 객체로 들어가게 만들었다!

이렇게 한 뒤 다시 지지고 볶아서 최종 우승자를 출력할 수 있었다^^..

 


2.

또 시간이 꽤 걸린 부분은 테스트 케이스를 돌리면서 계~~~~~속 요상하게 에러가 나는 것이었다.

test.each([
    [["pobi,javaji"]],
    [["pobi,eastjun"]]
  ])("이름에 대한 예외 처리", async (inputs) => {
    // given
    mockQuestions(inputs);

    // when
    const app = new App();

    // then
    await expect(app.play()).rejects.toThrow("[ERROR]");
  });
});

이 부분에서 계속 에러가 나는데ㅠㅠ 왜 자꾸 에러가 뜨는 건지 몰라서 코드를 이렇게도 바꿔보고 저렇게도 바꿔봤지만 해결되지 않았다..

 

문제는 바로

async startRacing() {
    Console.print(RACING.START);
    let inputCars = await Console.readLineAsync("");

    if (!inputCars) {
      throw new Error(ERROR.NOT_NAME);
    } else if (inputCars.includes(",")) {
      const carsArr = inputCars.split(",");
      await this.checkInputValidity(carsArr);
    } else {
      throw new Error(ERROR.ONE_NAME);
    }
  }

  async checkInputValidity(carsArr) {
    const invalidNames = carsArr.filter((car) => car.length > 5);

    if (invalidNames.length > 0) {
      throw new Error(ERROR.INVALID_NAME);
    } else {
      this.inputTrialNum(carsArr);
    }
  }

여기 startRacing() 함수에서 checkInputValidity()를 호출할 때 await를 하지 않아서 발생한 것이었다....

허무..

 

해결한 뒤 후련했지만 열받았고 테스트 케이스 코드가 어떻게 짜여있는지도 잘 봐야겠다는 생각을 했다.

 


Week 1 과 달리 추가된 두 가지 요구 사항은

indent depth가 2까지로 제한된 것이고 Jest를 이용하여 테스트 코드를 구현하는 것이었는데

사실 두 번째 요구 사항은 과제 제출하고 알아서ㅋㅋ... 못 했당ㅠ

 

indent depth가 2로 제한되어서 함수가 분리될 수 밖에 없었는데 오히려 기능적인 부분에서 효율적이고 가독성이 높아졌다.

 

또한 이전까지는 커밋을 할 때 간단히 type, subject만 작성했는데

찾아보니 커밋은 꽤나 자세하게 작성할 수 있었던 것..

 

https://velog.io/@shin6403/Git-git-커밋-컨벤션-설정하기

https://devlog-wjdrbs96.tistory.com/227

https://ekko.tistory.com/50

 

이렇게 참고해서 비록 간단한 과제이지만 issue를 세부적으로 생성한 뒤 body, footer도 작성하면서 커밋 컨벤션을 더 정확히 지켜보도록 노력했다. 이런 과정이 추후 협업에 있어 굉장한 도움이 될 것이라고 생각했다.

 

 

확실히 나중에 커밋을 확인했을 때 이해가 쉬울 것 같았고 footer에 issue 번호를 달아주니까 어떤 기능을 구현하고 있었는지 더 명확하게 확인할 수 있어서 굉장히 굉장히 굉장히 좋았다.

 

 

 

그래서 기존에 진행 중이던 프로젝트에 바로 반영했다.

아주 뿌듯하군.

 


 

https://github.com/oyatplum/javascript-racingcar-6/tree/oyatplum

 

2주차 끝

 


Week 3 - 로또

 

여담이자 핑계라면 방콕 여행 준비를 하느라 코드에 애정을 주지 못 했다.

하지만 어찌저찌 모두 구현해내긴 했는데 다음 주가 정말 문제다ㅠㅠ 하루만에 끝낼 수 있을지..

 

 


 

 

아무튼 이번 미션은 문제 자체가 그리 어렵진 않았고 지난 미션과 비슷했다. 하지만 주어진 Lotto 클래스를 이용해야 했는데 생각해보니 지금까지 과제를 제출했던 코드들은 전혀 객체 지향적이지 않았다. 그저 기능에 따라 함수를 분리하기만 했다.

 

 

그래서

 

우선 주어진 Lotto의 클래스는 입력받은 당첨 번호의 유효성을 검사했다.

InputValidator객체 리터럴에는 Input의 유효성을 검사하는 메소드를 작성했고 Input에서는 클래스로 이를 활용해 ERROR를 출력했다.

Compare 클래스에서는 생성된 랜덤 번호와 당첨 번호, 보너스 번호를 비교했고 Output 클래스에서는 랜덤 번호 생성, 결과 출력 등의 기능을 담당했다.

 

사실 아무리 생각해도 이보다 효율적으로 100만번 더 잘 구성할 수 있을텐데 허둥지둥 손이 가는대로 만들고.. 만들다보니.. 복잡하게 구현한 것 같다.

 

나름 핵심 로직과 UI 로직을 분리한다고 분리했지만 다음 미션에서는 시작할 때부터 구조를 잘 잡고 들어가야겠다고.. 생각했다.

 

 

 

지금이야 단순한 미션이기 때문에 큰 효과가 없어 보일 수 있지만 개발의 규모가 커진다면 이런 사소한 부분이 큰 영향을 미칠 것이고 확실히 객체 지향적 프로그래밍을 하면 코드가 훨씬 논리적이고 구조화되어 가독성도 높고 유지 보수가 용이하겠다는 생각이 들었다. 마치 리액트에서 컴포넌트를 분리하느라 애쓰는 것처럼....

 

 


 

 

 

Jest를 잘 몰라서 항상 테스트 돌릴 때마다 긴장한다. 고치는데 정말 오래 걸렸던 에러가 얘다.

이리 고치고 저리 고쳐도 계속 요상하게 에러가 났고..

그 이유는.. 지난 과제까지

MissionUtils.Random.pickNumbersInRange(1, 45);

 

이렇게 랜덤 번호를 생성했어서 문제를 자세히 들여다 보지 않고 그냥 바로 이 함수를 사용했다.

 

하지만 이번 과제에서는

MissionUtils.Random.pickUniqueNumbersInRange(1, 45, 6);

 

얘를 사용했어야 했던 것^^.... 하하 문제를 잘 읽자.

 

그래서 이렇게 고친 뒤 코드 순서를 조금 변경했더니 모든 테스트를 통과할 수 있었고

지난 과제에서는 하지 못 했던 Jest 테스트 코드도 짧게나마 구현했다.

 

import Input from "../src/Input";

describe("InputValidator Test", () => {
  test("구매 금액 1,000 단위로 나누어 떨어지지 않는 값 예외 처리", () => {
    const input = 1234;
    const result = new Input(input);
    expect(() => result.inputMoney()).toThrow("[ERROR]");
  });

  test("정수 이외의 값 예외 처리", () => {
    const input = "abc";
    const result = new Input(input);
    expect(() => result.inputMoney()).toThrow("[ERROR]");
  });

  test(",로 숫자 구분하지 않는 경우 예외 처리", () => {
    const input = "1 2 3 4 5 6";
    const result = new Input(input);
    expect(() => result.inputWin()).toThrow("[ERROR]");
  });

  test("숫자가 범위를 벗어나는 경우 예외 처리", () => {
    const input = 47;
    const result = new Input(input);
    expect(() => result.inputBonus()).toThrow("[ERROR]");
  });

  test("입력값이 없는 경우 예외 처리", () => {
    const input = "";
    const result = new Input(input);
    expect(() => result.inputMoney()).toThrow("[ERROR]");
    expect(() => result.inputWin()).toThrow("[ERROR]");
    expect(() => result.inputBonus()).toThrow("[ERROR]");
  });
});

 

 

다른 사람들 코드를 슉 훑어 보니 더 자세하고 멋깔나게 쓰던데.. 나도 좀 더 알아보고 다음 미션에서는 더욱 자세한 테스트 코드를 작성해보길..

 


 

 

https://github.com/oyatplum/javascript-lotto-6/tree/oyatplum

 

3주차 끝


Week 4

 

대망의 마지막 과제!

제출하지 못 했다! (당당)

 

...여행 다녀오느라.. 결국 하루만에 완성하지 못 하고 그냥 혼자 여유있게 풀어봤다^^..

 

 


 

지금까지의 과제와 달리 문제가 굉장히.. 굉장히 길었다.

그래서 처음에 솔직히 조금 당황했는데 막상 천천히 읽어보니까 말만 장황하고 크게 복잡한 내용은 없는 것 같았다.

 

 

우선 menyList.js에 메뉴판을 객체 배열로 저장했다.

입력과 출력을 담당하는 객체는 InputView.js와 OutputView.js로 구분하였고(주어진 조건)

Event.js는 여러 이벤트의 조건에 따라 결과값을 반환하는 객체이다.

CalcPrice.js는 이벤트의 조건에 따른 세부적인 결과값을 계산하는 클래스이다.

 

 


 

기억에 남았던 코드들만 간략하게 적어보자면

 

import { menuList } from "./utils/menuList.js";

class CalcPrice {
...

	getWeekdayDiscount() {
    this.#weekdayPrice = this.#inputMenu.reduce((total, menu) => {
      const item = menuList.find(
        (i) => i.name === menu.name && i.menu == "dessert"
      );
      return item ? total + 2023 * menu.quantity : total;
    }, 0);
    return this.#weekdayPrice;
  }
  
}

 

주중 할인을 계산하는 코드였다.

주중 할인 조건을 만족할 경우 디저트 메뉴를 개당 2,023원씩 할인해야 했다.

 

그래서 사용자로부터 입력받은 메뉴들인 inputMenu 객체 배열에 reduce함수를 사용해서 결과값을 누적했고 메뉴판인 menuList 객체 배열에는 find함수를 통해 디저트 메뉴인지를 판단했다.

 

보기엔 간단하지만.. 처음에는 굉장히 더럽게 코드를 짜서.. 많이 깔끔해진 것이다.

 

 


 

 

그리고 사용자로부터 입력받는 메뉴의 유효성을 판단하는 코드에서

const names = inputMenuList.map((menu) => menu.name);
const uniqueNames = new Set(names);
if (names.length !== uniqueNames.size) return false;

 

중복되는 메뉴를 입력했는지 판단해야 했다.

이때는 Set객체를 사용했다. Set은 중복되는 값이 있으면 이를 한 번만 저장해서 반환해준다.

 

그래서 간단하게 중복 여부를 확인할 수 있었다..

 


 

그리고 뭐 대단한 건 아니지만..

 

describe("InputValidator Menu Test", () => {
  test("메뉴 입력하지 않은 경우", () => {
    const input = [{ name: "", quantity: "" }];
    expect(InputValidator.validMenu(input)).toBe(false);
  });
  test("메뉴판에 없는 경우", () => {
    const input = [{ name: "무엇일까요", quantity: "1" }];
    expect(InputValidator.validMenu(input)).toBe(false);
  });
  
  ...}

 

InputValidator에서 함수의 반환 값을 boolean으로 주기 때문에

저번 과제에서는 expect().throw()를 사용했지만 이번엔 toBe()를 사용했다... 뭐 이정도?

 

 

사실 조건이 많아서 문제였지 어렵진 않았다.

 

 


 

https://github.com/oyatplum/javascript-christmas-6/tree/oyatplum

 

최종 끝.