mswjs

GI JUNG·2024년 1월 31일
0

react

목록 보기
5/8
post-thumbnail

mswjs를 알게 된 건 같이 프로젝트를 진행하던 팀원이 mswjs를 사용해보자고 해서 사용하게 되었다.
처음에는 간단하게 json-server를 사용하려고 했으나 보다 더 많은 기능과 특히 mock api server를 개발자 입맛에 맞게 어느정도 커스텀이 가능하다는 점에서 채택하게 되었다.

🤔 MSWJS란?

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>

Video Label

짧은 영상이지만 필요한 개념인 request interception & mock response & service worker 에 관해 나온다.

<mswjs 네트워크 흐름>

mswjs는 network level에 service worker를 두고 요청을 intercept(가로채기)하며 요청에 대한 mock response(모조 응답)을 내려준다.

  • service worker: client단과 backend단 사이에 존재하는 layer로써 request를 가로채며(stub; interception) mock response를 내려주는 역할을 담당하며 caching 또한 제공한다.
  • intercepting request: client에서 server로 보내는 요청을 중간에 service worker가 가로채 msw server에게 전달한다.
  • send mock response: service worker에 의해 모조응답이 client단으로 전달되며 이 모조 응답을 통해 테스트 및 디버깅을 할 수 있다.

🖥️ Intercepting requests

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)

📦 Mocking Responses

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하여 사용하길 권장하고 있다.

  1. HttpResponse 클래스는 json, xml, formData 등과 같은 단축 메서드를 캡슐화한다.
  2. 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를 내려주는 것을 확인할 수 있다.

❗️❗️ 주의할 점(version up)

msw를 공부하면서 아주 많은 에러에 마주했다. 이유는 1 -> 2로 버전 업데이트가 진행되어 많은 것이 바뀌었기 때문이다. docs나 블로그를 참조하면 http 대신 rest를 사용하는데 rest를 msw가 아예 읽지 못 해 혹시나 해서 버전을 찾아본 결과 rest 대신 http로 바뀌었다.

버전이 업데이트 되면서 크게 바뀐 3가지이다.

  • renames the rest object to http(rest.get -> http.get): rest에서 http로 바뀌었다.
  • All browser-side exports, like setupWorker, SetupWorkerApi etc... must be imported from msw/browser now: browser와 관련된 기능은 msw -> msw/browswer로 분리
  • Response resolver call signature is no longer(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가 되는지 몰라 헤맸었다...🥲🥲🥲

📚 참고

mswjs docs
use mswjs blog
msw v2.x.x release note

profile
step by step

0개의 댓글