MSW (Mock Service Worker) 완벽 가이드

# MSW (Mock Service Worker) 완벽 가이드

## 목차
1. [MSW란?](#msw란)
2. [왜 MSW를 사용하나?](#왜-msw를-사용하나)
3. [설치](#설치)
4. [기본 설정](#기본-설정)
5. [핸들러 작성](#핸들러-작성)
6. [고급 사용법](#고급-사용법)
7. [베스트 프랙티스](#베스트-프랙티스)

## MSW란?

MSW(Mock Service Worker)는 Service Worker API를 활용하여 네트워크 레벨에서 HTTP 요청을 가로채고 모킹하는 라이브러리입니다.

### 핵심 특징

– **네트워크 레벨 인터셉션**: 애플리케이션 코드 수정 없이 요청 가로채기
– **브라우저와 Node.js 모두 지원**: 개발과 테스트 환경 모두에서 사용 가능
– **실제 네트워크 동작**: DevTools 네트워크 탭에서 요청 확인 가능
– **타입 안정성**: TypeScript 완벽 지원

## 왜 MSW를 사용하나?

### 기존 모킹 방식의 문제점

“`javascript
// ❌ 기존 방식: axios를 직접 모킹
jest.mock(‘axios’);
axios.get.mockResolvedValue({ data: mockData });
“`

이 방식은 HTTP 클라이언트 라이브러리에 의존적이며, 실제 네트워크 동작과 다릅니다.

### MSW의 장점

“`javascript
// ✅ MSW 방식: 네트워크 레벨에서 모킹
http.get(‘/api/users’, () => {
return HttpResponse.json(mockData)
})
“`

– 실제 네트워크 요청처럼 동작
– HTTP 클라이언트 라이브러리 독립적
– 개발/테스트 환경에서 동일한 핸들러 재사용

## 설치

“`bash
npm install msw –save-dev
# 또는
yarn add msw -D
# 또는
pnpm add -D msw
“`

### Service Worker 파일 생성

“`bash
npx msw init public/ –save
“`

이 명령어는 `public/mockServiceWorker.js` 파일을 생성합니다.

## 기본 설정

### 1. 핸들러 정의

“`typescript
// 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. 브라우저 설정

“`typescript
// src/mocks/browser.ts
import { setupWorker } from ‘msw/browser’
import { handlers } from ‘./handlers’

export const worker = setupWorker(…handlers)
“`

### 3. 애플리케이션에 통합 (React 예제)

“`typescript
// 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(



)
})
“`

### 4. 테스트 환경 설정

“`typescript
// src/mocks/server.ts
import { setupServer } from ‘msw/node’
import { handlers } from ‘./handlers’

export const server = setupServer(…handlers)
“`

“`typescript
// src/setupTests.ts
import { server } from ‘./mocks/server’

beforeAll(() => server.listen())
afterEach(() => server.resetHandlers())
afterAll(() => server.close())
“`

## 핸들러 작성

### HTTP 메서드

“`typescript
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 })
}),
]
“`

### 요청 정보 접근

“`typescript
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 })
})
“`

### 응답 커스터마이징

“`typescript
// 상태 코드와 헤더 설정
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. 조건부 응답

“`typescript
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. 순차적 응답 (한 번만 사용)

“`typescript
http.get(
‘/api/poll’,
() => {
return HttpResponse.json({ status: ‘pending’ })
},
{ once: true } // 한 번만 사용
)

// 두 번째 요청부터는 다른 응답
http.get(‘/api/poll’, () => {
return HttpResponse.json({ status: ‘completed’ })
})
“`

### 3. 런타임에 핸들러 변경

“`typescript
// 테스트에서 특정 시나리오를 위한 핸들러 추가
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 지원

“`typescript
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. 핸들러 구조화

“`typescript
// 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 데이터 분리

“`typescript
// 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. 환경별 설정

“`typescript
// .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. 타입 안정성 확보

“`typescript
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(
‘/api/login’,
async ({ request }) => {
const { email, password } = await request.json()

return HttpResponse.json({
token: ‘mock-token’,
user: { id: 1, name: ‘User’, email }
})
}
)
“`

### 5. 실제 API와 매칭

“`typescript
// 백엔드 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. 디버깅 팁

“`typescript
// 요청 로깅
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 등록 실패

“`typescript
// public 폴더 확인
// mockServiceWorker.js 파일이 있는지 확인

// worker.start() 옵션 조정
worker.start({
serviceWorker: {
url: ‘/mockServiceWorker.js’,
},
})
“`

### CORS 에러

“`typescript
http.get(‘/api/data’, () => {
return HttpResponse.json(
{ data: ‘value’ },
{
headers: {
‘Access-Control-Allow-Origin’: ‘*’,
‘Access-Control-Allow-Methods’: ‘GET, POST, PUT, DELETE’,
}
}
)
})
“`

### 특정 요청만 모킹하고 나머지는 실제 서버로

“`typescript
// 모킹하지 않을 요청은 핸들러를 작성하지 않으면 됨
// onUnhandledRequest 옵션으로 제어 가능

worker.start({
onUnhandledRequest: ‘bypass’, // 처리되지 않은 요청은 실제 서버로
})
“`

## 참고 자료

– [MSW 공식 문서](https://mswjs.io/)
– [MSW GitHub](https://github.com/mswjs/msw)
– [Service Worker API](https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API)

## 결론

MSW는 프론트엔드 개발과 테스트를 위한 강력한 모킹 도구입니다. 네트워크 레벨에서 동작하기 때문에 실제 프로덕션 환경과 유사한 개발 경험을 제공하며, 백엔드 의존성 없이 독립적인 개발이 가능합니다.

댓글 남기기