현재 진행하고 있는 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);
}
};
...
}
콘솔에 아래와같이 모킹이 활성화 되었다는 메시지가 뜨면 성공!🎉
