soyalattee 님의 상세페이지[2팀 박소연] Chapter 3-1. 프론트엔드 테스트 코드

Medium

7주차 과제 체크포인트

기본과제

Medium

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

질문

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


/**
 * Vitest에서 제공하는 모의 함수 생성함수입니다.
 * 모의 함수는 테스트 코드에서 특정 함수를 대체하여 테스트를 진행할 때 사용됩니다. 이를 통해 언제, 어떤 인자를 받고, 몇번 호출되었는지 추적할 수 있습니다.
 */
const enqueueSnackbarFn = vi.fn();

 /**
 * 모의 함수를 사용하여 notistack 모듈을 오버드라이브하여 enqueueSnackbarFn 로 교체합니다.
 * 이를 통해 테스트 코드에서 enqueueSnackbar 대신 enqueueSnackbarFn 을 사용하게 됩니다.
 *  즉, notistack 모듈을 테스트용으로 대체하여 호출되었을때의 동작을 쉽게 추적할 수 있게 해줍니다.
 */
vi.mock('notistack', async () => {
	// notistack의 실제 구현모듈을 가져옵니다
  const actual = await vi.importActual('notistack'); 
  return {
    ...actual, //그리고 전부 펼쳐서 적용한뒤
    useSnackbar: () => ({
      //해당 모듈만을 오버드라이브하여 enqueueSnackbarFn(모의함수)로 교체합니다.
      enqueueSnackbar: enqueueSnackbarFn,
    }),
  };
});

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

element를 render 할때 묶어주고 있는 ThemeProvider와 SnackbarProvider에 대해 작성하겠습니다.

ThemeProvider는 MUI 가 제공하는 테마 컨텍스트로서, 앱내에서 사용하는 MUI 컴포넌트들의 테마 관련 상태들이 공통적으로 적용되게 해줍니다. 해당 테스트에서는 UI의 theme 변화에 따른 테스트케이스는 작성되지 않았으나, 실제 환경과 동일한 테마 환경을 세팅하여 테스트할 수 있게 되어있습니다. 없으면 혹시 MUI 컴포넌트들 사용이 어려워 테스트 불가한 환경이 될까 싶어 제거해봤는데 ThemeProvider는 없어도 현재 작성된 테스트들은 정상동작했습니다. theme이 기본값으로 적용될것 같습니다.

SnackbarProvider 는 useSnackbar 로 알림을 띄우기 위해 필요한 컨텍스트입니다. 일정 추가&수정 시 등록 관련해서 사용되는 토스트가 이 useSnackbar 훅으로 생성됩니다. 통합테스트에서는 일정 추가&수정 관련 테스트를 하며 실패&성공시의 토스트도 확인할 수 있기 때문에 해당 Provider로 element를 감싸주어 토스트가 정상 동작 할 수 있도록해야합니다.

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

MSW로 목api 통신을 하는 테스트에서 필요한 동작들을 미리 생성한 함수들입니다.

각 함수별로 목이벤트들을 미리 초기화한 후 사용할 수 있어 테스트시 이벤트 생성과정을 간단히 하거나, 생략할 수 있습니다. 또한 저장,업데이트한 events를 저장하고 반환할 수 있는 DB 역할도 포함합니다.

  • setupMockHandlerCreation

initEvents 인자로 mockEvent 들을 등록할수 있게 도와주는 함수입니다. 초기 추가된 이벤트들을 저장해둡니다. 등록된 일정들의 조회 관련 테스트를 할 때 사용할 수 있습니다.

  • setupMockHandlerUpdating

두개의 이벤트가 미리 등록되어 있으며, GET,PUT 에 해당하는 네트워크 요청을 대신 수행합니다. get을 통해 미리 등록된 이벤트를 조회할 수 있고, put을 통해 이 등록된 이벤트를 수정할 수있습니다. 일정 수정 동작 테스트를 할때 사용할 수 있습니다.

  • setupMockHandlerDeletion

하나의 이벤트가 미리 등록되어 있으며,GET,DELETE 에 해당하는 네트워크 요청을 대신 수행합니다. 삭제를 테스트하기 위한 이벤트가 하나 등록되어있어 이를 조회하고 삭제할 수 있습니다. 이벤트 삭제동작을 테스트 할때 사용합니다.

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

