우아한 테크코스 7기

우테코 프리코스 1주차를 돌아보며

1주차 완료와 함께 무슨 일이 벌어졌는데...

2024-10-22 작성

평균 13분 소요

132회 조회

#회고록#우아한 테크코스
cover

우테코 7기 프리코스 1주차가 시작되었습니다. 디스코드 채널을 통해 많은 지원자 분들과 소통도 하고 스터디나 모각코 등을 하면서 짧은 기간이지만 이번 기회에 많은 것을 얻어갈 수 있는 시간이 주어졌어요.

1주차 문제는 10월 15일 오후 3시에 공개되었고 문제에 대한 내용은 다음과 같습니다.

1주차 문제

입력한 문자열에서 숫자를 추출하여 더하는 계산기를 구현한다.

  • 쉼표(,) 또는 콜론(:)을 구분자로 가지는 문자열을 전달하는 경우 구분자를 기준으로 분리한 각 숫자의 합을 반환한다.
    • 예: "" => 0, "1,2" => 3, "1,2,3" => 6, "1,2:3" => 6
  • 앞의 기본 구분자(쉼표, 콜론) 외에 커스텀 구분자를 지정할 수 있다. 커스텀 구분자는 문자열 앞부분의 "//"와 "\n" 사이에 위치하는 문자를 커스텀 구분자로 사용한다.
    • 예를 들어 "//;\n1;2;3"과 같이 값을 입력할 경우 커스텀 구분자는 세미콜론(;)이며, 결과 값은 6이 반환되어야 한다.
  • 사용자가 잘못된 값을 입력할 경우 "[ERROR]"로 시작하는 메시지와 함께 Error를 발생시킨 후 애플리케이션은 종료되어야 한다.

입력

  • 구분자와 양수로 구성된 문자열

출력

  • 덧셈 결과
결과 : 6

실행 결과 예시

덧셈할 문자열을 입력해 주세요.
1,2:3
결과 : 6

문제 풀이

먼저 필요한 각 기능을 정리했습니다.

  • 콘솔 모듈
    • read : 사용자 입력 읽어오기
    • print : 콘솔 출력
  • 문자열 입력
  • 문자열에서 커스텀 구분자 추출
  • 구분자를 하나로 치환
  • 구분자를 제외한 나머지 문자열에 대한 검증
  • 합산 결과 출력

그리고 각 기능에 대한 코드를 작성했어요.

전체 코드는 본문 길이가 지나치게 길어기는 관계로 링크로 대체합니다.

Calculator.js
Console.js
App.js

그럼 각각의 코드에 대해서 간단히 정리를 해보도록 하겠습니다.


Calculator

커스텀 구분자를 추출하고 검증하며, 합산하는 계산 클래스입니다.

  1. CUSTOM_EXPRESSION : 커스텀 구분자를 필터링하기 위한 상수. 전역 객체에 등록되는 것을 막기 위해 Object.freeze() 사용
CUSTOM_EXPRESSION
const CUSTOM_EXPRESSION = Object.freeze({
  START: "//",
  END: "\\n",
});
  1. constructor : 부모 클래스에서 넘겨받는 상태값으로, 기본 구분자 목록을 주입받도록 설정
constructor
constructor(defaultSeperators) {
    this.seperators = defaultSeperators;
    this.constantSeperator = ",";
  }
  1. extractCustomSeperator : 커스텀 구분자를 분리하는 메서드. 커스텀 구분자를 추출하고 setCustomSeperator 메서드로 메서드 목록에 추가 .단, 길이가 0인 구분자는 에러 반환
extractCustomSeperator
extractCustomSeperator(input) {
    if (
      input.startsWith(CUSTOM_EXPRESSION.START) &&
      input.includes(CUSTOM_EXPRESSION.END)
    ) {
      const startIndex = CUSTOM_EXPRESSION.START.length;
      const endIndex = input.indexOf(CUSTOM_EXPRESSION.END);
      const customSeperator = input.slice(startIndex, endIndex);
 
      if (customSeperator.length === 0) {
        throw new Error("[ERROR] 커스텀 구분자의 길이는 0일 수 없습니다.");
      }
 
      this.setCustomSeperator([
        CUSTOM_EXPRESSION.START + customSeperator + CUSTOM_EXPRESSION.END,
        customSeperator,
      ]);
    }
  }
  1. setCustomSeperator : 구분자 목록에 커스텀 구분자를 추가하는 메서드
