PromleeBlog
sitemap
aboutMe

posting thumbnail
API Mock 전략과 MSW 활용 - React로 시작하는 TDD 9편
API Mocking Strategy and Using MSW - TDD with React Part 9

📅

🚀

들어가기 전에 🔗

지난 8편에서는 global.fetch를 직접 가로채서 테스트를 진행했습니다.
하지만 이 방식은 매번 코드를 재정의해야 하고, 실제 네트워크 동작을 완벽히 흉내 내기 어렵습니다.

오늘은 프론트엔드 테스트의
사실상 표준
(De facto standard)으로 불리는
MSW
(Mock Service Worker)를 도입해 보겠습니다.
이 도구는 네트워크 요청을 중간에서 가로채는(Interceptor) 역할을 하여, 성공 케이스뿐만 아니라
서버 에러(500) 상황
까지 자유자재로 테스트할 수 있게 해줍니다.

🚀

1단계: MSW 설치 및 설정 🔗

먼저 라이브러리를 설치합니다.
(터미널에서 프로젝트 폴더로 이동 후 실행해 주세요.)
Terminal
npm install -D msw

핸들러(Handler) 생성 🔗

핸들러
(Handler)는 "특정 URL로 요청이 오면 이렇게 응답해라"라고 정의하는 규칙서입니다.
src/mocks 폴더를 만들고 handlers.ts 파일을 생성합니다.
src/mocks/handlers.ts
import { http, HttpResponse } from 'msw';
 
// 가짜 API 주소를 정의합니다.
// 실제 UserList 컴포넌트가 호출하는 URL과 똑같아야 합니다.
export const handlers = [
  http.get('https://jsonplaceholder.typicode.com/users', () => {
    // 이 URL로 GET 요청이 오면, 아래 JSON 데이터를 돌려줍니다.
    return HttpResponse.json([
      { id: 1, name: 'Leanne Graham (MSW)' },
      { id: 2, name: 'Ervin Howell (MSW)' },
    ]);
  }),
];

테스트 서버 설정 🔗

우리는 브라우저가 아닌 터미널(Node.js 환경)에서 테스트를 돌리고 있습니다.
따라서 브라우저용 워커 대신
Node.js용 서버
를 만들어야 합니다.
src/mocks/server.ts 파일을 생성합니다.
src/mocks/server.ts
import { setupServer } from 'msw/node';
import { handlers } from './handlers';
 
// 위에서 만든 핸들러들을 묶어서 가짜 서버를 생성합니다.
export const server = setupServer(...handlers);

🚀

2단계: 테스트 환경에 MSW 적용 🔗

이제 테스트가 시작될 때 가짜 서버를 켜고, 끝나면 끄도록 설정해야 합니다.
이전에 만들어둔 src/setupTests.ts 파일을 수정합니다.
src/setupTests.ts
import '@testing-library/jest-dom';
import { server } from './mocks/server';
import { beforeAll, afterEach, afterAll } from 'vitest';
 
// [설정] 모든 테스트가 시작되기 전에 가짜 서버를 켭니다.
beforeAll(() => server.listen());
 
// [정리] 각 테스트가 끝날 때마다 핸들러를 초기화합니다.
// (테스트 중간에 에러 상황으로 바꿨더라도 다시 정상 상태로 되돌립니다)
afterEach(() => server.resetHandlers());
 
// [종료] 모든 테스트가 끝나면 서버를 끕니다.
afterAll(() => server.close());

🚀

3단계: 컴포넌트 기능 추가 (에러 처리) 🔗

에러 테스트를 하려면, 컴포넌트가 에러 났을 때 화면에 뭔가를 보여줘야 합니다.
src/components/UserList.tsx를 수정하여 에러 상태를 처리하도록 만듭니다.
src/components/UserList.tsx
import { useEffect, useState } from 'react';
 
interface User {
  id: number;
  name: string;
}
 