달력과 일정관리가 메인 기능인 서비스로 날짜를 고정해 테스트 해야할 필요성이 있습니다.

서비스의 초기 뷰가 현재 날짜에 따라 다른 달이 보여야합니다. 그리고 다른 달/주 에 따라 그 시기에 맞는 일정이 노출되는데 이 또한 현재 날짜에 의존합니다. 날짜를 고정하지 않으면 테스트를 돌리는날마다 다른 화면이 뜨니 테스트가 실패하는 케이스가 생길 수 있습니다. 테스트도 더 까다로워질수 있습니다. 때문에 날짜를 고정하여 시나리오를 제어할 필요성이 있어 시간 설정을 사용해야 합니다.

또한 알림 같은 타이밍 기반 동작은 vi.advanceTimersByTime(...)로 시간를 전진시켜 시나리오를 제어해야합니다. 전반적으로 useFakeTimers() 를 사용해 가짜 타이머를 키고, vi.setSystemTime(고정할 시각)으로 현재 날짜를 고정해서 계속 재현가능한 테스트를 작성할 수 있습니다. 또한, 매 테스트 후 vi.useRealTimers()로 원복해 다른 테스트에 영향이 가지 않도록 방지합니다.

심화 과제

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

과제 셀프회고

기술적 성장

테스트 케이스 정의하기

과제 시작에 앞서 어떤 테스트코드가 잘 짠 테스트코드일지 고민해습니다. Client 단의 테스트에서는 사용자의 액션을 기준으로 검증을 한다면, 서비스의 검증도 잘 될 수 있겠다 생각했습니다.

그래서 우선 User action을 정의했습니다.

User Action 정의
  • 일정을 추가한다. -> 제목,날짜,시간,설명,위치,카테고리,알림설정,반복설정 입력

    • 날짜 선택은 달력아이콘을 눌러 달력에서 선택할 수 있다

      • 삭제버튼을 누르면 설정한 날짜가 삭제된다
      • 오늘버튼을 눌러 오늘로 날짜를 설정한다
      • 직접 입력하여 날짜를 지정할 수 있다
    • 시간 선택은 시계아이콘을 눌러 선택할 수 있다

      • 종료시간이 시작시간보다 늦으면 error 상태가 되며 '시작 시간은 종료 시간보다 빨라야 합니다. 종료 시간은 시작 시간보다 늦어야 합니다'라는 툴팁이 뜬다
    • 카테고리는 드롭다운메뉴로 설정한다

    • 반복일정은 체크박스로 표시할 수 있다

    • 알림설정은 드롭다운메뉴로 설정한다

    • 추가된 일정이 일정목록에서 확인가능하다

    • 일정 추가 버튼 클릭해 일정을 등록한다

      • 시간이 잘못 설정되어 있으면 '시간 설정을 확인해주세요.' 토스트가 뜬다
      • 필수 입력값(제목,날짜,시간)이 입력되지않을시 '필수 정보를 모두 입력해주세요' 토스트가 뜬다
      • 다른 일정과 시간이 겹쳐 일정 추가시 경고 모달을 보여준다
        • 계속진행을 눌러 등록한다
        • 취소를 눌러 등록과정을 계속 진행한다
      • 일정 추가 성공시 '일정이 추가되었습니다.'라는 토스트가 뜬다
  • 등록된 일정을 수정할 수 있다

    • 일정 리스트에서 편집 아이콘을 클릭하면 일정추가 영역이 일정 수정모드가 된다
    • 그 외는 추가와 동일
    • 일정 수정 버튼 클릭시 일정이 수정되었습니다. 토스트가 뜬다
    • 수정된 일정을 일정목록에서 수정됨을 확인할 수 있다
  • 달력 형태로 일정을 본다

    • Month/Week 를 선택해 원하는 형태로 달력을 본다
    • 전/다음 달(혹은 주)로 이동할 수 있다
    • 달력에서 등록된 일정을 확인할 수 있다
    • 알림예정(다가올 예정)인 일정이 표시되어있음을 확인할 수 있다
    • 달력에서 공휴일을 확인할 수 있다
  • 현재 보고있는 달력 기준으로 일정 리스트를 본다

    • 검색어로 일정을 검색한다
    • 한개의 일정을 수정한다
    • 한개의 일정을 삭제한다
    • 알림이 뜬 일정은 알림체크가 있음을 본다
    • 검색 결과or일정이 없으면 '검색 결과가 없습니다.' 메세지를 확인한다
  • 예정인 일정의 알림을 본다

    • 닫기를 눌러 닫을 수 있다

