본문 바로가기

React/실험실

[React] TDD, 클린 코드 with React 3기 - 1주차

TDD, 클린 코드 with React 3기에서 매주 과제를 진행한다. 

 

이번주는 온보딩 미션으로 계산기를 만드는 과제를 진행하였다. 

계산기?! 라고 생각하고 내가 듣기로는 TDD, 클린 코드 강의는 난이도가 어렵다고 생각했는데 김이 팍 식었다...

 

알고보니 말 그대로 온보딩 미션이고 본격적인 미션들은 난이도가 있어보였다. 

어쨋든 돌아가서 계산기 과제를 하는데 조건이 React와 Vite 환경에서 구현을 해야했다. 

 

Vite,,,, 참 많이 들었지만 CRA가 너무 편해서 굳이 사용할 생각이 없었는데 이번에 사용해보니 많이 신기했다. 

npm run dev를 하면 순식간에 프로젝트가 실행되고 웹사이트에서 확인할 수 있다. 

 

그리고 Jest를 테스팅을 위해서 사용했는데 CRA에서는 바로 사용할 수 있던게 추가적인 환경 설정을 해야했다. 

( 이부분은 진짜 진짜 다음에 작성하겠다...!!! ) 

 

처음으로 TDD를 고려하면서 개발을 해봤는데, 확실히 한번에 모든 테스트 케이스를 고려해서 개발하는 것은 어려웠다.

나름의 테스트 케이스를 통해서 개발을 하다가 중간에 떠오르는 새로운 테스트를 위해서 다시 테스트를 작성하는 과정을 반복하면서 자괴감에 쌓였다,,,, 

 

그래도 미리 테스트케이스를 통해서 개발하니 중간에 수정해야 하는 경우가 발생해도 오류에 대한 걱정이 없어졌다. 

 

리뷰어님과의 질답

질문 1.

현재 전체적인 과정인 연산은 utils의 calculator에서 처리를 하는데,
작업을 하다보니 어디까지 커스텀훅 (useCalculator.js)에서 처리하고 어디까지 유틸함수
(calculator.js)에서 처리해야할지고민이 되더라구요!

유틸 함수에 매번 total 값을 넘겨주고 있는데 이것을 해결하자고 calculator.js에서 total을 가지고 
있으면 상태 값 total과 변수 total이 중복되는 것 같아서 현재는 상태 값으로만 관리하고 있습니다!

작업을 하다보니 커스텀 훅에서는 상태와 값을 받아와서 상태를 업데이트 하는 정도의 작업만 하고 

실제 연산 처리들은 유틸함수를 따로 만들어서 처리했다. 

 

그러다보니 해당 커스텀 훅이 필요할지, 아니면 유틸함수의 함수들을 커스텀 훅으로 넣어주는게 더 좋을지 

의문이 생겼다. 

 

해당 미션의 경우 유지해야 하는 상태가 단순하기 때문에 연산에 대한 
로직을 Calculator 클래스가 담당하기만 해도 될 것 같습니다.

useCalculator 훅의 역할은... 현재로선 상태를 제공하는 것 밖에 없어보여요. 
그렇다면 클래스 메서드로 연산만 처리하는 로직으로도 앱 구성은 충분해 보입니다!

만약 에러 핸들링, 좀 더 복합적인 연산들(연산 정보 저장, 엣지 케이스 처리 등)이 포함되거나 
컴포넌트의 복잡성이 높아진다면 렌더링 성능을 위해서 훅으로 분리할 순 있을 것 같네요!

답변으로 큰 프로젝트가 아니기 때문에 굳이 커스텀 훅으로 만들 필요가 없고 

useCalculator 커스텀 훅이 상태를 제공하는 것 이상의 기능을 제공하지 않기 때문에 굳이 필요없을 것 같다는 답변을

받았다. 

 

그렇다면 해당 useCalculator 커스텀 훅의 기능이 반복되는 경우라면 커스텀 훅으로 빼도 괜찮을까? 

이런 부분에 대해서 추가적으로 질문을 드렸다. 

 

답변을 받으면 추가해서 등록해두겠다. 

 

질문 2. 

테스트를 진행하면서 기능 자체는 중복인데 해당 함수에서 정상 작동하는지 확인해야 하는 경우가 
있더라구요!
이런 부분이 오버코딩인지 궁금합니다!

개인적으로 저는 필요한 과정이라고 생각합니다!

