이전의 프로젝트는 이미 API가 어느정도 윤곽이 잡힌 상태에서 개발에 투입이 되었지만, 현재 하고 있는 프로젝트는 기획이 완료됨과 동시에 프론트엔드 + 백엔드 개발이 동시에 시작이 되었다. 따라서 더미데이터를 만들어 개발하거나 UI만 개발하면서 API가 개발이 될 때까지 무한정 기다려야한다.
이를 극복하고자 검색하던 도중 초기 개발 단계에서 이러한 문제를 극복하고자 MSW로 개발하는 것을 알게 되었고 팀원에게 MSW를 소개하면서 우리 프로젝트에도 도입하기로 결정하였다.
MSW는 Mock Service Worker 이다.
HTTP 요청을 보내면 MSW가 가로채 사전에 설정해둔 데이터로 응답을 보내는 도구다. 따라서 사전에 백엔드 개발자와 협의된 API 스펙이 있다면 수월하게 작업할 수 있을 것이다.
개발 환경에서만 사용할 것이기 때문에 devDependencies
로 설치한다.
npm i -D msw
yarn add -D msw
브라우저에서 사용하기 위해서는 MSW를 서비스 워커에 등록해야한다.
MSW에서 제공하는 CLI을 통해 Service Worker
를 등록할 수 있다.
npx msw init public/ --save
mkdir src/mocks
touch src/mocks/handlers.js
// src/mocks/browser.ts
import { setupWorker } from "msw";
import { handlers } from "@mocks/handlers";
export const worker = setupWorker(...handlers);
export const worker = setupWorker(...authHandlers, ...studyHandler);
// 나는 api가 많아져서 관리하기 힘들어질 상황을 고려해서 밑에 방법을 택했다.
Next.js는 브라우저와 노드 환경을 추가로 설정해주어야 한다.
https://github.com/vercel/next.js/tree/canary/examples/with-msw/mocks
mocks/browser.ts
import { SetupWorker, setupWorker } from 'msw';
import { handlers } from '@mocks/handlers/handlers';
import { studyHandlers } from '@mocks/handlers/studyHandlers';
export const worker: SetupWorker = setupWorker(...handlers);
mocks/server.ts
import { SetupServer, setupServer } from 'msw/node';
import { handlers } from '@mocks/handlers/handlers';
import { studyHandlers } from '@mocks/handlers/studyHandlers';
export const server: SetupServer = setupServer(...handlers);
mocks/index.ts
const initMocks = async (): Promise<void> => {
if (typeof window === 'undefined') {
const { server } = await import('@mocks/server');
server.listen();
} else {
const { worker } = await import('@mocks/browser');
worker.start();
}
};
export { initMocks };
마운트 되기 전에 worker가 실행될 준비가 완료되기 전에 실행되므로 에러가 발생한다. 그래서
_app.tsx
에 worker를 지연시키기 위한 로직을 작성해야한다. 나 같은 경우는 로직이 너무 길어져서 커스텀 훅으로 분리해서 적용하였다.
관련 이슈 : https://github.com/mswjs/msw/discussions/1049
useIsWorker.ts
/**
* @description msw worker를 지연시키기 위한 커스텀 훅입니다.
* @returns { shouldRender } 모킹 사용가능 여부
*/
const useIsWorker = () => {
const mockingEnabled = !!process.env.NEXT_PUBLIC_API_MOCKING;
const [shouldRender, setShouldRender] = useState(!mockingEnabled);
useEffect(() => {
if (mockingEnabled) {
import('../mocks').then(async ({ initMocks }) => {
await initMocks();
setShouldRender(true);
});
}
}, []);
return { shouldRender };
};
export default useIsWorker;
_app.tsx
// _app.tsx
export default function App(...){
const { shouldRender } = useIsWorker();
if (!shouldRender) return null;
...
}
// src/mocks/data/todos.json
[
{
id: 1,
text: '운동하기',
},
{
id: 2,
text: '공부하기',
},
{
id: 3,
text: '장보기',
},
];
// src/mocks/handlers.ts
import { rest } from 'msw'
import { rest } from 'msw';
import todo from '@mocks/data/todo.json';
export const handlers = [
rest.get('/api/todos', (req, res, ctx) => {
return res(ctx.json(todo));
}),
];