그리고 실제 테스트코드와 비교해보았습니다. 시간이 없어 거의 목요일부터 과제를 시작를 시작해서 실 적용은 해보지 못하고 문서로 작성해둡니다. ㅠㅠ

추가되면 좋을 테스트코드

  • 일정 추가/수정 폼
    • 날짜 선택 UI: 달력 아이콘으로 캘린더 열기, 날짜 선택 반영, 직접 입력과의 동작 일치 여부.
    • 시간 선택 UI: 시계 아이콘으로 시간 선택(혹은 직접 입력) 시 에러 상태/툴팁 표시 검증.
    • 필수값 검증: 제목/날짜/시간 누락 시 폼 제출 시도 → "필수 정보를 모두 입력해주세요" 토스트, 저장되지 않음.
    • 시간 에러 제출 경고: 잘못된 시간으로 제출 시 "시간 설정을 확인해주세요." 토스트, 저장되지 않음. -일정 겹침 모달
    • 계속 진행 시 저장
    • 취소 선택시 저장 안됨
  • 달력
    • Next,Previous 버튼을 통한 달력 이동 검증
    • Week/Month 버튼 으로 주단위/월단위 달력 노출 검증
  • 일정 리스트
    • 캘린더 뷰 기준 리스트 싱크: 통합 관점에서 월/주 변경에 따라 리스트도 동기화 되는지 확인
  • 알림
    • UI의 닫기 동작: 닫기 버튼 클릭 시 알림 사라지는것 확인
    • 폼에서 알림값 설정 후 실제 알림 발생 흐름 확인(현재는 mockupEvents 등록하여 진행)

코드 품질

mockEvents 전역 관리

mockEvents 를 계속 생성해서 테스트해야해서 이를 별도 constants 를 만들어 공용으로 사용할 수 있게 했습니다. 8c8a492a517bbd94158e9c2ce713d851b0aed44d

심화 과제: 리팩토링

App 컴포넌트 리팩토링을 진행했습니다.

src/
├─ 📄 App.tsx
├─ 📄 main.tsx
├─ 📄 constants.ts
├─ 📄 types.ts
├─ 📁 components
│  ├─ 📄 CalendarDateBox.tsx
│  ├─ 📄 CalendarContainer.tsx  
│  ├─ 📄 EventItem.tsx
│  ├─ 📄 MonthView.tsx
│  ├─ 📄 OverlapDialog.tsx
│  └─ 📄 WeekView.tsx
├─ 📁 hooks
│  ├─ 📄 useEventSubmit.ts
│  ├─ 📄 useOverlapDialog.ts
│  └─ 📄 ...
└─ 📁 utils
   ├─ 📄 eventValidation.ts
   ├─ 📄 ...

총 6개의 컴포넌트와, 2개의 훅, 1개의 유틸함수로 분리했습니다.

처음에는 로직을 App 컴포넌트에서 최대한 제거하기위해 useSubmit 훅을 만들었습니다. useSubmit 훅은 eventData객체 생성, 생성된eventData validation 체크, 일정 겹침 모달을 보여줄 여부 판단 후 저장및수정 까지 한 훅에 들어있게 되었습니다. 그래서 해당 훅을 다시 4가지로 쪼개어 정리했습니다. useEventSubmit : 비대한 Event 데이터를 useEventForm 훅에서 내보내도록 하고, 해당 훅에서 받아온 eventData 를 주입해 eventSubmit을 수행하는 훅을 생성했습니다. useOverlapDialog: 일정겹침 모달은 두개의 상태로 관리되고 있어, 훅으로 관리되도록했습니다. eventValidation: 일정 폼의 입력값을 검증하는 함수는 순수함수로 분리했습니다.

일정 추가&수정 폼도 분리하고싶었으나 시간이 부족해 미처 하지 못했습니다 ㅠㅠ

학습 효과 분석