setCustomSeperator
  setCustomSeperator(customSeperators) {
    this.seperators = [...customSeperators, ...this.seperators];
  }
  1. replaceAllSeperators : 문자열을 순회돌며 구분자 목록에 있는 구분자를 기본 구분자로 치환하는 메서드
replaceAllSeperators
  replaceAllSeperators(input) {
    let processedInput = input;
 
    this.seperators.forEach((seperator) => {
      processedInput = processedInput.replaceAll(
        seperator,
        this.constantSeperator
      );
    });
 
    return processedInput;
  }
  1. validateInput : 기본 구분자로 치환된 문자열의 합산이 가능한 상태인지 검증하는 메서드. BigInt 타입을 사용해 253-1 이상의 수를 입력해도 문제가 되지 않도록 처리
validateInput
 validateInput(input) {
    const inputArray = input
      .split(this.constantSeperator)
      .filter(Boolean)
      .map((num) => {
        try {
          return BigInt(num);
        } catch (error) {
          throw new Error("[ERROR] 정의되지 않은 구분자가 포함되어 있습니다.");
        }
      });
 
    if (inputArray.some((num) => num <= 0n)) {
      throw new Error("[ERROR] 숫자는 자연수만 입력할 수 있습니다.");
    }
 
    return inputArray;
  }
  1. sumAll : 배열로 반환된 문자열의 총 합산을 구하는 메서드
sumAll
 sumAll(inputArray) {
    return inputArray.reduce((sum, num) => sum + num, 0n);
  }

Console

우아한 테크코스에서 제공한 미션용 유틸 함수를 관리하는 클래스입니다.

  1. read : 사용자의 입력값을 유도하기 위한 텍스트 출력과 입력된 사용자 입력을 불러오는 메서드
read
 async read(input) {
    return MissionUtils.Console.readLineAsync(input);
  }
  1. print : 콘솔에 텍스트를 출력하기 위한 메서드
print
 print(input) {
    return MissionUtils.Console.print(input);
  }

App

어플리케이션이 실행되는 클래스입니다.

  1. constructor : App 클래스가 사용할 인스턴스 정의
constructor
  constructor() {
    this.calculator = new Calculator([",", ":"]);
    this.console = new Console();
  }
 
  1. run : 각 기능을 조합해 하나의 기능 파이프라인 구성 및 실행
run
async run() {
    try {
      const userInput = await this.console.read(
        "덧셈할 문자열을 입력해 주세요.\n"
      );
      this.calculator.extractCustomSeperator(userInput);
      const processedInput = this.calculator.replaceAllSeperators(userInput);
      const validInputArray = this.calculator.validateInput(processedInput);
 
      const sum = this.calculator.sumAll(validInputArray);
      this.console.print(`결과 : ${sum}`);
    } catch (error) {
      throw new Error(error.message);
    }
  }

테스트 코드

테스트코드를 작성하라는 문제 조건은 없었지만, 연습 겸 추가 테스트 코드를 작성했습니다.

AdditionalTest.js
const mockQuestions = (inputs) => {
  MissionUtils.Console.readLineAsync = jest.fn();
 
  MissionUtils.Console.readLineAsync.mockImplementation(() => {
    const input = inputs.shift();
    return Promise.resolve(input);
  });
};
 
const getLogSpy = () => {
  const logSpy = jest.spyOn(MissionUtils.Console, "print");
  logSpy.mockClear();
  return logSpy;
};
 
