mswjs를 알게 된 건 같이 프로젝트를 진행하던 팀원이 mswjs를 사용해보자고 해서 사용하게 되었다.
처음에는 간단하게 json-server를 사용하려고 했으나 보다 더 많은 기능과 특히 mock api server를 개발자 입맛에 맞게 어느정도 커스텀이 가능하다는 점에서 채택하게 되었다.
mock은 사전적인 의미로 모조품
을 뜻하며 mswjs docs에서 이와 같이 소개한다.
Mock Service Worker is an API mocking library that allows you to write client-agnostic mocks and reuse them across any frameworks, tools, and environments.
MSW는 Mock Service Worker의 약자로 어느 도구, 환경 및 클라이언트 프레임워크에 상관없이 재사용 가능한 모조품을 만들게 해주는 API Mocking Library라 한다.
🤔 mocking이란??
실제 API를 호출하는 대신 가상의 API 호출을 시뮬레이션하는 기술을 뜻하여 개발 중 또는 테스트할 때 유용하다. (e.g json-server)
간단하게 말해서 가상의 API 호출을 담당하는 서버를 구축할 수 있다는 것이다.
프로젝트가 돌아가는 환경은 browser이지만 node.js에서 사용한다면 주의사항이 있다.
⚠️ mswjs의 service worker는 브라우저에서만 사용이 가능하기 때문에 node.js에서 사용하려면 별도의 모듈 확장을 통해 가능하다.
MSWJS를 사용함으로써 백엔드 서버 없이도 쉽게 API 호출 시뮬레이션, 테스트, 프로토타이핑, 디버깅 등에 유용하게 사용할 수 있다.
mswjs는 브라우저에 Service Worker(서비스 워커)를 등록함으로써 외부로 나가는 네트워크 요청을 감지한다.
<mswjs 작동 영상 from mswjs docs>
짧은 영상이지만 필요한 개념인 request interception & mock response & service worker
에 관해 나온다.
<mswjs 네트워크 흐름>
mswjs는 network level에 service worker를 두고 요청을 intercept(가로채기)하며 요청에 대한 mock response(모조 응답)을 내려준다.
request를 가로채며(stub; interception) mock response를 내려주는 역할
을 담당하며 caching
또한 제공한다.요청을 중간에 service worker가 가로채 msw server에게 전달
한다.이 모조 응답을 통해 테스트 및 디버깅
을 할 수 있다.msw는 browser에 service worker를 두고 외부로 나가는 request를 intercept
한다고 위에서 언급했다. 이 때 intercept(가로채기)를 하는 주체가 resolver(Response Resolver)
이다.
요청에 대한 모조 응답을 보내기 위해서는 요청을 먼저 가로채야 한다. request handler 호출을 통해 Service Worker가 요청을 가로챌 수 있게 만들 수 있다.
💡 docs에서는 path를
predicate: (string | RegExp)
request handler를resolve: (Resonponse resolver)
라고 하는데 편의상path, request handler
라고 하자
request handler는 어떤 요청을 가로채고 어떻게 처리할지 대한 함수이다.
간단한 예시를 봐보자.
import { http } from 'msw
http.get('/pets', ({ request, params, cookies }) => {
return HttpResponse.json(['Tom', 'Jerry', 'Spike'])
})
'/pets'
라는 route로 요청이 오면 모조 응답으로 Tom, Jerry, Spike
를 요소로 하는 배열을 응답으로 내려준다.
node.js로 서버를 구축하는 것과 매우 비슷하다. mswjs또한 path에 대한 여러가지 기능을 제공한다.
string pathname
: 정확히 string과 일치하는 path에 대한 request를 가로챈다.
http.get('/pets', request handler)
wild card
: 부모 path(/pets
)가 같은 보든 path에 대한 request를 가로챈다.
http.get('/pets/*', requeste handler)
RegExp
: 정규식을 통해서 가로채질 request를 정의할 수 있다.
http.delete(/\/settings\/(sessions|messages)/, resolver)
MSW는 WHATWG API 명세를 준수하여 모든 mock response(모조 응답)은 WHATWG API 명세서의 내용과 동일하다.
가로챈 request에 대해서 mock response를 해주는 방법은 유효한 response 인스턴스
를 return함으로써 수행할 수 있다.
import { http } from 'msw';
export const handler = [
http.get('/resource', () => {
return new Response("Hello MSW");
}
]
위와 같이 Response를 이용하여 응답하는 방법도 있지만, MSW에서는 아래 두 가지 이유에 근거하여 자체 HttpResponse를 import하여 사용하길 권장
하고 있다.
- HttpResponse 클래스는
json, xml, formData 등과 같은 단축 메서드를 캡슐화
한다.- browser에서 제공하는
Response
와 다르게HttpResponse
를 이용하면Set-Cookie 등
을 설정하여 mock response에 쿠키를 담을 수 있다.
위의 2가지의 기능을 편하게 사용하고 싶다면 MSW에서 제공하는 HttpResponse 객체를 사용
하면 좋을 듯 하다. docs에서는 결국 HttpResponse는 Response instance를 return하며 native response와 100% 호환이 된다고 한다.
response에 상태코드를 담아 보낼 수 있듯이 mswjs도 가능하다.
import { http, HttpResponse } from 'msw';
export const handlers = [
http.get('/apples', () => {
return new HttpResponse(null, {
status: 404,
statusText: 'Out of Apples'
})
})
]
원래는 native Response객체에 headers에 지정할 수 없는 것들은 msw에서 제공하는 HttpResponse를 통해서 지정할 수 있다.
import { http, HttpResponse } from 'msw'
export const handlers = [
http.post('/auth', () => {
return new HttpResponse(null, {
headers: {
'Set-Cookie': 'mySecret=abc-123', // 👉🏻 cookie 설정
'X-Custom-Header': 'yes',
},
})
}),
]
HttpResponse도 native Response와 마찬가지로 Body에 plainText 및 json 등등을 작성하여 응답할 수 있다.
import { http, HttpResponse } from 'msw'
export const handlers = [
http.get('/name', () => {
// HttpResponse('John')를 'text' shorthand method를 이용하여 작성할 수 있다.
// HttpResponse.text('John');
return new HttpResponse('John')
}),
]
이 외에도 error를 전달할 수도 있다.
// error 전달
import { http, HttpResponse } from 'msw'
export const handlers = [
http.post('/checkout/cart', () => {
return HttpResponse.error()
}),
]
// throw error
import { http, HttpResponse } from 'msw'
http.post('/login', ({ request }) => {
if (!request.headers.has('cookie')) {
throw new HttpResponse(null, { status: 400 })
}
})
⚠️ error를 throw하면 4xx, 5xx에 대해 response.ok를 자동으로
false
로 설정된다.
get, post이 외의 request method인 patch, put, delete
도 native response와 100%호환이므로 사용이 가능하다.
mswjs를 사용하기 위해 library를 설치하자
yarn add -D msw
pulblic directory에 mockServiceWorker.js
란 파일이름으로 둠으로써 service worker를 호스팅
할 수 있다.
$) npx msw init <PUBLIC_DIR>
$) npx msw init ./public
명령어를 실행하면 public directory하위에 mockServiceWorker.js
가 만들어지며 domain/mockServiceWorker.js
로 routing하면 service worker에 대한 script를 볼 수 있다.
이제 service worker를 등록하자
// src/mocks/browser.ts
import { setupWorker } from 'msw/browser'
import { handlers } from './handlers'
export const worker = setupWorker(...handlers)
msw에서 service worker를 실행시키기 위해선 worker.start()
를 통해 실행시킬 수 있다. service worker는 비동기 연산을 수행
한다.
// main.tsx
import App from './App';
// * run the mocking api before createRoot called
async function enableMocking() {
// process.env.NODE_ENV
if (import.meta.env.MODE !== 'development') {
return;
}
const { worker } = await import('@/mocks/browser');
return worker.start();
}
// service worker가 실행된 후 APP이 mount됨
enableMocking().then(() => {
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>,
);
});
mocking이 완료되면 browser의 console창에서 아래와 같이 확인할 수 있다.
[MSW] Mocking enabled
이 뜬다면 정상적으로 msw를 실행시킨 것이다.
💡 vite에서는 process.env.NODE_ENV를 import.meta.MODE로 자체적으로 변환하여 처리한다.
그 전에 mock data가 필요하므로 src/db/userDB.json
을 만든다. 이후 request handler를 src/mock/handlers.ts
에 정의하여 mock response를 처리한다.
import { HttpResponse, http } from 'msw';
import { userEndpoints } from '@/constants/apiEndpoints';
import data from '@/db/userDB.json';
type UserParams = {
userId: string;
};
const userHandler = [
http.get<UserParams>(userEndpoints.user, async ({ params }) => {
const { userId } = params;
const foundUser = data.user.find(user => user.id === userId);
if (!foundUser)
return new HttpResponse(null, {
status: 404, // 4xx, 5xx에 대해서는 client에서 자동으로 ok=false가 된다.
statusText: `Couldn't found user`,
});
return new HttpResponse(JSON.stringify(foundUser), {
status: 200,
statusText: 'found user!',
headers: {
'Content-Type': 'application/json',
},
});
}),
];
export const handlers = [...userHandler];
user endpoint대한 mock response처리를 request handler에서 처리해주었다. 이를 성공 및 실패시 어떤 msw는 우리에게 어떤 정보를 주는지 확인해보자.
<✅ 성공 시>
<❌ 실패 시>
성공과 실패 시 request handler에서 정의했던 status code & status text
를 내려주는 것을 확인할 수 있다.
msw를 공부하면서 아주 많은 에러에 마주했다. 이유는 1 -> 2로 버전 업데이트가 진행되어 많은 것이 바뀌었기 때문이다. docs나 블로그를 참조하면 http 대신 rest
를 사용하는데 rest를 msw가 아예 읽지 못 해 혹시나 해서 버전을 찾아본 결과 rest 대신 http
로 바뀌었다.
버전이 업데이트 되면서 크게 바뀐 3가지이다.
rest
object to http(rest.get -> http.get)
: rest에서 http로 바뀌었다.setupWorker, SetupWorkerApi
etc... must be imported from msw/browser
now: browser와 관련된 기능은 msw -> msw/browswer
로 분리res, req, ctx => res()
) but instead({request}) => new Response()
: 매개변수 res, req, ctx는 request객체로 통합
나머지 change log들은 여기를 참고하자.
지금 현재 진행중인 프로젝트에 막 도입시킨 것으로 초입의 상태이다. 간단한 msw의 작동방식과 사용을 알아보았다. 나는 귀찮아서 json-server만을 사용했었는데 msw는 response의 interface또한 커스텀 가능하며 실제 서버대신 서버를 사용하는 것과 같은 효과를 만들어 테스트, 디버깅 및 실제 서버에 붙이기 전에 임시 mock data를 사용하기 위한 간단한 REST API 작성이 매우 쉬워서 애용할 듯 하다. msw를 공부하면서 오류 상태 코드에 대한 ok의 응답은 자동으로 false가 되는지 몰라 헤맸었다...🥲🥲🥲