테스트케이스를 작성하니 확실히 심화과제를 진행하며 리팩토링하는동안 더 대담(?)하게 코드를 쪼개고 분리할 수 있었습니다. 하지만.. 아직 통합테스트는 많이 어려워서 '왜 에러가 나는거지? 이건 왜 안나지?' 하면서 한참 헤맸습니다.. 하지만 코드 작성 스타일이 크게 튀는건 없어 많이 해보면서 테스트 케이스 범위 관리만 잘 한다면 오랫동안 좋은 퀄리티의 코드를 유지하기 좋겠다는 생각이 들었습니다! 조만간 1인 플젝이 생긴다면 적용해보려고 합니다~! (우선 혼자 해보고.. 전파하는걸로..)

과제 피드백

양심 고백하자면... 코치님께서 AI사용하지 말고 작업하라 하셨으나 unit 테스트의 엄청난 물량을 보고 조금씩 사용 해버렸습니다 ....ㅠㅠ 단, 최대한 직접 어떤 메소드를 사용하면 좋을지 생각하려고 노력하고, 단순 반복 코드들에만 사용하려고 노력했습니다...!

description 중간중간 '적절하게' 라는 표현이 있어 모호함을 느꼈습니다. 이런것들도 전부 '적절하게' 수정해서 작성하라는 히든 과제인거죠? 하지만 과제를 목요일에 반차쓰고 시작해서... PASS 만을 목표로 코치님의 숨은 과제구나 싶은것들은 냄새만 맡고 넘겼습니다 ㅠㅠ

리뷰 받고 싶은 내용

히든 과제중, 불필요한 테스트들도 있으며 이는 주석처리해보라고 하셨는데요, 저같은 경우 이건 통일(혹은 제거)해도 되지않을까? 라는 생각이 든 것들을 몇개 남겨봅니다!

  it('월이 한 자리 수일 때 앞에 0을 붙여 포맷팅한다', () => {
    expect(formatDate(new Date('2025-08-02'))).toBe('2025-08-02');
  });

  //Q. 위의 테스트케이스와 한번에 해도 되지 않을까
  it('일이 한 자리 수일 때 앞에 0을 붙여 포맷팅한다', () => {
    expect(formatDate(new Date('2025-08-02'))).toBe('2025-08-02');
  });
});

0을 붙혀 포맷팅하는것은 일/월을 한번에 처리해도 되지 않나 라는 생각이 들었습니다.

it('검색어에 맞는 이벤트만 필터링해야 한다', () => {
  const { result } = renderHook(() => useSearch(MOCK_EVENTS, TEST_DATE, 'month'));

  act(() => {
    result.current.setSearchTerm('점심');
  });

  expect(result.current.filteredEvents).toEqual([LUNCH_0822, LUNCH_0828]);
});

//Q. 위에 테스트랑 중복되는 테스트 같음
it('검색어가 제목, 설명, 위치 중 하나라도 일치하면 해당 이벤트를 반환해야 한다', () => {
  const { result } = renderHook(() => useSearch(MOCK_EVENTS, TEST_DATE, 'month'));

  act(() => {
    result.current.setSearchTerm('회의');
  });

  // LUNCH_0828는 설명에 회의 키워드가 있다.
  expect(result.current.filteredEvents).toEqual([METTING_0823, MEETING_0829, LUNCH_0828]);
});

검색어에 맞는 이벤트만 필터랑 하는 테스트에서 제목, 설명, 위치가 일치하는 이벤트도 반환하는거랑 비슷하지 않나? 한번에 써도 되지않나? 라는 생각이 들었습니다.

코치님은 어떻게 생각하시나 궁금합니다!

그리고 제가 '기술적 성장'쪽에서 user Action 작성했던것을 토대로 추가 테스트 목록을 작성했는데요, 적절한지 궁금합니다. 사실 hook 테스트나, unit 테스트에서도 어느정도 검증이 되고있는 케이스들이긴한데 이럴경우는 구지 통합테스트에서는 불필요할지, 아니면 유저플로우에 맞게(e.g. 입력 -> 모달 클릭 -> 모달 닫힘 확인) 통합테스트에서도 테스트케이스를 작성하는게 좋을지 궁금합니다.

과제 피드백

소연님 수고하셨습니다. 음 이정도로 하실거면 그냥 hard를 도전해보시지 그러셨어요 :) 많은 노력을 하신 흔적이 보여서 너무 좋습니다! 정말 시간만 더 있으셨다면 hard로 bp도 노릴 수 있으셨지 않았을까 싶어요! 수고하셨습니다~