describe("문자열 테스트", () => {
  const ERROR_INPUTS = [
    [["//\\n1:2"], "[ERROR] 커스텀 구분자의 길이는 0일 수 없습니다."],
    [["//!\\n1:3!4;3"], "[ERROR] 정의되지 않은 구분자가 포함되어 있습니다."],
    [["-2:3,4"], "[ERROR] 숫자는 자연수만 입력할 수 있습니다."],
  ];
 
  const PASS_INPUTS = [
    [["//!\\n1!2:3"], "결과 : 6"],
    [["//!@\\n1!@2,5"], "결과 : 8"],
    [["1,2"], "결과 : 3"],
  ];
 
  test.each(ERROR_INPUTS)("문자열 에러 테스트", async (input, message) => {
    mockQuestions(input);
 
    const app = new App();
 
    await expect(app.run()).rejects.toThrow(message);
  });
 
  test.each(PASS_INPUTS)("문자열 통과 테스트", async (input, message) => {
    mockQuestions(input);
 
    const logSpy = getLogSpy();
    const app = new App();
    await app.run();
 
    expect(logSpy).toHaveBeenCalledWith(expect.stringContaining(message));
  });
});

jest에 대한 학습이 더 필요한 만큼 기본 테스트 코드를 조금 변형해서 기본적인 테스트만 구현했어요. 다음 주차 문제부터는 본격적으로 테스트 코드를 만들어야 될 것으로 보이기 때문에 더 많은 연습이 필요할 것 같네요.

소감

예제 테스트코드를 통과

이번 1주차 문제를 풀기 전에 중점적으로 지킬 사항을 정리했습니다.

  • MVC 패턴으로 코드 구성하기
  • SRP 원칙 지키기

최대한 이 부분을 지키고자 여러 문서를 읽고 찾아보면서 코드를 구성했지만 아직 많이 부족하다고 생각합니다. extractCustomSeperator 메서드가 하는 일이 지나치게 많다거나, Calculator 클래스에서 검증 로직을 분리해 별도의 클래스로 관리한다거나 하는 부분을 놓친 것 같아 더 공부해야겠다 싶었어요.

추가로 정규표현식에 대한 학습이 부족해 정규표현식을 사용하지 않는 방향으로 진행했는데 이 부분에서 성능적 문제가 일어날 여지를 만든 것 같습니다. 커스텀 구분자를 포함한 구분자 목록을 순회돌며 문자열의 구분자들을 하나로 치환하여 다시 구분자를 제거하는 방식을 사용했는데, 정규표현식을 사용했다면 여러번의 순회를 사용할 일 없이 한번에 구분자를 처리할 수 있었을 테니 이 부분이 많이 아쉬웠어요.

하지만 이런 아쉬운점을 얻은 것도 1주차에서 이뤄낸 수확이라고 생각하고 2주차부터 더 좋은 코드를 작성하기 위해 노력해야겠다고 생각했습니다.

그리고 뜻밖의 사건

10월 22일, 2주차 문제가 나오는 날이자 1주차 문제에 대한 피어 리뷰가 시작된 날. 코드 리뷰를 위해 다른 지원자분들의 코드를 읽다가 다른 작업을 하고 있었는데 커뮤니티에서 심상찮은 분위기가 목격됐습니다.

소란스러워진 디스코드

어 나잖아?

알고보니 우테코의 원본 저장소가 어떤 이유로 삭제되면서 제 포크 저장소가 메인 저장소로 변경된 이슈였어요. 덕분에 갑자기 578 forks를 가진 대형 저장소의 주인이 되는 해프닝을 겪었습니다.

우리집에 운석이 떨어졌어요!

물론 아무런 문제가 없다면 상관없지만 기존의 PR 리뷰가 중단되었다는 문제가 생겨서 많은 분들이 문의를 보내셨고 제 계정에 있는 원본(이 되어버린) 저장소를 우테코 계정이 포크하는 형태로 문제는 일단락 되었어요.

프리코스 진행에 문제가 될만한 일은 아니어서 한편으로는 다행이고 이렇게 큰 선물(?)을 주신 우테코 7기 관계자 분들께 감사하다는 말씀 드립니다 🤣

샤라웃까지?!

2주차 문제부터는 1주차에서 겪은 부족한 부분들을 잘 다듬어서 더 나은 코드를 작성할 수 있도록 공부하고, 아직 다 진행하지 못한 1주차 코드에 대한 피어 리뷰도 열심히 해볼 생각이에요.

그럼 2주차 회고에서 만나요!