프론트엔드 통합 테스트를 작성할 때 실제 백엔드 API 서버에 직접 연결해서 테스트하다가 서버 다운이나 네트워크 지연 때문에 테스트가 끊임없이 실패하는 상황을 경험해봤을 것이다.
정작 작성한 프론트엔드 UI 로직에는 아무런 문제가 없는데도 백엔드 데이터 변경 하나에 테스트 전체가 무너지는 억울한 상황이 자주 발생한다.
다만 대부분의 개발자들은 jest.fn()이나 fetch 함수를 직접 감싸서 모킹하려다가, 구현 세부사항과 테스트 코드가 강하게 결합되는 늪에 빠지곤 한다.
이번에는 네트워크 레이어에서 요청을 가로채는 MSW(Mock Service Worker)가 정확히 무엇인지, 왜 필요한지, 그리고 최신 MSW v2 버전으로 테스트 환경을 어떻게 구현하는지 완벽하게 정리해서 소개하겠다.
MSW는 브라우저와 Node.js 환경에서 실제 네트워크 요청을 브라우저의 Service Worker API를 활용해 네트워크 레이어 레벨에서 가로채는 가상 모킹 라이브러리다.
기존의 fetch나 axios 객체를 직접 덮어쓰는(Monkey Patching) 방식과 달리, 실제 네트워크 요청을 보낸 뒤 중간에서 이를 하이재킹하여 목 데이터(Mock Data)를 반환한다.
덕분에 애플리케이션 코드 내부의 API 호출 방식을 단 한 줄도 수정하지 않고도 실제 운영 환경과 거의 동일한 상태로 테스트할 수 있다.
Node.js 환경인 Jest나 Vitest에서 실행할 때는 Service Worker 대신 내부적으로 `node-request-interceptor`를 사용하여 네트워크 단을 동일하게 모킹한다.
| 비교 항목 | 기존 방식 (jest.mock / fetch-mock) | MSW (Mock Service Worker) |
|---|---|---|
| 작동 위치 | 애플리케이션 런타임 코드 내부 오버라이드 | 네트워크 레이어 (Service Worker / Node Interceptor) |
| 개발자 도구 확인 | 네트워크 탭에 요청이 남지 않음 | 실제 네트워크 요청 및 응답 확인 가능 (브라우저) |
| 코드 결합도 | 테스트 도구 및 모킹 방식에 의존성 높음 | 애플리케이션 코드와 완벽히 분리됨 |
| 지원 환경 | 단위 테스트(Node.js) 위주 | 브라우저(개발 서버) 및 Node.js(테스트) 공통 사용 |
MSW v2는 기존 v1에 비해 API 구문이 더 명확해졌다. `rest` 객체 대신 `http` 및 `HttpResponse` 클래스를 사용하여 표준 Web API 스펙을 충실히 따른다.
먼저 프로젝트에 msw 패키지를 개발 의존성으로 설치한다.
npm install msw@latest --save-dev설치 후 모킹할 API 핸들러(Handler)를 작성해야 한다. 프로젝트 root 경로 근처에 `src/mocks/handlers.js` 파일을 작성한다.
import { http, HttpResponse } from 'msw';
export const handlers = [
// GET 요청 가로채기
http.get('/api/users/:id', ({ params }) => {
const { id } = params;
return HttpResponse.json({
id: Number(id),
name: '홍길동',
email: 'user@redinfo.co.kr',
});
}),
];
이제 React Testing Library(RTL) 환경에서 MSW를 활용해 컴포넌트 통합 테스트를 작성해본다.
기존의 잘못된 모킹 방식과 MSW를 사용한 올바른 방식의 차이를 비교해보자.
✗ 잘못된 코드: global.fetch 직접 오버라이딩 (구현 세부사항 결합 및 유연성 저하)
// ✗ 잘못된 방식: fetch 함수를 직접 덮어씌워 내부 로직과 강하게 결합됨
test('사용자 프로필을 가져와 화면에 표시한다', async () => {
global.fetch = jest.fn().mockImplementation(() =>
Promise.resolve({
ok: true,
json: () => Promise.resolve({ id: 1, name: '홍길동', email: 'user@redinfo.co.kr' }),
})
);
render(<UserProfile userId={1} />);
expect(await screen.findByText('홍길동')).toBeInTheDocument();
});위 방식은 애플리케이션 코드가 fetch 대신 axios나 React Query 등으로 내부 라이브러리를 변경할 경우 테스트가 즉시 파괴되는 약점이 있다.
✓ 올바른 코드: MSW Node 서버를 가동하여 네트워크 통합 테스트 수행
// ✓ 올바른 방식: MSW setupServer를 구축하여 표준 테스트 환경 생성
import { render, screen } from '@testing-library/react';
import { http, HttpResponse } from 'msw';
import { setupServer } from 'msw/node';
import UserProfile from './UserProfile';
const server = setupServer(
http.get('/api/users/:id', () => {
return HttpResponse.json({ id: 1, name: '홍길동', email: 'user@redinfo.co.kr' });
})
);
// 테스트 라이프사이클에 맞춰 MSW 서버 구동
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
test('MSW를 통해 사용자 프로필 데이터를 안전하게 비동기 렌더링한다', async () => {
render(<UserProfile userId={1} />);
// 화면에 렌더링될 때까지 비동기 대기
expect(await screen.findByText('홍길동')).toBeInTheDocument();
expect(await screen.findByText('user@redinfo.co.kr')).toBeInTheDocument();
});실행 결과, 애플리케이션 코드가 어떤 HTTP 클라이언트 라이브러리를 사용하는지와 무관하게 네트워크 응답을 가로채서 컴포넌트에 전달하므로 완벽히 독립된 테스트가 실행된다.
MSW를 도입할 때 흔히 침착함을 잃고 범하는 라이프사이클 관리 및 핸들러 오버라이딩 미숙 문제를 정리했다.
✗ 잘못된 것: 각 테스트에서 오버라이드한 핸들러가 다음 테스트로 오염되는 경우
특정 테스트에서 500 에러 상태를 검증하기 위해 `server.use()`로 핸들러를 일회성 변경한 후 리셋하지 않으면, 이후 실행되는 다른 정상 테스트까지 실패하게 된다.
✓ 올바른 것: afterEach에서 resetHandlers() 호출 명시
모든 테스트가 끝난 시점마다 `server.resetHandlers()`를 잊지 말고 호출해야 테스트 간 상호 간섭(Side Effect)이 사라진다.
// 특정 단일 테스트에서만 500 서버 에러 상황 모킹하기 test('서버 에러 발생 시 에러 메시지를 표시한다', async () => { // 현재 테스트에 한해 핸들러 덮어쓰기 server.use( http.get('/api/users/:id', () => { return new HttpResponse(null, { status: 500 }); }) ); render(<UserProfile userId={1} />); expect(await screen.findByText('프로필을 불러오지 못했습니다.')).toBeInTheDocument(); }); // afterEach(() => server.resetHandlers()) 덕분에 다음 테스트는 다시 기본 핸들러로 복구됨
5. 마무리: 안정적인 테스트 환경 구축하기MSW(Mock Service Worker)는 네트워크 종속성을 제거하고 진짜 유저 환경에 가까운 프론트엔드 테스트를 만드는 강력한 도구다.
네트워크 요청 모킹 레이어를 실제 컴포넌트 코드와 분리하는 작은 습관이 모여서 개발 생산성과 테스트 신뢰도를 극대화한다는 점을 잊지 말자.
이 글의 3단계 예제와 핸들러 라이프사이클 설정을 참고해 프로젝트에 MSW를 도입하면, 서버 상태에 전혀 영향을 받지 않는 견고한 테스트 환경을 얻을 수 있을 것이다.