- 소개
- 시작하기
- 철학
- 비교
- 제한 사항
- 디버깅 실행 매뉴얼
- FAQ
- Basics
- Concepts
- Network behavior
- Integrations
- API
- CLI
- Best practices
- Recipes
- Cookies
- Query parameters
- Response patching
- Polling
- Streaming
- Network errors
- File uploads
- Responding with binary
- Custom worker script location
- Global response delay
- GraphQL query batching
- Higher-order resolver
- Keeping mocks in sync
- Merging Service Workers
- Mock GraphQL schema
- Using CDN
- Using custom "homepage" property
- Using local HTTPS
1.x → 2.x
버전 2.0 마이그레이션 가이드
릴리스 정보
버전 2.0은 라이브러리 출시 이후 가장 큰 API 변경을 가져왔습니다. 새로운 API와 함께 ReadableStream
지원, ESM 호환성, 그리고 수많은 버그 수정이 포함되었습니다. 이 가이드는 여러분의 애플리케이션을 버전 2.0으로 마이그레이션하는 데 도움을 줄 것입니다. 처음부터 끝까지 꼼꼼히 읽어보시길 강력히 권장합니다.
공식 발표를 놓치셨다면 꼭 읽어보세요!
Introducing MSW 2.0
Official announcement post.
Codemods
Codemod.com의 친구들이 MSW 2.0으로 마이그레이션을 도와줄 코드모드 컬렉션을 준비했습니다.
설치
npm install msw@latest
Breaking changes
Environment
Node.js 버전
이번 릴리스는 지원되는 최소 Node.js 버전을 18.0.0으로 설정합니다.
Node.js 18 이전 버전은 더 이상 지원되지 않습니다. MSW의 다음 버전을 사용하려면 Node.js 18 이상으로 마이그레이션하세요.
TypeScript 버전
이번 릴리스는 지원되는 최소 TypeScript 버전을 4.7로 설정합니다. 이전 TypeScript 버전을 사용 중이라면 MSW를 사용하려면 버전 4.7 이상으로 마이그레이션해야 합니다. 현재 시점에서 TypeScript 4.6은 거의 2년 된 버전임을 고려해 주세요.
Imports
Worker imports
브라우저 측 통합과 관련된 모든 내용은 이제 msw/browser
엔트리포인트에서 내보내집니다. 여기에는 setupWorker
함수와 관련 타입 정의가 포함됩니다.
이전:
import { setupWorker } from 'msw'
이후:
import { setupWorker } from 'msw/browser'
응답 리졸버 인자
응답 리졸버 함수는 더 이상 req
, res
, ctx
인자를 받지 않습니다. 대신 가로채진 요청에 대한 정보를 담은 단일 객체를 인자로 받습니다.
이전:
rest.get('/resource', (req, res, ctx) => {})
이후:
http.get('/resource', (info) => {})
사용한 핸들러 네임스페이스(http
또는 graphql
)에 따라 info
객체는 서로 다른 속성을 포함합니다. 이제 요청 정보에 접근하는 방법은 요청 변경 사항에서 확인할 수 있습니다.
업데이트된 요청 핸들러 네임스페이스의 호출 시그니처에 대해 더 알아보세요:
http
`http` 네임스페이스 API 참조.
graphql
`graphql` 네임스페이스 API 참조.
Request changes
Request URL
가로챈 요청이 이제 Fetch API의 Request
인스턴스로 설명되기 때문에, request.url
속성은 더 이상 URL
인스턴스가 아닌 일반 string
입니다.
이전:
rest.get('/resource', (req) => {
const productId = req.url.searchParams.get('id')
})
이후:
URL
인스턴스로 작업하려면, 먼저 request.url
문자열에서 이를 생성해야 합니다.
import { http } from 'msw'
http.get('/resource', ({ request }) => {
const url = new URL(request.url)
const productId = url.searchParams.get('id')
})
요청 파라미터
경로 파라미터는 더 이상 req.params
아래에 노출되지 않습니다.
이전:
rest.get('/post/:id', (req) => {
const { id } = req.params
})
이후:
경로 파라미터에 접근하려면 응답 리졸버의 params
객체를 사용하세요.
import { http } from 'msw'
http.get('/post/:id', ({ params }) => {
const { id } = params
})
요청 쿠키
요청 쿠키는 더 이상 req.cookies
아래에 노출되지 않습니다.
이전:
rest.get('/resource', (req) => {
const { token } = req.cookies
})
이후:
요청 쿠키에 접근하려면 응답 리졸버의 cookies
객체를 사용하세요.
import { http } from 'msw'
http.get('/resource', ({ cookies }) => {
const { token } = cookies
})
요청 본문
이제 req.body
속성을 통해 가로챈 요청 본문을 읽을 수 없습니다. 사실, Fetch API 명세에 따르면, 본문이 설정된 경우 request.body
는 ReadableStream
을 반환합니다.
이전:
rest.post('/resource', (req) => {
// 라이브러리는 요청의 "Content-Type" 헤더를 기반으로
// JSON 요청 본문을 가정했습니다.
const { id } = req.body
})
이후:
MSW는 더 이상 요청 본문 타입을 가정하지 않습니다. 대신, .text()
, .json()
, .arrayBuffer()
와 같은 표준 Request
메서드를 사용하여 원하는 방식으로 요청 본문을 읽어야 합니다.
import { http } from 'msw'
http.post('/user', async ({ request }) => {
// 요청 본문을 JSON으로 읽습니다.
const user = await request.json()
const { id } = user
})
응답 선언 방식 변경
이제 res()
합성 함수를 사용하여 모의 응답을 선언하지 않습니다. 웹 표준을 준수하기 위해 합성 방식을 벗어나기로 결정했습니다.
이전 방식:
rest.get('/resource', (req, res, ctx) => {
return res(ctx.json({ id: 'abc-123' }))
})
새로운 방식:
모의 응답을 선언하려면 Fetch API의 Response
인스턴스를 생성하고 응답 리졸버에서 반환합니다.
import { http } from 'msw'
http.get('/resource', () => {
return new Response(JSON.stringify({ id: 'abc-123' }), {
headers: {
'Content-Type': 'application/json',
},
})
})
더 간결한 인터페이스를 제공하고 응답 쿠키 모의 기능도 지원하기 위해, 이제 라이브러리에서 기본 Response
클래스 대신 사용할 수 있는 커스텀 HttpResponse
클래스를 제공합니다.
import { http, HttpResponse } from 'msw'
export const handlers = [
http.get('/resource', () => {
return HttpResponse.json({ id: 'abc-123' })
}),
]
새로운 HttpResponse
API에 대해 더 알아보세요:
HttpResponse
`HttpResponse` 클래스의 API 참조 문서입니다.
req.passthrough()
이전:
rest.get('/resource', (req, res, ctx) => {
return req.passthrough()
})
이후:
import { http, passthrough } from 'msw'
export const handlers = [
http.get('/resource', () => {
return passthrough()
}),
]
res.once()
res()
합성 API가 사라지면서, 일회성 요청 핸들러를 선언하는 res.once()
도 더 이상 사용할 수 없습니다.
이전:
rest.get('/resource', (req, res, ctx) => {
return res.once(ctx.text('Hello world!'))
})
이후:
일회성 요청 핸들러를 선언하려면, 세 번째 인자로 객체를 제공하고 해당 객체의 once
속성을 true
로 설정합니다.
import { http, HttpResponse } from 'msw'
http.get(
'/resource',
() => {
return new HttpResponse('Hello world!')
},
{ once: true },
)
res.networkError()
네트워크 오류를 모의로 만들려면 HttpResponse.error()
정적 메서드를 호출하고 응답 리졸버에서 반환하면 됩니다.
이전:
rest.get('/resource', (req, res, ctx) => {
return res.networkError('Custom error message')
})
이후:
import { http, HttpResponse } from 'msw'
http.get('/resource', () => {
return HttpResponse.error()
})
Response.error()
는 커스텀 오류 메시지를 받지 않는다는 점에 유의하세요. 이전에는 MSW가 제공한 커스텀 오류 메시지를 기본 요청 클라이언트에 강제로 적용하려고 했지만, 네트워크 오류 메시지를 처리하거나 무시하는 것은 요청 클라이언트의 몫이기 때문에 항상 안정적으로 작동하지는 않았습니다.
Context 유틸리티
이번 릴리스에서는 ctx
유틸리티 객체를 더 이상 사용하지 않습니다. 대신, HttpResponse
클래스를 사용하여 상태 코드, 헤더, 본문과 같은 모의 응답 속성을 선언하세요.
ctx.status()
이전:
rest.get('/resource', (req, res, ctx) => {
return res(ctx.status(201))
})
이후:
import { http, HttpResponse } from 'msw'
http.get('/resource', () => {
return new HttpResponse(null, {
status: 201,
})
})
ctx.set()
이전:
rest.get('/resource', (req, res, ctx) => {
return res(ctx.set('X-Custom-Header', 'foo'))
})
이후:
import { http, HttpResponse } from 'msw'
http.get('/resource', () => {
return new HttpResponse(null, {
headers: {
'X-Custom-Header': 'foo',
},
})
})
표준
Headers
API에 대해 알아보세요.
ctx.cookie()
이전:
rest.get('/resource', (req, res, ctx) => {
return res(ctx.cookie('token', 'abc-123'))
})
이후:
import { http, HttpResponse } from 'msw'
http.get('/resource', () => {
return new HttpResponse(null, {
headers: {
'Set-Cookie': 'token=abc-123',
},
})
})
라이브러리는 HttpResponse
클래스를 통해 응답 쿠키를 모킹할 때 이를 감지할 수 있습니다. 응답 쿠키를 모킹하려면 반드시 이 클래스를 사용해야 합니다. 왜냐하면 기본 Response
클래스에서는 쿠키가 설정된 후에 이를 읽을 수 없기 때문입니다.
ctx.body()
이전:
rest.get('/resource', (req, res, ctx) => {
return res(ctx.body('Hello world'), ctx.set('Content-Type', 'text/plain'))
})
이후:
import { http, HttpResponse } from 'msw'
http.get('/resource', (req, res, ctx) => {
return new HttpResponse('Hello world')
})
ctx.text()
이전:
rest.get('/resource', (req, res, ctx) => {
return res(ctx.text('Hello world!'))
})
이후:
import { http, HttpResponse } from 'msw'
http.get('/resource', () => {
return new HttpResponse('Hello world!')
})
ctx.json()
이전:
rest.get('/resource', (req, res, ctx) => {
return res(ctx.json({ id: 'abc-123' }))
})
이후:
import { http, HttpResponse } from 'msw'
http.get('/resource', () => {
return HttpResponse.json({ id: 'abc-123' })
})
HttpResponse.text()
,HttpResponse.json()
과 같은 정적HttpResponse
메서드를 사용할 때는Content-Type
응답 헤더를 명시적으로 지정할 필요가 없습니다.
ctx.xml()
이전:
rest.get('/resource', (req, res, ctx) => {
return res(ctx.xml('<foo>bar</foo>'))
})
이후:
import { http, HttpResponse } from 'msw'
http.get('/resource', () => {
return HttpResponse.xml('<foo>bar</foo>')
})
ctx.data()
이전:
graphql.query('GetUser', (req, res, ctx) => {
return res(
ctx.data({
user: {
firstName: 'John',
},
}),
)
})
이후:
graphql
핸들러 네임스페이스는 더 이상 특별한 처리를 받지 않습니다. 대신, 표준 JSON 응답을 직접 선언해야 합니다.
GraphQL 작업에 대한 모의 응답 정의를 더 편리하게 만들기 위해 HttpResponse.json()
정적 메서드를 사용하세요:
import { graphql, HttpResponse } from 'msw'
graphql.query('GetUser', () => {
return HttpResponse.json({
data: {
user: {
firstName: 'John',
},
},
})
})
HttpResponse
를 사용할 때는 응답에 루트 레벨data
속성을 명시적으로 포함해야 합니다.
ctx.errors()
이전:
graphql.mutation('Login', (req, res, ctx) => {
const { username } = req.variables
return res(
ctx.errors([
{
message: `Failed to login: user "${username}" does not exist`,
},
]),
)
})
이후:
import { graphql, HttpResponse } from 'msw'
graphql.mutation('Login', ({ variables }) => {
const { username } = variables
return HttpResponse.json({
errors: [
{
message: `Failed to login: user "${username}" does not exist`,
},
],
})
})
HttpResponse
를 사용할 때는 응답에errors
루트 레벨 속성을 명시적으로 포함해야 합니다.
ctx.extensions()
이전:
graphql.query('GetUser', (req, res, ctx) => {
return res(
ctx.data({
user: {
firstName: 'John',
},
}),
ctx.extensions({
requestId: 'abc-123',
}),
)
})
이후:
import { graphql, HttpResponse } from 'msw'
graphql.query('GetUser', () => {
return HttpResponse.json({
data: {
user: {
firstName: 'John',
},
},
extensions: {
requestId: 'abc-123',
},
})
})
ctx.delay()
이전:
rest.get('/resource', (req, res, ctx) => {
return res(ctx.delay(500), ctx.text('Hello world'))
})
이후:
이제 라이브러리에서 delay()
함수를 내보내며, 이 함수는 타임아웃 Promise
를 반환합니다. 서버 측 지연을 모방하기 위해 응답 리졸버 내 어디에서나 이 함수를 await
할 수 있습니다.
import { http, HttpResponse, delay } from 'msw'
http.get('/resource', async () => {
await delay(500)
return HttpResponse.text('Hello world')
})
delay()
함수의 호출 시그니처는 이전 ctx.delay()
와 동일하게 유지됩니다.
delay
`delay` 함수의 API 참조.
ctx.fetch()
이전:
rest.get('/resource', async (req, res, ctx) => {
const originalResponse = await ctx.fetch(req)
const originalJson = await originalResponse.json()
return res(
ctx.json({
...originalJson,
mocked: true,
}),
)
})
이후:
핸들러 내에서 추가 요청을 수행하려면 msw
에서 내보낸 새로운 bypass
함수를 사용하세요. 이 함수는 주어진 Request
인스턴스를 감싸서 MSW가 요청을 가로챌 때 무시하도록 표시합니다.
import { http, HttpResponse, bypass } from 'msw'
http.get('/resource', async ({ request }) => {
const originalResponse = await fetch(bypass(request))
const originalJson = await originalResponse.json()
return HttpResponse.json({
...originalJson,
mocked: true,
})
})
bypass
`bypass` 함수의 API 참조.
printHandlers()
worker
/server
의 .printHandlers()
메서드는 새로운 .listHandlers()
메서드로 대체되었습니다.
이전:
worker.printHandlers()
이후:
새로운 .listHandlers()
메서드는 현재 활성화된 요청 핸들러의 읽기 전용 배열을 반환합니다.
worker.listHandlers().forEach((handler) => {
console.log(handler.info.header)
})
onUnhandledRequest
onUnhandledRequest
의 request
인자가 추상적인 요청 객체에서 Fetch API의 Request
인스턴스로 변경되었습니다. request.url
과 같은 속성에 접근할 때 이 점을 고려해야 합니다.
이전:
server.listen({
onUnhandledRequest(request, print) {
const url = request.url
if (url.pathname.includes('/assets/')) {
return
}
print.warning()
},
})
이후:
request
인자가 Request
의 인스턴스가 되면서, url
속성은 string
타입이 되었습니다.
server.listen({
onUnhandledRequest(request, print) {
// URL 인스턴스를 직접 생성해야 합니다.
const url = new URL(request.url)
if (url.pathname.includes('/assets/')) {
return
}
print.warning()
},
})
라이프사이클 이벤트
이번 릴리스에서는 라이프사이클 이벤트 리스너의 호출 시그니처가 변경되었습니다.
이전:
server.events.on('request:start', (request, requestId) => {})
이후:
모든 라이프사이클 이벤트 리스너는 이제 _단일 인자_를 받으며, 이 인자는 객체입니다.
server.events.on('request:start', ({ request, requestId }) => {})
새로운 API
이번 릴리스에서는 주요 변경 사항 외에도 새로운 API 목록이 추가되었습니다. 대부분은 더 이상 사용되지 않는 기능과의 호환성을 제공하는 데 초점이 맞춰져 있습니다.
Frequent issues
Request
/Response
/TextEncoder
가 정의되지 않음 (Jest)
이 문제는 여러분의 환경이 어떤 이유로든 Node.js 전역 변수를 가지고 있지 않아 발생합니다. 이는 주로 jest-environment-jsdom
을 사용할 때 발생하는데, 이 환경이 의도적으로 내장 API를 폴리필로 대체하여 Node.js 호환성을 깨뜨리기 때문입니다.
이 문제를 해결하려면 jest-environment-jsdom
대신 jest-fixed-jsdom
환경을 사용하세요.
npm i jest-fixed-jsdom
// jest.config.js
module.exports = {
testEnvironment: 'jest-fixed-jsdom',
}
이 커스텀 환경은 jest-environment-jsdom
의 상위 집합으로, 내장 Node.js 모듈이 다시 추가된 버전입니다. 하지만 Jest/JSDOM이 테스트 환경에서 깨뜨리는 많은 것들이 있어 이를 고치는 것은 문제가 많습니다. 이 설정은 임시 해결책입니다.
이 설정이 번거롭다면, Node.js 전역 변수 문제가 없고 기본적으로 ESM을 지원하는 Vitest와 같은 모던 테스트 프레임워크로 마이그레이션하는 것을 고려해 보세요.
‘msw/node’ 모듈을 찾을 수 없음 (JSDOM)
이 오류는 테스트 러너가 JSDOM이 기본적으로 browser
내보내기 조건을 사용하기 때문에 발생합니다. 이는 MSW와 같은 서드파티 패키지를 임포트할 때 JSDOM이 browser
내보내기를 강제로 사용하도록 한다는 의미입니다. 이는 잘못된 설정이며 위험한데, JSDOM은 여전히 Node.js에서 실행되기 때문에 설계상 브라우저와의 완전한 호환성을 보장할 수 없기 때문입니다.
이 문제를 해결하려면 jest.config.js
파일에서 testEnvironmentOptions.customExportConditions
옵션을 ['']
로 설정하세요:
// jest.config.js
module.exports = {
testEnvironmentOptions: {
customExportConditions: [''],
},
}
이렇게 하면 JSDOM이 msw/node
를 임포트할 때 기본 내보내기 조건을 사용하도록 강제하여 올바른 임포트가 이루어집니다.
Node.js에서 multipart/form-data
미지원 오류
Node.js의 이전 버전(예: v18.8.0)에서는 request.formData()
를 공식적으로 지원하지 않았습니다. 해당 기능이 추가된 최신 Node.js 18.x 버전으로 업그레이드하세요.