Mock Service Worker를 이용한 테스트에서의 원활한 API 모킹
Min-jun Kim
Dev Intern · Leapcell

소개
현대 웹 개발 환경에서 애플리케이션은 데이터를 가져오고, 작업을 수행하며, 동적인 사용자 경험을 제공하기 위해 외부 API에 빈번하게 의존합니다.
이러한 상호 연결성은 강력하지만, 테스트 측면에서 상당한 어려움을 야기합니다. 테스트 실행 중에 느리거나, 불안정하거나, 심지어 사용 불가능할 수 있는 API에 의존하는 컴포넌트를 어떻게 테스트할 수 있을까요? API의 데이터가 변경될 수 있을 때 일관된 테스트 결과를 어떻게 보장할 수 있을까요? 전통적으로 개발자들은 전용 테스트 서버, 투박한 스텁 라이브러리, 또는 fetch
나 XMLHttpRequest
를 직접 모킹하는 복잡한 설정을 할 수 있습니다. 이는 종종 취약한 테스트와 높은 유지 보수 부담으로 이어집니다.
이때 Mock Service Worker(MSW)와 같은 강력한 도구가 등장합니다. MSW는 네트워크 수준에서 직접 API 요청을 가로채고 모킹하는 우아하고 강력한 솔루션을 제공하며, 테스트에 대한 탁월한 제어와 안정성을 제공합니다.
애플리케이션 코드를 건드리지 않고 실제 API 동작을 시뮬레이션하는 MSW의 능력은 탄력적이고 효율적인 테스트 스위트를 구축하는 데 필수적인 자산이 됩니다. 이 글에서는 MSW에 대해 자세히 알아보고, MSW의 핵심 원칙, 작동 방식, 그리고 JavaScript 테스트 전략을 강화하기 위해 MSW를 어떻게 활용할 수 있는지 살펴보겠습니다.
핵심 개념 이해하기
실제 구현에 들어가기 전에 관련 주요 용어에 대한 공통된 이해를 확립해 봅시다.
- API (Application Programming Interface): 서로 다른 소프트웨어 애플리케이션이 서로 통신할 수 있도록 하는 정의된 규칙 집합입니다. 웹 개발에서는 종종 데이터 교환을 위한 HTTP 기반 통신을 의미합니다.
- 모킹 (Mocking): 테스트에서 모킹은 코드가 상호 작용하는 종속성(API 등)의 시뮬레이션된 버전을 만드는 것을 포함합니다. 목표는 테스트 대상 단위를 외부 종속성으로부터 격리하여 테스트가 단위의 논리에만 초 집중하도록 하는 것입니다.
- 서비스 워커 (Service Worker): 브라우저가 메인 실행 스레드와 분리된 백그라운드에서 실행하는 JavaScript 파일입니다. 서비스 워커는 네트워크 요청을 가로채고, 리소스를 캐싱하고, 푸시 알림 등을 처리할 수 있습니다. MSW는 이 브라우저 기능을 모킹 기능에 영리하게 사용합니다.
- 네트워크 요청 가로채기 (Network Request Interception): 네트워크 요청(예: HTTP
GET
,POST
,PUT
,DELETE
)이 원래 목적지에 도달하기 전에 이를 캡처하고 조작하는 능력입니다. MSW는 서비스 워커를 통해 이를 달성합니다. - 단위 테스트 (Unit Testing): 애플리케이션의 개별 컴포넌트 또는 함수를 격리하여 테스트하는 것입니다.
- 통합 테스트 (Integration Testing): 애플리케이션의 서로 다른 부분이 통합된 단위로 어떻게 작동하는지 테스트하는 것으로, 종종 모의 또는 실제 API와의 상호 작용을 포함합니다.
MSW 작동 방식: 네트워크 수준에서의 가로채기
MSW의 천재성은 서비스 워커 API 활용에 있습니다. fetch
또는 XMLHttpRequest
와 같은 전역 객체를 패치하는 전통적인 모킹 라이브러리와 달리(이는 경쟁 조건 및 프레임워크별 문제에 취약할 수 있음), MSW는 네트워크 수준에서 작동합니다.
MSW를 설정하면:
- 서비스 워커 등록: MSW는 브라우저(또는 Node.js 환경)에 서비스 워커를 등록합니다.
- 요청 가로채기: 이 서비스 워커는 애플리케이션에서 발생하는 모든 나가는 네트워크 요청에 대한 프록시 역할을 합니다.
- 핸들러 일치: MSW가 가로채야 할 URL과 HTTP 메서드를 지정하는 '요청 핸들러'를 정의합니다. 요청이 정의된 핸들러와 일치하면 MSW가 이를 가로챕니다.
- 모의 응답: MSW는 실제 API로 요청이 진행되도록 하는 대신, 핸들러 정의에 따라 정교하게 구성된 모의 응답을 반환합니다. 이 응답에는 사용자 정의 상태 코드, 헤더 및 JSON 본문이 포함될 수 있습니다.
- 투명한 작동: 애플리케이션의 관점에서 볼 때, 실제 API와 통신하는 것과 같습니다. 애플리케이션 코드는 요청이 가로채지고 모킹되고 있음을 알 필요가 없습니다.
이 접근 방식은 몇 가지 중요한 이점을 제공합니다:
- 진정한 격리: 테스트는 외부 API 가용성 및 데이터 변동으로부터 독립됩니다.
- 프레임워크 비호환성: MSW는 네트워크 계층에서 작동하고 애플리케이션 계층에서 작동하지 않기 때문에, 모든 HTTP 클라이언트 라이브러리(
fetch
,axios
,XMLHttpRequest
) 및 모든 JavaScript 프레임워크(React, Vue, Angular 등)와 함께 작동합니다. - 현실적인 동작: 네트워크 오류, 지연 및 복잡한 데이터 구조를 시뮬레이션하여 테스트를 더욱 강력하게 만들 수 있습니다.
- 테스트 불안정성 감소: 일관된 모의 응답은 안정적이고 재현 가능한 테스트 결과를 제공합니다.
테스트에서의 실제 구현
API에서 데이터를 가져오는 간단한 React 컴포넌트를 사용하는 일반적인 테스트 시나리오에서 MSW를 사용하는 방법을 설명해 보겠습니다. 테스트 환경으로 Jest와 React Testing Library를 사용합니다.
1. 설치
먼저 MSW와 필요한 테스트 라이브러리를 설치합니다.
npm install --save-dev jest @testing-library/react @testing-library/jest-dom @mswjs/http-middleware msw # 또는 yarn add -D jest @testing-library/react @testing-library/jest-dom @mswjs/http-middleware msw
2. 요청 핸들러 정의
모의 API 응답을 정의하기 위해 src/mocks/handlers.js
와 같은 파일을 만듭니다.
// src/mocks/handlers.js import { http, HttpResponse } from 'msw'; export const handlers = [ // /users에 대한 GET 요청 모킹 http.get('https://api.example.com/users', () => { return HttpResponse.json([ { id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }, ], { status: 200 }); // 200 OK 응답 시뮬레이션 }), // /posts에 대한 POST 요청 모킹 http.post('https://api.example.com/posts', async ({ request }) => { const newPost = await request.json(); console.log('Received new post:', newPost); // 요청 본문을 검사할 수 있음 return HttpResponse.json({ id: 99, ...newPost }, { status: 201 }); // 201 Created 응답 시뮬레이션 }), // 경로 매개변수와 함께 GET 요청 모킹 http.get('https://api.example.com/users/:id', ({ params }) => { const { id } = params; if (id === '1') { return HttpResponse.json({ id: 1, name: 'Alice' }, { status: 200 }); } return HttpResponse.json({}, { status: 404 }); // 404 Not Found 시뮬레이션 }), ];
3. 테스트를 위한 MSW 설정
Jest와 같은 Node.js 환경을 위해 MSW를 초기화하는 설정 파일(예: src/mocks/server.js
)을 만듭니다.
// src/mocks/server.js import { setupServer } from 'msw/node'; import { handlers } from './handlers'; // 주어진 요청 핸들러로 요청 모킹 서버 설정 export const server = setupServer(...handlers);
그런 다음 Jest가 이 설정을 사용하도록 구성합니다.
// src/setupTests.js (또는 테스트 환경을 구성하는 곳) import '@testing-library/jest-dom'; import { server } from './mocks/server.js'; // 모든 테스트 전에 API 모킹 설정 beforeAll(() => server.listen()); // 테스트의 일부로 선언된 모든 요청 핸들러를 재설정합니다 (일회성 요청의 경우). // 이는 테스트 간에 깨끗한 테스트 상태를 유지합니다. afterEach(() => server.resetHandlers()); // 테스트가 끝난 후 정리합니다. afterAll(() => server.close());
Jest 구성에 src/setupTests.js
가 포함되어 있는지 확인하세요 (예: package.json
또는 jest.config.js
에서).
// package.json { "jest": { "setupFilesAfterEnv": ["<rootDir>/src/setupTests.js"] } }
4. 테스트할 컴포넌트 생성
사용자 목록을 가져오는 UserList.js
컴포넌트가 있다고 가정해 보겠습니다.
// src/components/UserList.jsx import React, { useState, useEffect } from 'react'; function UserList() { const [users, setUsers] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { fetch('https://api.example.com/users') .then(response => { if (!response.ok) { throw new Error('Network response was not ok'); } return response.json(); }) .then(data => { setUsers(data); setLoading(false); }) .catch(err => { setError(err); setLoading(false); }); }, []); if (loading) { return <div>Loading users...</div>; } if (error) { return <div>Error: {error.message}</div>; } return ( <div> <h1>User List</h1> <ul> {users.map(user => ( <li key={user.id}>{user.name}</li> ))} </ul> </div> ); } export default UserList;
5. 테스트 작성
이제 React Testing Library를 사용하여 UserList.js
에 대한 테스트를 작성합니다.
// src/components/UserList.test.jsx import React from 'react'; import { render, screen, waitFor } from '@testing-library/react'; import '@testing-library/jest-dom'; import UserList from './UserList'; import { server } from '../mocks/server'; import { http, HttpResponse } from 'msw'; describe('UserList component', () => { it('API에서 가져온 사용자 이름을 표시합니다', async () => { render(<UserList />); expect(screen.getByText(/loading users/i)).toBeInTheDocument(); await waitFor(() => { expect(screen.getByText('Alice')).toBeInTheDocument(); }); expect(screen.getByText('Bob')).toBeInTheDocument(); expect(screen.queryByText(/loading users/i)).not.toBeInTheDocument(); }); it('API 호출 실패 시 오류 메시지를 표시합니다', async () => { // 이 특정 테스트 케이스에 대해 기본 핸들러를 재정의합니다. server.use( http.get('https://api.example.com/users', () => { return HttpResponse.json({ message: 'Internal Server Error' }, { status: 500 }); }) ); render(<UserList />); await waitFor(() => { expect(screen.getByText(/error: network response was not ok/i)).toBeInTheDocument(); }); expect(screen.queryByText('Alice')).not.toBeInTheDocument(); }); it('ID별로 단일 사용자를 가져올 때 표시합니다', async () => { // 경로 매개변수와 함께 모킹을 설명하기 위해; // 이 예제는 단일 사용자를 가져오는 컴포넌트를 가정합니다. // UserList의 경우, 일반적으로 단일 사용자 보기에 대한 별도의 컴포넌트가 있을 것입니다. server.use( http.get('https://api.example.com/users/1', () => { return HttpResponse.json({ id: 1, name: 'Alice Smith' }, { status: 200 }); }) ); // UserList가 특정 사용자를 가져오도록 하는 props를 받을 수 있었다면: // render(<UserList userId={1} />); // await waitFor(() => { // expect(screen.getByText('Alice Smith')).toBeInTheDocument(); // }); // 이 UserList의 경우, 다른 ID에 대한 부정적인 경로를 테스트할 것입니다. server.use( http.get('https://api.example.com/users/99', () => { return HttpResponse.json({}, { status: 404 }); }) ); // 99번 사용자를 가져오는 컴포넌트가 있다면 404 동작이 표시될 것입니다. }); });
server.use()
를 사용하면 특정 테스트 케이스에 대해 특정 핸들러를 재정의하여 애플리케이션 코드나 전역 모크를 수정하지 않고 다양한 API 응답(성공, 오류, 빈 데이터)을 테스트할 수 있습니다. afterEach
의 resetHandlers()
는 이러한 재정의가 후속 테스트로 유출되지 않도록 보장합니다.
애플리케이션 시나리오
MSW의 다재다능함은 다양한 테스트 시나리오에 적합합니다.
- 단위 및 통합 테스트: 보여준 것처럼, API와 상호 작용하는 UI 컴포넌트를 테스트하여 다양한 데이터 상태에 대해 올바르게 렌더링하는지 확인하는 데 완벽합니다.
- Storybook 컴포넌트 개발: MSW를 Storybook과 통합하여 컴포넌트에 현실적인 정적 데이터를 제공함으로써, 디자이너와 개발자가 라이브 백엔드 없이 다양한 API 상태에서 컴포넌트와 상호 작용할 수 있습니다.
- 엔드투엔드(E2E) 테스트 (Cypress, Playwright, Selenium): E2E 테스트는 종종 실제 백엔드에 접근하지만, MSW는 초기 개발 중이거나 문제가 있는 외부 서비스의 경우, 특정 E2E 시나리오에 대한 일관된 데이터를 보장하거나 기능 프로토타이핑을 신속하게 수행하는 데 강력한 도구가 될 수 있습니다.
- 핫 모듈 리로드를 포함한 로컬 개발: MSW는 Vite 또는 Webpack Dev Server와 같은 도구를 사용하여 브라우저에서 로컬 개발 중에도 사용할 수 있습니다. 이를 통해 개발자는 백엔드 API가 아직 개발 중이거나 사용할 수 없을 때 프론트엔드 기능을 작업할 수 있으며, 일관된 모의 데이터를 제공합니다.
결론
Mock Service Worker는 JavaScript 애플리케이션에서 API 모킹에 접근하는 방식을 근본적으로 변화시킵니다. 네트워크 수준에서 작동함으로써 전통적인 모킹 기법의 일반적인 함정을 제거하고, API 상호 작용을 시뮬레이션하는 강력하고 프레임워크 비호환이며 놀라울 정도로 투명한 방법을 제공합니다. 이는 더 안정적이고 유지 보수하기 쉬우며 효율적인 테스트로 이어져, 궁극적으로 개발자가 더 높은 품질의 애플리케이션을 더 큰 자신감으로 구축할 수 있도록 지원합니다. MSW는 진정으로 프론트엔드 테스트를 백엔드로부터 분리하여, 테스트 스위트를 회귀 및 예상치 못한 동작에 대한 신뢰할 수 있는 보호 장치로 만듭니다.