MSW (Mock Service Worker) 완벽 가이드
목차
MSW란?
MSW(Mock Service Worker)는 Service Worker API를 활용하여 네트워크 레벨에서 HTTP 요청을 가로채고 모킹하는 라이브러리입니다.
핵심 특징
- 네트워크 레벨 인터셉션: 애플리케이션 코드 수정 없이 요청 가로채기
- 브라우저와 Node.js 모두 지원: 개발과 테스트 환경 모두에서 사용 가능
- 실제 네트워크 동작: DevTools 네트워크 탭에서 요청 확인 가능
- 타입 안정성: TypeScript 완벽 지원
왜 MSW를 사용하나?
기존 모킹 방식의 문제점
// ❌ 기존 방식: axios를 직접 모킹
jest.mock('axios');
axios.get.mockResolvedValue({ data: mockData });
이 방식은 HTTP 클라이언트 라이브러리에 의존적이며, 실제 네트워크 동작과 다릅니다.
MSW의 장점
// ✅ MSW 방식: 네트워크 레벨에서 모킹
http.get('/api/users', () => {
return HttpResponse.json(mockData)
})
- 실제 네트워크 요청처럼 동작
- HTTP 클라이언트 라이브러리 독립적
- 개발/테스트 환경에서 동일한 핸들러 재사용
설치
npm install msw --save-dev
# 또는
yarn add msw -D
# 또는
pnpm add -D msw
Service Worker 파일 생성
npx msw init public/ --save
이 명령어는 public/mockServiceWorker.js
파일을 생성합니다.
기본 설정
1. 핸들러 정의
// src/mocks/handlers.ts
import { http, HttpResponse } from 'msw'
export const handlers = [
// GET 요청
http.get('/api/users', () => {
return HttpResponse.json([
{ id: 1, name: '홍길동', email: 'hong@example.com' },
{ id: 2, name: '김철수', email: 'kim@example.com' },
])
}),
// POST 요청
http.post('/api/login', async ({ request }) => {
const { email, password } = await request.json()
if (email === 'test@example.com' && password === 'password') {
return HttpResponse.json({
token: 'mock-jwt-token',
user: { id: 1, email }
})
}
return new HttpResponse(null, { status: 401 })
}),
// 동적 라우팅
http.get('/api/users/:id', ({ params }) => {
const { id } = params
return HttpResponse.json({
id: Number(id),
name: `사용자 ${id}`,
email: `user${id}@example.com`
})
}),
]
2. 브라우저 설정
// src/mocks/browser.ts
import { setupWorker } from 'msw/browser'
import { handlers } from './handlers'
export const worker = setupWorker(...handlers)
3. 애플리케이션에 통합 (React 예제)
// src/main.tsx
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
async function enableMocking() {
if (import.meta.env.MODE !== 'development') {
return
}
const { worker } = await import('./mocks/browser')
return worker.start({
onUnhandledRequest: 'warn', // 처리되지 않은 요청 경고
})
}
enableMocking().then(() => {
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>
)
})
4. 테스트 환경 설정
// src/mocks/server.ts
import { setupServer } from 'msw/node'
import { handlers } from './handlers'
export const server = setupServer(...handlers)
// src/setupTests.ts
import { server } from './mocks/server'
beforeAll(() => server.listen())
afterEach(() => server.resetHandlers())
afterAll(() => server.close())
핸들러 작성
HTTP 메서드
import { http, HttpResponse } from 'msw'
const handlers = [
// GET
http.get('/api/resource', () => {
return HttpResponse.json({ data: 'value' })
}),
// POST
http.post('/api/resource', async ({ request }) => {
const body = await request.json()
return HttpResponse.json({ created: true })
}),
// PUT
http.put('/api/resource/:id', async ({ params, request }) => {
const { id } = params
const body = await request.json()
return HttpResponse.json({ updated: true, id })
}),
// DELETE
http.delete('/api/resource/:id', ({ params }) => {
return new HttpResponse(null, { status: 204 })
}),
// PATCH
http.patch('/api/resource/:id', () => {
return HttpResponse.json({ patched: true })
}),
]
요청 정보 접근
http.post('/api/data', async ({ request, params, cookies }) => {
// 1. Request body
const jsonBody = await request.json()
const textBody = await request.text()
const formData = await request.formData()
// 2. Headers
const authHeader = request.headers.get('Authorization')
// 3. URL params
const { id } = params
// 4. Query parameters
const url = new URL(request.url)
const search = url.searchParams.get('q')
// 5. Cookies
const sessionId = cookies.sessionId
return HttpResponse.json({ success: true })
})
응답 커스터마이징
// 상태 코드와 헤더 설정
http.get('/api/data', () => {
return HttpResponse.json(
{ message: 'Created' },
{
status: 201,
headers: {
'Content-Type': 'application/json',
'X-Custom-Header': 'value',
}
}
)
})
// 지연 시뮬레이션
import { delay } from 'msw'
http.get('/api/slow', async () => {
await delay(2000) // 2초 지연
return HttpResponse.json({ data: 'slow response' })
})
// 네트워크 에러
http.get('/api/error', () => {
return HttpResponse.error()
})
고급 사용법
1. 조건부 응답
http.get('/api/users', ({ request }) => {
const url = new URL(request.url)
const role = url.searchParams.get('role')
if (role === 'admin') {
return HttpResponse.json([
{ id: 1, name: 'Admin User', role: 'admin' }
])
}
return HttpResponse.json([
{ id: 2, name: 'Regular User', role: 'user' }
])
})
2. 순차적 응답 (한 번만 사용)
http.get(
'/api/poll',
() => {
return HttpResponse.json({ status: 'pending' })
},
{ once: true } // 한 번만 사용
)
// 두 번째 요청부터는 다른 응답
http.get('/api/poll', () => {
return HttpResponse.json({ status: 'completed' })
})
3. 런타임에 핸들러 변경
// 테스트에서 특정 시나리오를 위한 핸들러 추가
import { server } from './mocks/server'
import { http, HttpResponse } from 'msw'
test('에러 상황 테스트', async () => {
server.use(
http.get('/api/users', () => {
return new HttpResponse(null, { status: 500 })
})
)
// 테스트 로직...
})
4. GraphQL 지원
import { graphql, HttpResponse } from 'msw'
const handlers = [
graphql.query('GetUser', ({ query, variables }) => {
return HttpResponse.json({
data: {
user: {
id: variables.id,
name: 'John Doe',
}
}
})
}),
graphql.mutation('CreateUser', ({ variables }) => {
return HttpResponse.json({
data: {
createUser: {
id: '1',
name: variables.name,
}
}
})
}),
]
베스트 프랙티스
1. 핸들러 구조화
// src/mocks/handlers/users.ts
export const userHandlers = [
http.get('/api/users', getUsersHandler),
http.post('/api/users', createUserHandler),
]
// src/mocks/handlers/posts.ts
export const postHandlers = [
http.get('/api/posts', getPostsHandler),
]
// src/mocks/handlers/index.ts
export const handlers = [
...userHandlers,
...postHandlers,
]
2. Mock 데이터 분리
// src/mocks/data/users.ts
export const mockUsers = [
{ id: 1, name: '홍길동', email: 'hong@example.com' },
{ id: 2, name: '김철수', email: 'kim@example.com' },
]
// src/mocks/handlers/users.ts
import { mockUsers } from '../data/users'
export const userHandlers = [
http.get('/api/users', () => {
return HttpResponse.json(mockUsers)
}),
]
3. 환경별 설정
// .env.development
VITE_ENABLE_MSW=true
// .env.production
VITE_ENABLE_MSW=false
// src/main.tsx
async function enableMocking() {
if (import.meta.env.VITE_ENABLE_MSW !== 'true') {
return
}
const { worker } = await import('./mocks/browser')
return worker.start()
}
4. 타입 안정성 확보
import { http, HttpResponse } from 'msw'
interface User {
id: number
name: string
email: string
}
interface LoginRequest {
email: string
password: string
}
interface LoginResponse {
token: string
user: User
}
http.post<never, LoginRequest, LoginResponse>(
'/api/login',
async ({ request }) => {
const { email, password } = await request.json()
return HttpResponse.json({
token: 'mock-token',
user: { id: 1, name: 'User', email }
})
}
)
5. 실제 API와 매칭
// 백엔드 API 스펙과 동일하게 유지
// OpenAPI/Swagger 문서를 참고하여 핸들러 작성
http.get('/api/v1/users', ({ request }) => {
const url = new URL(request.url)
const page = url.searchParams.get('page') || '1'
const limit = url.searchParams.get('limit') || '10'
return HttpResponse.json({
data: mockUsers.slice(0, Number(limit)),
meta: {
page: Number(page),
limit: Number(limit),
total: mockUsers.length,
}
})
})
6. 디버깅 팁
// 요청 로깅
http.get('/api/users', ({ request }) => {
console.log('MSW intercepted:', request.method, request.url)
return HttpResponse.json(mockUsers)
})
// 조건부 로깅
worker.start({
onUnhandledRequest: (req) => {
console.warn('Unhandled request:', req.method, req.url)
},
})
문제 해결
Service Worker 등록 실패
// public 폴더 확인
// mockServiceWorker.js 파일이 있는지 확인
// worker.start() 옵션 조정
worker.start({
serviceWorker: {
url: '/mockServiceWorker.js',
},
})
CORS 에러
http.get('/api/data', () => {
return HttpResponse.json(
{ data: 'value' },
{
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE',
}
}
)
})
특정 요청만 모킹하고 나머지는 실제 서버로
// 모킹하지 않을 요청은 핸들러를 작성하지 않으면 됨
// onUnhandledRequest 옵션으로 제어 가능
worker.start({
onUnhandledRequest: 'bypass', // 처리되지 않은 요청은 실제 서버로
})
참고 자료
결론
MSW는 프론트엔드 개발과 테스트를 위한 강력한 모킹 도구입니다. 네트워크 레벨에서 동작하기 때문에 실제 프로덕션 환경과 유사한 개발 경험을 제공하며, 백엔드 의존성 없이 독립적인 개발이 가능합니다.