프로젝트를 진행하면서 가장 중요하게 생각했던 것 중 하나는 코드의 안정성이었습니다. 특히 여행 관련 서비스를 개발하면서, 사용자 경험에 직접적인 영향을 미치는 UI 컴포넌트들의 안정성은 매우 중요했습니다.
▪︎ 테스트 코드의 필요성 ▫︎ 복잡한 UI 상태 관리 여행 서비스의 특성상 다양한 상태를 가진 컴포넌트들이 많았습니다. 예를 들어 리뷰 카드의 경우 아래와 같은 복잡한 상태들을 수동으로 테스트하는 것은 비효율적이며, 실수하기 쉬웠습니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 describe ('리뷰 카드 컴포넌트' , () => { const props = { reviewId : 1 , profileImage : '/user.jpg' , reviewImage : '/test.png' , title : '테스트 리뷰' , content : '이것은 테스트 리뷰 내용입니다.' , starRating : 4.5 , travelLocation : '서울' , createdAt : '2023-10-10' , likesFlag : false , }; it ('리뷰 카드가 올바르게 렌더링되어야 합니다 (리뷰 페이지)' , () => { renderWithQueryClient (<ReviewCard {...props } nickname ="테스터" /> ); expect (screen.getByText ('테스트 리뷰' )).toBeInTheDocument (); expect ( screen.getByText ('이것은 테스트 리뷰 내용입니다.' ), ).toBeInTheDocument (); expect (screen.getByText ('테스터' )).toBeInTheDocument (); expect (screen.getByText ('4.5' )).toBeInTheDocument (); }); it ('리뷰 카드가 올바르게 렌더링되어야 합니다 (마이페이지)' , () => { renderWithQueryClient (<ReviewCard {...props } /> ); expect (screen.getByText ('테스트 리뷰' )).toBeInTheDocument (); expect ( screen.getByText ('이것은 테스트 리뷰 내용입니다.' ), ).toBeInTheDocument (); expect (screen.getByText ('4.5' )).toBeInTheDocument (); expect (screen.getByText ('서울' )).toBeInTheDocument (); }); it ('이미지가 올바르게 표시되어야 합니다' , () => { renderWithQueryClient (<ReviewCard {...props } /> ); const image = screen.getByAltText ('테스트 리뷰' ); expect (image).toBeInTheDocument (); });
▫︎ 비동기 데이터 처리의 안정성 API 호출과 같은 비동기 작업이 많은 서비스 특성상, 데이터 로딩, 에러 상태 등 다양한 상황에 대한 테스트가 필요했습니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 it ("로딩 중일 때 스켈레톤 UI를 표시한다" , () => { (useReview as jest.Mock ).mockReturnValue ({ isLoading : true , }); const { container } = renderWithQueryClient (<ReviewContents /> ); const skeletonElements = container.querySelectorAll (".skeleton-style" ); expect (skeletonElements.length ).toBeGreaterThan (0 ); }); it ("리뷰 데이터가 있을 때 리뷰 카드가 렌더링된다" , () => { (useReview as jest.Mock ).mockReturnValue ({ data : { pages : [ { data : { content : [ { id : 1 , nickname : "사용자1" , profileImage : "https://example.com/profile1.jpg" , reviewImage : "https://example.com/review1.jpg" , title : "리뷰 제목 1" , content : "리뷰 내용 1" , starRating : 5 , travelLocation : "서울" , createdAt : "2023-10-01" , isLiked : true , }, ], }, }, ], }, isLoading : false , isError : false , hasNextPage : false , fetchNextPage : jest.fn (), }); const { getByText } = renderWithQueryClient (<ReviewContents /> ); expect (getByText ("리뷰 제목 1" )).toBeInTheDocument (); expect (getByText ("리뷰 내용 1" )).toBeInTheDocument (); }); it ("리뷰 데이터가 없을 때 아무것도 렌더링하지 않는다" , () => { (useReview as jest.Mock ).mockReturnValue ({ data : { pages : [ { data : { content : [], }, }, ], }, isLoading : false , isError : false , hasNextPage : false , fetchNextPage : jest.fn (), }); const { queryByText } = renderWithQueryClient (<ReviewContents /> ); expect (queryByText (/리뷰 제목 1/i )).not .toBeInTheDocument (); });
▪︎ 테스트 코드 작성 전략 ▫︎ 테스트 시나리오 구성 테스트 코드는 크게 세 가지 관점에서 작성했습니다.
1. 기본 렌더링 테스트 1 2 3 4 5 6 7 8 9 10 11 describe ("ReviewComment" , () => { it ("리뷰 작성칸을 렌더링합니다" , () => { render (<ReviewComment /> ); expect (screen.getByText ("여행에 대한 후기를 남겨주세요!" )).toBeInTheDocument (); expect (screen.getByLabelText ("최대 20자 입력 가능 textarea" )).toBeInTheDocument (); expect (screen.getByPlaceholderText ("여행 제목을 입력해 주세요." )).toBeInTheDocument (); expect (screen.getByLabelText ("최대 100자 입력 가능 textarea" )).toBeInTheDocument (); expect (screen.getByPlaceholderText ("여행에 대한 다양한 후기를 공유해 주세요!" )).toBeInTheDocument (); }); });
2. 사용자 인터렉션 테스트 1 2 3 4 5 6 7 it ("페이지 버튼 클릭 시 paginate 함수가 호출된다" , () => { const paginateMock = jest.fn (); const { getByText } = render (<Pagination totalPages ={4} currentPage ={1} paginate ={paginateMock} /> ); fireEvent.click (getByText ("2" )); expect (paginateMock).toHaveBeenCalledWith (2 ); });
3. 엣지 케이스 테스트 1 2 3 4 5 6 7 8 9 10 11 12 13 14 it ("리뷰 데이터가 없을 때 빈 상태 메시지를 렌더링한다" , () => { (useMyReview as jest.Mock ).mockReturnValue ({ data : { data : { content : [], total : 0 , }, }, }); renderWithProvider (<Written /> ); expect (screen.getByText ("아직 작성한 리뷰가 없어요!" )).toBeInTheDocument (); });
▫︎ 비동기 테스트 처리 1. 로딩 상태 처리 1 2 3 4 5 6 7 8 9 10 it ("로딩 중일 때 스켈레톤 UI를 표시한다" , () => { (useReview as jest.Mock ).mockReturnValue ({ isLoading : true , }); const { container } = renderWithQueryClient (<ReviewContents /> ); const skeletonElements = container.querySelectorAll (".skeleton-style" ); expect (skeletonElements.length ).toBeGreaterThan (0 ); });
2. 데이터 페칭 결과 처리 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 it ("여행 데이터가 있을 때 여행 카드가 렌더링된다" , () => { (usePastTravel as jest.Mock ).mockReturnValue ({ data : { data : { content : [ { travelId : 1 , travelName : "과거 여행 1" , maxTravelMateCount : 5 , currentTravelMateCount : 2 , isDomestic : true , location : "서울" , image : "https://example.com/image.jpg" , startAt : "2023-10-01" , endAt : "2023-10-10" , }, ], total : 1 , }, }, }); const { getByText } = renderWithQueryClient (<PastTravel /> ); expect (getByText (/과거 여행 1/i )).toBeInTheDocument (); });
3. 에러 상태 처리 추가로 각 컴포넌트에서 발생할 수 있는 에러 상황을 고려하여 테스트케이스를 작성했습니다.
▫︎ 모킹 전략 활용 외부 의존성이 있는 컴포넌트의 경우, 적절한 모킹을 통해 테스트의 안정성을 확보했습니다.
1 2 3 4 5 6 7 8 jest.mock ("next/navigation" , () => ({ useRouter : () => ({ push : jest.fn (), replace : jest.fn (), back : jest.fn (), }), }));
▪︎ 테스트 코드 작성 시 중점 사항 ▫︎ 테스트 가독성 각 테스트 케이스의 의도가 명확히 드러나도록 작성했습니다.
1 2 3 4 5 6 7 it ('기본적으로 "myTravel" 탭이 선택되어야 한다' , () => { renderWithQueryClient ( <MainTab selectedTab ="myTravel" setSelectedTab ={setSelectedTabMock} setSelectedSubTab ={setSelectedSubTabMock} /> ); expect (screen.getByText (/나의 여행/i )).toBeInTheDocument (); });
▫︎ 테스트 유지보수성 반복되는 테스트 로직은 유틸리티 함수로 분리하여 재사용성을 높였습니다.
1 2 3 const renderWithProvider = (ui: React.ReactNode ) => { return render (<QueryClientProvider client ={queryClient} > {ui}</QueryClientProvider > ); };
▪︎ 마치며 테스트 코드 작성은 단순히 버그를 잡기 위한 도구가 아닌, 더 나은 설계를 위한 도구이자 문서의 역할도 수행했습니다. 특히 비동기 처리가 많은 현대 웹 애플리케이션에서 다양한 상황에 대한 테스트 케이스를 작성함으로써, 더 안정적인 서비스를 제공할 수 있었습니다.
앞으로도 테스트 커버리지를 높이고, 더 효율적인 테스트 전략을 발전시켜 나갈 예정입니다.