export default function UserList() {
  const [users, setUsers] = useState<User[]>([]);
  const [loading, setLoading] = useState(true);
  // [추가] 에러 메시지를 저장할 상태
  const [error, setError] = useState('');
 
  useEffect(() => {
    fetch('https://jsonplaceholder.typicode.com/users')
      .then((response) => {
        if (!response.ok) {
          throw new Error('서버 에러가 발생했습니다.');
        }
        return response.json();
      })
      .then((data) => {
        setUsers(data);
        setLoading(false);
      })
      .catch((err) => {
        // [추가] 에러 발생 시 상태 업데이트
        setError('에러가 발생했습니다.');
        setLoading(false);
      });
  }, []);
 
  if (loading) return <p>불러오는 중...</p>;
  
  // [추가] 에러 메시지가 있으면 표시
  if (error) return <p>{error}</p>;
 
  return (
    <ul>
      {users.map((user) => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

🚀

4단계: 테스트 작성 (성공 및 에러 검증) 🔗

이제 UserList.test.tsx를 수정합니다.
기존의 global.fetch 코드는 모두 삭제하고,
MSW
를 활용해 성공과 실패 케이스를 모두 테스트합니다.
여기서 중요한 점은,
MSW의 server.use 메서드를 사용하여 특정 테스트에서만 핸들러를 덮어쓸 수 있다
는 것입니다.
src/components/UserList.test.tsx
import { render, screen } from '@testing-library/react';
import { describe, test, expect } from 'vitest';
import { http, HttpResponse } from 'msw'; // MSW 모듈 import
import { server } from '../mocks/server'; // 가짜 서버 import
import UserList from './UserList';
 
describe('UserList 컴포넌트 (MSW 적용)', () => {
 
  test('데이터를 불러오는 동안 로딩 문구가 뜨고, 이후 데이터가 표시된다', async () => {
    render(<UserList />);
 
    console.log('[Test 1] 로딩 상태 확인');
    expect(screen.getByText('불러오는 중...')).toBeInTheDocument();
 
    console.log('[Test 1] MSW 정상 응답 대기');
    // handlers.ts에 정의한 "Leanne Graham (MSW)" 데이터가 오길 기다립니다.
    const userItem = await screen.findByText('Leanne Graham (MSW)');
    
    console.log('[Test 1] 데이터 수신 완료');
    screen.debug();
 
    expect(userItem).toBeInTheDocument();
  });
 
  // [추가] 에러 상황 테스트
  test('서버 에러 발생 시 에러 문구가 표시된다', async () => {
    // 1. 이 테스트 동안만 서버가 500 에러를 뱉도록 설정을 덮어씁니다(Override).
    server.use(
      http.get('https://jsonplaceholder.typicode.com/users', () => {
        return new HttpResponse(null, { status: 500 });
      })
    );
 
    render(<UserList />);
    
    console.log('[Test 2] 로딩 상태 확인');
    expect(screen.getByText('불러오는 중...')).toBeInTheDocument();
 
    console.log('[Test 2] 에러 발생 대기');
    
    // 2. 컴포넌트가 에러를 감지하고 "에러가 발생했습니다."를 띄울 때까지 기다립니다.
    const errorMsg = await screen.findByText('에러가 발생했습니다.');
 
    console.log('[Test 2] 에러 UI 렌더링 확인');
    screen.debug();
 
    expect(errorMsg).toBeInTheDocument();
  });
});

🚀

5단계: 테스트 실행 및 결과 분석 🔗

터미널에서 테스트를 실행합니다.
Terminal
npm test -- src/components/UserList.test.tsx

실행 로그 확인 🔗

실행 로그 확인
실행 로그 확인
[Test 1] 로딩 상태 확인
[Test 1] MSW 정상 응답 대기
[Test 1] 데이터 수신 완료
<body>
  <div>
    <ul>
      <li>Leanne Graham (MSW)</li>
      <li>Ervin Howell (MSW)</li>
    </ul>
  </div>
</body>
 
[Test 2] 로딩 상태 확인
[Test 2] 에러 발생 대기
[Test 2] 에러 UI 렌더링 확인
<body>
  <div>
    <p>에러가 발생했습니다.</p>
  </div>
</body>
 
PASS src/components/UserList.test.tsx
로그를 보면
Test 1
에서는 정상적으로 목록을 가져왔고,
Test 2
에서는 server.use 덕분에 500 에러를 받고 에러 메시지를 보여주었습니다.
우리는 실제 서버를 끄거나 코드를 망가뜨리지 않고도, 에러 상황을 검증해냈습니다.

🚀

결론 🔗

소스 코드는 GitHub - Promleeblog/react-tdd-setup 에서 확인할 수 있습니다.
Terminal
git clone https://github.com/PROMLEE/my-tdd-app.git
cd my-tdd-app
git checkout part9
오늘은 MSW를 도입하여 API 테스트 전략을 업그레이드했습니다.

이제 데이터를 가져오는(GET) 테스트는 마스터했습니다.
하지만 사용자가 데이터를 입력하고 서버로 보내는(POST) 폼 처리도 중요합니다.
다음 시간에는
10편. Form 입력과 Validation 테스트
를 통해, 사용자가 텍스트를 입력하고 전송 버튼을 누르는 과정을 테스트하는 방법을 알아보겠습니다.

참고 🔗