JiHoon-0330 님의 상세페이지[5팀 이지훈] Chapter 3-1. 프론트엔드 테스트 코드

Medium

7주차 과제 체크포인트

기본과제

Medium

  • 총 11개의 파일, 115개의 단위 테스트를 무사히 작성하고 통과시킨다.

질문

Q. medium.useEventOperations.spec.tsx > 아래 toastFn과 mock과 이 fn은 무엇을 해줄까요?

import { useSnackbar } from 'notistack';

export const useEventOperations = (editing: boolean, onSave?: () => void) => {
  const { enqueueSnackbar } = useSnackbar();
  ...

  function ...() {
    // 훅 내부에서 어디선가 호출...
    enqueueSnackbar(...)
  }
}

위 코드 처럼 훅 내부에서 특정 코드를 가져다 사용하는 경우 테스트 코드에서는 해당 코드가 실행 되었는지 확인할 방법이 없다. 이때 mock 을 사용해 테스트에서 주입할 수 없는 코드를 모의할 수 있다.

const enqueueSnackbarFn = vi.fn();

// notistack 라이브러리를 모의한다.
vi.mock('notistack', async () => {
  const actual = await vi.importActual('notistack');
  return {
    ...actual,
    useSnackbar: () => ({
      // useSnackbar 훅의 enqueueSnackbar 함수를 모의한다.
      enqueueSnackbar: enqueueSnackbarFn,
    }),
  };
});

테스트 코드에서 vi.mock 은 'notistack' 라는 패키지를 모의하도록 해준다. 훅 내부에서 'notistack' 패키지를 가져와 사용하는데 useSnackbar 훅의 enqueueSnackbar 함수가 호출 되었는지 확인하기 위해 enqueueSnackbarFn 라는 모의 함수를 만들어 사용한다. enqueueSnackbar 함수가 실행되면 enqueueSnackbarFn 함수가 실행 된 것 처럼 동작한다.

Q. medium.integration.spec.tsx > 여기서 ChakraProvider로 묶어주는 동작은 의미있을까요? 있다면 어떤 의미일까요?

미디엄에선 다음과 같이 JSX 를 렌더링 하는 샘플이 작성되어 있었다.

const setup = (element: ReactElement) => {
  const user = userEvent.setup();

  return {
    ...render(
      <ThemeProvider theme={theme}>
        <CssBaseline />
        <SnackbarProvider>{element}</SnackbarProvider>
      </ThemeProvider>
    ),
    user,
  };
};

리액트를 사용할 때 Context 를 사용하는 경우가 있는데, 이때 테스트 환경에서도 정상적으로 동작하도록 만들기 위해 Context 를 설정해 주어야 한다. Context 설정은 공통 사항이므로 함수로 분리해 재사용 가능하도록 한 것

Q. handlersUtils > 아래 여러가지 use 함수는 어떤 역할을 할까요? 어떻게 사용될 수 있을까요?

미디엄에선 다음과 같이 msw 설정이 작성되어 있다.

export const setupMockHandlerCreation = (initEvents = [] as Event[]) => {
  const mockEvents: Event[] = [...initEvents];

  server.use(
    http.get('/api/events', () => {
      return HttpResponse.json({ events: mockEvents });
    }),
    http.post('/api/events', async ({ request }) => {
      const newEvent = (await request.json()) as Event;
      newEvent.id = String(mockEvents.length + 1); // 간단한 ID 생성
      mockEvents.push(newEvent);
      return HttpResponse.json(newEvent, { status: 201 });
    })
  );
};

테스트 환경에서 각 테스트마다 격리된 환경을 만들어 서로 영향을 받지 않도록 해야 한다. 테스트에서 setupMockHandlerCreation 함수를 사용하는 경우 데이터로 사용되는 mockEvents 값은 메모리에 존재하는 값이고, 함수 호춣마다 생성되기 때문에 격리된 환경을 만들 수 있다. 공통으로 필요한 msw 동작도 적어주어 중복을 줄이고 재사용할 수 있다.

Q. setupTests.ts > 왜 이 시간을 설정해주는 걸까요?

beforeEach(() => {
  vi.setSystemTime(new Date('2025-10-01')); 
});

테스트의 경우 같은 입력에 대해 같은 출력을 반환하는 것을 테스트하게 된다. 하지만 날짜의 경우 테스트 실행 시점에 따라 값이 변하게 되는데, 이는 날짜를 활용하는 테스트에서 동일한 입력에 대해 동일한 출력을 반환하지 못하게 만든다. beforeEach 에서 특정 날짜를 지정하는 이유는 테스트 환경에서 사용할 날짜를 고정시켜 항상 동일한 환경에서 코드가 실행될 수 있도록 제어하기 위함이다.

심화 과제

  • App 컴포넌트 적절한 단위의 컴포넌트, 훅, 유틸 함수로 분리했는가?
  • 해당 모듈들에 대한 적절한 테스트를 5개 이상 작성했는가?

과제 셀프회고

그동안 테스트를 열심히 작성해 본 적이 없었다. 그래서 테스트 코드들이 익숙하지 않고, 코드를 읽어서 파악하려는 의지가 없었던 것 같다. AI 를 통해 테스트 코드를 생성하는 경우에도 테스트를 통과 하는지 실패하는지 확인만 하는 경우가 많았다. 실제로 지난주 과제는 테스트 코드가 제공되지 않았는데, e2e 테스트를 자동으로 생성한 후 테스트를 통과하지 못해 그냥 테스트 코드를 지우고 과제를 진행했다. 이번 주차 과제를 진행하면서 테스트 코드에 익숙해 지는 계기가 되었다. query, get, find 에 대한 차이점 훅과 컴포넌트를 테스트 하는 방법 등 앞으로 테스트 코드를 파악할 때 자주 봐야하는 함수들에 대해 알게되었다. 물론 공식 문서를 읽으면 쉽게 알 수 있는 부분들도 있지만, 그동안 실천으로 옮기지 못 한 것을 생각했을 때 실천을 하도록 만든 점에 의미가 있다고 생각한다. 아직 요소를 선택하는 방법에서 익숙하지 않고, 어떤 방법을 사용해야 효율적일지 의문이 드는 점도 있다. 접근성을 활용하는 방법이 권장되는 것을 알고 있지만, mui 를 사용한 컴포넌트 요소를 선택하는 것이 복잡하게 느껴졌다. 그냥 id 로 가져오면 안되나? 이런 생각이 많이 들었다.