describe("숫자 입력 테스트", () => {
  const calculator = new Calculator();

  it("0 연속 입력", () => {
    let total = "0";
    total = calculator.updateNumber(0, total);
    total = calculator.updateNumber(0, total);

    expect(total).toBe("0");
  });

// ...

describe("입력 테스트", () => {
  it("첫번째 숫자 입력", () => {
    const { result } = renderHook(() => useCalculator());

    act(() => {
      result.current[1](1);
    });

    expect(result.current[0]).toBe("1");
  });

// ...

이 부분은 앞서 useCalculator와 Calculator 유틸함수를 만들면서 비슷한 테스트 코드를 반복해서 

작성하게 되는 모습을 어느순간 발견했다. 

 

예를들어 코어 기능은 Calculator 유틸 함수에 작성을 해서 테스트를 통해 통과했지만 

useCalculator 커스텀 훅에서 해당 Calculator 유틸 함수를 제대로 사용하는지 확인하기 위해서 동일한 테스트를 

작성할 수 있지 않을까?? 

 

이 부분은... 렌더링이 잘 일어나는지 + 기능이 잘 동작하는지의 기준이 명확해야 할 것 같은데요, 
Calculator 클래스의 동작이 “정상적이다“라는 결과값이 있다면 렌더링에 사용되는 
Calculator 클래스의 역할은 제대로 수행된다는 전제가 동작하기 때문에 중복된 기능 테스트로 
볼 수 있을 것 같습니다.

사실 질문 1과 이어지는 내용인데 useCalculator 훅이 실제로는 Calculator 클래스를 래핑한 
형태라서 이런 테스트가 생기지 않았을까 싶어요.

답변을 통해서 느낀 부분은 특정 기능이 다 동작하는지를 테스트하기보단 

원하는 경우의 수에 도달하는 과정을 테스트하면 될 것 같다는 생각이 들었다. 

 

작성자는 기능을 직접 구현했기 때문에 이해를 하지만 모르는 읽는 분들을 위해서 추가 설명을 하자면 

useCalculator에서 update라는 함수가 Calculator 유틸 함수를 호출해서 사용한다. 

 

그렇다면 useCalculator에서는 update라는 함수에 값이 제대로 가고 있는지만 테스트를 하고 그 외 

Calculator 유틸 함수가 제대로 동작하는지는 굳이 검증할 필요가 없다는 생각을 받았다. 

 

질문 3. 

기능 구현하다보니 어디까지 추상화를 해야 할지 애매한 부분이 생기더라구요!

checkOperator(lastChar, total) {
    return OPERATOR.includes(lastChar) || total === INITIAL;
  }

  checkMaxOperator(total) {
    let check = false;

    OPERATOR.forEach((oper) => {
      if (total.includes(oper)) {
        check = true;
      }
    });

    return check;
  }

  updateOperator(value, total) {
    const lastChar = total.slice(-1);

    if (this.checkOperator(lastChar, total)) {
      window.alert(ERROR.operator);
      return total;
    }

    if (this.checkMaxOperator(total)) {
      window.alert(ERROR.maxOperator);
      return total;
    }

    return total + value;
  }
  
  위 코드에서는 바로 비교를 하는 경우 (checkOperator)가 있지만 2가지를 경우 
  ( OPERATOR.includes(lastChar) | | total === INITIAL가 있고, 
  
  아에 순회를 해야하는 경우 ( checkMaxOperator)가 있어서 추상화를 했습니다.
  
  updateNumber(value, total) {
    const parts = total.split(OPERATOR_REGEX);
    const lastPart = parts[parts.length - 1];

    const updateValue = Number(lastPart + "" + value);

    if (updateValue > MAXIMUM) {
      window.alert(ERROR.maximum);
      return total;
    }

    parts[parts.length - 1] = updateValue.toString();

    return parts.join("");
  }
위 코드에서는 하나의 조건으로 비교를 하기 때문에 if문을 바로 사용하는 게 
오히려 가독성이 좋다고 생각해서 바로 사용했습니다.

또한 추상화를 할 때, if 조건만 하는게 좋을지 아니면 window 같은 함수도 한번에 처리하는게 
좋을지 고민이 되더라구요! 이부분도 의견 주시면 감사하겠습니다!

이번에는 질문을 하면서도 질문을 못했다는 생각이 들었다. 

( 실제로 답변도 이해하기 힘들었다는 답변이 ... )

 

어쨋든 함수를 만들 때 어떤 경우에 추상화를 하는게 좋을지 궁금한 부분이 있었다. 

그래서 예시를 들었는데 오히려 예시 때문에 헷갈리신거 같다는,,,,, ( 질문도 못함... )

 

추상화의 질문인지, 가독성의 질문인지 잘 이해하지 못했습니다😅 
책임과 역할을 구분해서 메소드를 분리한 게 아니라 로직 특이성으로 코드를 나눈 것 같아서 
어떻게 리팩토링하면 좋을까요? 에 포커싱이 될 것 같네요.

우선 클래스부터 볼 때, 비즈니스 로직의 처리를 담당하고 있는데 UI 쪽의 window.alert을 
핸들링하고 있습니다. 

window나 globalThis 를 계산기 클래스가 알아야 하는 이유가 있을까요? 
에러를 리턴하거나 throw 하여 UI 레벨에서 처리하게 하는 것이 우선적으로 선행되면 좋겠네요!

헷갈려 하셨지만 그래도 챡! 답변을 해주시는 멘토님...ㅠㅠ 

 

기능에서 굳이 에러 핸들링을 할 필요가 없을 것 같다고 하셨다.

맞는 것 같다는 생각이 든다.

 

하지만 클래스에서 상태를 처리하는데 이것을 커스텀 훅 또는 UI 로직에서 처리하려면 이벤트의 흐름이

너무 길어지는게 아닐까? 라는 생각이 든다. 

 

프로젝트를 더 진행하면서 이 부분을 염두하고 개발을 해보며 경험을 쌓고 추가 질문을 해보자! 

 

질문  4. 

const MAXIMUM = 999;
const ERROR = {
  maximum: "숫자는 세 자리까지만 입력 가능합니다!",
  operator: "숫자를 먼저 입력한 후 연산자를 입력해주세요!",
  maxOperator: "두개의 숫자 연산만 가능합니다!",
};
export const INITIAL = "0";

const INFINITY = "오류";

const OPERATOR = ["+", "-", "X", "/"];
const OPERATOR_REGEX = new RegExp(
  `(${OPERATOR.map((op) => `\\${op}`).join("|")})`
);

const CALCULATE = "=";

상수의 경우 따로 상수 파일을 만들어서 관리하는 것도 좋았지만
이런 파일이 너무 많아지면 이것도 찾아서 수정하는게 생각보다 시간이 걸리더라구요!

그래서 가장 연관이 많이 되는 곳에서 상수를 선언하고 동일한 값이 필요하면
각각의 장소에서 export를 하고 있는데, 이것을 따로 뺴는게 관리 측면에서 좋을지 고민입니다.

제 경험상에는 한번에 있는게 프로젝트가 커졌을 때 찾는 게 수월해서 좋다고 생각되는데
리뷰어님의 생각이 궁금합니다!

상수를 많이 사용하는데 이런 상수를 한 곳에 모아서 선언을 하는게 좋을지

아니면 필요한 함수에서 선언을 하는게 좋을지에 대한 고민을 질문했다. 

 

당연히 답이 없을 것 같은데 그래도 어느정도의 인사이트를 얻어보고 싶어서 질문했다. 

( 질문에서도 그렇다는 느낌으로 질문했지... )

 

상수를 관리하는 방법에 대해 생각해볼 때, 
"지역성(locality)"과 "전역성(globality)"이라는 두 가지 개념이 유용하게 적용될 수 있습니다.

지역 상수는 보통 코드에서 관련된 부분을 함께 유지하는 개념으로 특정한 문맥이나 모듈 내에서 
상수를 정의하는 것이죠. 
상수가 특정 모듈 내에서만 사용되고 다른 모듈과 관련이 없는 경우, 
이를 지역적으로 유지함으로써 코드의 가독성을 높일 수 있습니다.

전역 상수는 상수를 한 번 정의하고 프로젝트 전반에서 공유하여 사용하는 개념인데, 
여러 모듈에서 공통적으로 사용되는 경우에 전역적으로 정의하여 중복을 줄이고 관리를 
효율적으로 할 수 있습니다.

각각의 장단점이 있지만, 둘 모두 코드 품질을 신경 쓸 때 지역으로 둘 지, 
전역으로 둘 지 프로젝트의 크기와 상수의 의미, 사용 범위를 명확히 하고 중복 정의를 피하는 고민을 
치열하게 해야 합니다. 

명확한 정답은 없고 고민 포인트를 지속적으로 개발해보면서 적용하는 게 최선일 것 같네요😇

역시 명확한 답은 없었지만 그래도 각각 어떤 경우에 많이 가는지에 대한 인사이트를 얻을 수 있었다. 

 

정답이 있는 질문을 통해 얻어가는 것도 좋지만 이런 애매한 부분에 개발자의 의견을 듣기 위해서 

이러한 질문을 해보는 것도 좋은 것 같다. 

반응형

'React > 실험실' 카테고리의 다른 글

useFunnel 만들기  (2) 2024.06.16
[React] Controlled and UnControlled Input  (0) 2024.03.14
[React] Funnel 컴포넌트  (1) 2024.03.07
[React] PWA 그것은 무엇인가?  (0) 2024.03.03
[React] Vite 환경 구성하기  (0) 2024.02.28