현재 진행하고 있는 Next.js
프로젝트에서 MSW(Mock Service Worker)
를 도입하게 되었다.
프로젝트에서 사용하고 있는 API가 호출할 때마다 비용이 발생하는 구조라서, 개발 및 테스트 단계에서 매번 실제 API를 호출하기에는 비용적으로 부담이 될 것 같았다. 이를 위해 실제 API를 호출하지 않고 가상의 응답을 제공하는 MSW를 도입하기로 결정했다. MSW를 사용해서 개발 및 테스트 환경에서 과금에 대한 걱정없이 API 호출할 수 있었고, 그 과정을 정리해보고자 한다.
MSW 공식문서
Mock Service Worker(MSW)
는 브라우저나 Node.js
환경에서 네트워크 요청을 가로채고, 미리 정의된 가상의 응답을 제공하는 라이브러리다. 이를 통해 실제 API 서버에 요청을 보내지 않고도 애플리케이션을 개발하고 테스트할 수 있다. MSW
는 Service Worker
를 기반으로 동작하며, 클라이언트 사이드와 서버 사이드 모두에서 사용 가능하다.
npm install msw@latest --save-dev
// src/mocks/handlers.ts
import { http, HttpResponse } from 'msw'
export const handlers = [
// GET 요청을 가로채고 가상 응답을 제공
http.get('https://example.com/user', () => {
// 가상의 응답 데이터 반환
return HttpResponse.json({
id: 'c7b3d8e0-5e0b-4b0f-8b3a-3b9f4b3d3b3d',
firstName: 'John',
lastName: 'Maverick',
})
}),
]
MSW는 Service Worker
를 사용하기 때문에, mockServiceWorker.js
파일을 생성해야 한다. 다음 명령어를 실행하면 public
디렉토리에 Service Worker
파일이 추가되고, package.json
파일에 msw.workerDirectory
가 추가된다.
npx msw init public/ --save
MSW
는 브라우저와 Node.js
환경에서 각각 다른 방식으로 동작하기 때문에, 브라우저용과 서버용 워커를 설정해야 한다.
browser.ts
: 브라우저 환경에서 MSW 워커 설정// src/app/mocks/browser.ts
import { setupWorker } from "msw/browser";
import { handlers } from "./handlers";
export const worker = setupWorker(...handlers);
server.ts
: Node.js 환경(예: 서버사이드 렌더링)에서 MSW 서버 설정// src/app/mocks/server.ts
import { setupServer } from "msw/node";
import { handlers } from "./handlers";
export const server = setupServer(...handlers);
// src/app/mocks/index.ts
export async function initMsw() {
if (typeof window === "undefined") {
const { server } = await import("./server");
server.listen();
} else {
const { worker } = await import("./browser");
await worker.start();
}
}
initMsw
함수는 실행 환경이 브라우저인지 Node.js인지 확인하여 해당 환경에 맞는 워커를 시작한다.worker.start
를 호출하여 워커를 시작하고, Node.js 환경에서는 server.listen
을 호출하여 서버용 MSW를 시작한다.// src/app/mocks/MSWComponent.tsx
"use client";
import { useEffect, useRef, useState } from "react";
export const MSWComponent = ({ children }: { children: React.ReactNode }) => {
const [mswReady, setMswReady] = useState(false);
const hasStartedRef = useRef(false);
useEffect(() => {
const init = async () => {
if (hasStartedRef.current) return;
hasStartedRef.current = true;
const { initMsw } = await import("./index");
await initMsw();
setMswReady(true);
};
if (!mswReady) {
init();
}
}, [mswReady]);
return <>{children}</>;
};
MSWComponent
는 최상위 컴포넌트로, MSW를 초기화하고 자식 컴포넌트를 렌더한다.useEffect
훅을 사용해 MSW가 여러번 초기화되는 것을 방지했다.[MSW] Found a redundant "worker.start()" call. Note that starting the worker while mocking is already enabled will have no effect. Consider removing this "worker.start()" call.
// src/app/layout.tsx
import { MSWComponent } from "./mocks/MSWComponent";
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body>
<MSWComponent>{children}</MSWComponent>
</body>
</html>
);
}
RootLayout
컴포넌트 내에 MSWComponent
를 포함시켜 애플리케이션 전체에서 MSW가 활성화되도록 했다.
// src/app/page.tsx
import axios from "axios";
import { useState } from "react";
export default function Home() {
const [data, setData] = useState();
const callAPI = async () => {
const apiURL = `${process.env.NEXT_PUBLIC_API_BASE_URL}/api`;
setIsLoading(true);
try {
const response = await axios.get(apiURL);
const data = response.data;
setData(data);
} catch (error) {
console.error("Error fetching data: ", error);
} finally {
setIsLoading(false);
}
};
...
}
콘솔에 아래와같이 모킹이 활성화 되었다는 메시지가 뜨면 성공!🎉