MSW (Mock Service Worker) 완벽 가이드

MSW (Mock Service Worker) 완벽 가이드

목차

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

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

댓글 남기기