React, Beyond the Basics 19일차

anvel·2025년 4월 9일

항해 플러스

목록 보기
11/39
post-thumbnail

멘토링 정리

금주는 주중 출장으로 인해 주말에 과제를 모두 처리하고, 멘토링을 받는 오늘 배포과제 까지 완료했습니다.

직면한 문제

과제를 진행하면서 직면한 문제는 환경 오류 2, 테스트 오류 1 이었습니다.

  • 저장 시 eslintprettier 충돌 오류
  • 커밋 시 주석이 사라지며 변경사항이 다시 생기는 오류
  • 테스트 코드 중 마지막 조건 generateItemsSpy

해결한 방법

  • 저장 시 충돌 문제는 함수의 파라미터에 들어오는 마지막 인자가 eslint의 설정에 의해 콤마가 없으면 에러라인이 발생하고, 저장시에는 prettier에 의해서 콤마가 삭제되어 저장되어 다시 에러라인이 발생하였습니다.

    • .prettier 에서 규칙을 통합하여 모두 콤마가 붙도록 수정하여 해결했습니다.

      
      {
        "trailingComma": "all"
      }
  • 커밋 시 주석이 지워져 다시 변경사항이 생기는 오류는 해결하지 못했습니다.

  • 마지막으로 코드상의 문제는 먼저 GPT를 통해 답을 찾았지만, 정확한 로직을 이해하지 못해 멘토링 시간에 질문을 드렸었고, 정확한 답변을 받았습니다.

  • 또한, 그 문제 또한 최적화 관련 의도였다고 확인하여 왜 해야 하는 지 까지 알게된 문제였습니다.

문제

테스트 코드 중 vitest의 함수를 실행하는 빈도를 테스트하는 코드에서 generateItemsSpy가 1번만 호출되어야 하지만 2번 호출된다는 테스트 실패 문제였습니다.

  • 테스트 코드

    
    const renderLogMock = vi.spyOn(utils, "renderLog");
    const generateItemsSpy = vi.spyOn(utils, "generateItems");
    
    describe("최적화된 App 컴포넌트 테스트", () => {
      beforeEach(() => {
        renderLogMock.mockClear();
        generateItemsSpy.mockClear();  
      });
      
      it("여러 작업을 연속으로 수행해도 각 컴포넌트는 필요한 경우에만 리렌더링되어야 한다", async () => {
        render(<App />);
        renderLogMock.mockClear();
        /* ... */
      
        // 알림 닫기 버튼 찾기 및 클릭
        await fireEvent.click(await screen.findByText("닫기"));
        expect(renderLogMock).toHaveBeenCalledWith("NotificationSystem rendered");
        expect(renderLogMock).toHaveBeenCalledWith("ComplexForm rendered");
        expect(renderLogMock).toHaveBeenCalledTimes(2);
        // HERE
        expect(generateItemsSpy).toHaveBeenCalledTimes(1);
      });
    });
  • App.tsx

    
    const AppContent = memo(() => {
      const { theme } = useThemeContext();
      // lazy initalizer 문제 >> advanced 마지막 테스트 gnerateItemsSpy
      // const [items, setItems] = useState(generateItems(1000)); // 실패
      const [items, setItems] = useState(() => generateItems(1000)); // 성공
    
      const addItems = useCallback(() => {
        setItems((p) => [...p, ...generateItems(1000, p.length)]);
      }, []);
    
      return (/* ... */)
    }

원인

일단 테스트 코드는 generateItems 함수를 실행하는 코드이고, 재랜더링 시 해당 함수가 호출이 되는 지를 확인하여 불필요한 함수 실행이 일어나는 지를 묻는 문제였습니다.

  • useState에 함수에 의한 결과 값을 지정하는 경우에는 컴포넌트가 재랜더 될 때, 해당 함수도 계속 다시 호출하여 불필요한 함수 실행이 이뤄지고, 아이템을 생성하는 갯수에 따라 성능이 끝도없이 늘어날 수 있었습니다.

  • 다만, 해당 값은 함수를 실행한다고 하여 useState에 계속 반영되는 값이 아니어서 불필요한 함수 호출이 되는 것이었고, 이는 페이지가 바뀌면서 재랜더 되는 경우에 성능을 악화시켰습니다.

  • 함수를 실행한 결과 값이 아닌, 함수를 useState에 넘기게 되면, 함수를 저장해두었다가 최초 랜더링시에 한번만 호출하여 사용하는 로직으로 변경됩니다.

    // 1. 값을 반영
    const [items, setItems] = useState(generateItems(1000));
    // 랜더링 시 마다 값을 호출함
    // 컴포넌트 호출 즉시 실행
    const tmp = generateItems(1000); // <<
    const [items, setItems] = useState(tmp);
    // HERE >> useState에 값이 갱신되진 않지만 generateItems함수는 계속 호출하는 문제인 지?
    
    // 2. 함수를 반영
    const [items, setItems] = useState(() => generateItems(1000));
    // 초기 랜더 시 1번만 실행함
    // 함수를 저장해두었다가 초기 랜더 시에만 한번 실행되고,
    // 그 결과값이 초기값이 됨
  • 멘토링 간에 질문 드린 내용

    • useState에 반영되진 않지만, 함수를 계속 불필요하게 계속 호출하는 게 문제가 맞는 지?
      → 맞음
    • 다른 테스트 코드와 마찬가지로 최적화를 진행하는 지 의도한 문제 인지?
      → 맞음

마치며

멘토링 간에는 개발 이외에 항목에 대하여도 구체적으로 가르쳐주셨습니다.
해당 내용은 다른 부분에서 정리하도록 하고, 이번 과제를 진행하면서 라이브러리 React가 내부적으로 동작하는 원리를 기본 개념을 바탕으로 이해할 수 있었던 과정이었습니다.

회사에서 가끔 교육을 진행할 때가 있는데, React Hook에 대하여 교육을 진행하게 되면 해당 과제를 참조하면 좋은 교육 내용을 만들어 낼 수 있을 것 같다고 느껴졌던 과제였습니다.

또한, 과제를 일찍 끝냈던 만큼 오랜시간 고민하여 테스트 코드의 의도한 바와 문제점을 잘 찾아낼 수 있었던 것 같아 앞으로도 과제를 최대한 빨리 완료하고, 문제가 되는 부분 위주로 해결방법과 원리를 익히는 데 시간을 활용하면 더 좋은 결과를 보일 것이라고 생각되었습니다.

0개의 댓글