어떤 것을 테스트 해야 하는지 생각해 보는 시간도 가졌다. 단순한 코드는 테스트 할 필요가 없을까? 테스트를 한다면 어떤 범위를 다뤄야 할까? 유틸리티 성향의 함수들에 대해선 라이브러리 예제를 적는 것 처럼 테스트를 작성하는 것이 좋다고 생각한다. 유틸리티 함수들은 맥락에 상관 없이 사용이 가능하고, 사용 범위를 명확히 하는 것이 필요하다 생각한다. 도메인을 다루는 함수들의 경우 정해진 역할을 하는 경우가 많기 때문에, 기획 단계에서 정해진 케이스 들을 테스트 할 수 있으면 충분하다는 생각이 든다. getDaysInMonth 함수의 경우 입력 폼에서 전달되는 112 이외 범위를 테스트 해야 하는가 라는 주제로 이야기를 나눈 적이 있는데, 이런 관점에서 생각해 보면 과제에 있는 getDaysInMonth 함수의 경우 입력 폼에서 112 월 까지만 입력되는 것과 별개로 0, 음수 값, 12 를 초과하는 값 모두 테스트하는 것이 유틸리티 성향의 함수에 알맞다고 생각한다.

기술적 성장

  • getBy, queryBy, findBy 의 차이점
  • 요소를 가져오는 함수를 document.querySelector 와 비슷하게 생각하고 있었는데, document.querySelector 의 경우 해당하는 요소가 여러개인 경우 첫번째 요소를 반환하지만, getBy, queryBy, findBy 함수들의 경우 여러 오소가 있을 시 오류가 발생한다는 점
  • 접근성을 통해 원하는 요소를 선택하는 것이 생각보다 어려웠다. 접근성 요소들에 대해 조금은 알게 된 것 같다.

코드 품질

  • 코드를 분리하면서 props 전달이 많아지게 되었는데, 깊이가 깊지 않아 우선 prpos로 처리했다. 전역 상태를 사용하면 더 깔끔하게 가능할 것 같다.
  • 테스트 메세지를 명확하게 작성하고 싶었는데, 생각보다 어려운 것 같다. 특히 날짜를 다루는 부분에서 '주의 날짜를 반환한다.' 와 같은 문장들을 더 개선하면 좋을 것 같다.

학습 효과 분석

  • 테스트 코드에 대해 익숙해지게 된 것이 가장 큰 배움이라 생각한다.
  • 간단한 유틸 함수부터, 훅 까지 실무에 적용이 가능할 것 같다.
  • 통합 테스트의 경우 상대적으로 중요한 부분 부터 적용해 볼 수 있을 것 같다.

과제 피드백

  • 테스트를 처음 접하는 기준에서 적당히 어려운 난이도인 것 같습니다.
  • 과제를 진행하면서 캘린더, 알림 이라는 주제가 테스트를 학습하기 알맞다고 느꼈습니다. 다양한 엣지 케이스들..

리뷰 받고 싶은 내용

src/__tests__/medium.integration.spec.tsx 파일에서 'event를 추가 제거하고 저장하는 로직을 잘 살펴보고, 만약 그대로 구현한다면 어떤 문제가 있을 지 고민해보세요.' 라는 주석이 존재하는데, 어떤 문제가 존재할 수 있는지 잘 모르겠습니다. api 호출시 에러를 처리하는 테스트가 존재하지 않는 것을 의미하는 걸까요?

describe('일정 CRUD 및 기본 기능', () => {
  it('입력한 새로운 일정 정보에 맞춰 모든 필드가 이벤트 리스트에 정확히 저장된다.', async () => {
    // ! HINT. event를 추가 제거하고 저장하는 로직을 잘 살펴보고, 만약 그대로 구현한다면 어떤 문제가 있을 지 고민해보세요.

과제 피드백

안녕하세요 지훈님! 7주차 과제 잘 진행해주셨네요 ㅎㅎ 고생하셨습니다!!

src/tests/medium.integration.spec.tsx 파일에서 'event를 추가 제거하고 저장하는 로직을 잘 살펴보고, 만약 그대로 구현한다면 어떤 문제가 있을 지 고민해보세요.' 라는 주석이 존재하는데, 어떤 문제가 존재할 수 있는지 잘 모르겠습니다. api 호출시 에러를 처리하는 테스트가 존재하지 않는 것을 의미하는 걸까요?

  1. 동시성 문제: 비동기 작업 중 상태 변경으로 인한 불일치 가능성
  2. 에러 복구 부족: 실패 시 사용자 데이터 보호 미흡
  3. 중복 요청 미방지: 사용자 액션에 대한 디바운싱 부재
  4. 트랜잭션 부족: 여러 단계 작업의 원자성 보장 미흡

일단 이렇게 4가지 정도라고 생각하는데, 사실 다 퉁쳐서 에러처리라고 묶을 수 있을 것 같아요 ㅎㅎ "원하는대로 동작하지 않는 경우"에 대한 내용들이니까요. 이에 대한걸 테스트로 검증해볼 수 있지 않을까 싶네요!