최근 Next.js 버전 14에서 소개된 새로운 앱 라우터 기능을 활용하여 프로젝트를 진행하면서, MSW(Mock Service Worker)의 적용 방법에 대해 깊이 고민하였습니다.
이 글은 MSW를 잘 이해하고 있는 것을 전제 하에 작성되었으며, Next.js 환경에서 MSW를 통합하는 과정에서 겪었던 트러블 슈팅 경험을 정리하고자 합니다.
특히, 구현까지 도달하기 위한 과정을 중심적으로 정리할 것입니다.
MSW 소개
MSW는 네트워크 요청을 가로채고 모의 응답을 제공하는 강력한 라이브러리로, 개발자들이 백엔드 없이도 API 호출을 시뮬레이션할 수 있게 해 줍니다. 이는 테스트 환경에서의 네트워크 요청 관리를 용이하게 하며, Service Worker API를 사용해 실제와 유사한 네트워크 환경을 구현합니다.
결과부터 말하면 express로 해결하였습니다. 코드 예시는 글의 마지막 부분에서 확인하실 수 있습니다. 만약 바로 코드를 보고 싶으신 분들은 글의 최하단으로 스크롤해주시기 바랍니다.
MSW(Mock Service Worker)는 개발 과정에서 두 가지 주요 환경을 모킹할 수 있도록 설계되었습니다:
Next.js 애플리케이션의 초기 실행은 Node.js 환경에서 이루어집니다. 이는 window
객체가 없다는 것을 의미하며, 이로 인해 MSW의 Worker를 바로 사용하는 것이 어렵습니다. 반면, 클라이언트 사이드에서는 window
객체가 존재하므로 Worker 초기화가 가능합니다.
먼저 레퍼런스를 참고하였습니다.
올리브영 기술 블로그에서 테스트 환경에서 MSW를 효과적으로 사용하는 방법을 공유했습니다.
저는 실제 브라우저 환경에서 테스트가 필요하였는데, 저와는 다른 경우였습니다.
하지만 제가 생각한 것과 일치하게 동작하는지 확인을 하고 싶어서 적용해보았습니다.
// MSW 초기화 함수
async function initMSW() {
if (typeof window === "undefined") {
const { server } = await import("./server");
server.listen(); // Node.js 환경에서 Server 활성화
} else {
const { worker } = await import("./browser");
worker.start(); // 브라우저 환경에서 Worker 활성화
}
}
export { initMSW };
// 애플리케이션의 최상위 컴포넌트에서 initMSW() 호출
환경에 따라 적합한 MSW 모드를 선택하는 전략을 사용했습니다. 서버 측에서는 MSW의 Server를, 클라이언트 측에서는 Worker를 사용하는 전략입니다.
✅ 실제 적용 결과
예상한 것처럼 실행 환경이 node.js이기 때문에 server를 실행하였고, 모킹하였습니다.
추가적인 문제가 있었습니다.
server를 실행하여 모킹하였다고 생각했지만, node 환경에서 모킹이 작동하지않았습니다.
첫 시작 시에는 값을 잘주었지만, 페이지 이동이나 새로고침을 누르면 모킹이 안되었습니다.
(해당 이유는 당시에는 알지 못하였고, 아래에서 다룰 예정입니다.)
위 레퍼런스와 확인한 결과를 통해 고민 해본 결과, 서버와 클라이언트 각각에 맞게 MSW를 분리하여 설정하는 전략으로 전환했습니다.
Next.js 프로젝트에서 MSW를 효과적으로 적용하기 위해, 서버와 브라우저 환경을 분리하여 각각 다르게 MSW를 초기화하는 전략을 선택했습니다. 서버에서는 프로젝트 시작과 동시에 MSW의 Server를 활성화하고, 클라이언트 사이드에서는 브라우저 환경이 준비되면 MSW의 Worker를 활성화합니다.
Node.js 환경에서는 기존 방식을 유지하여 MSW의 Server를 초기화합니다.
// 서버에서 MSW 초기화
async function initMSW() {
if (typeof window === "undefined") {
const { server } = await import("./server");
server.listen(); // Node.js 환경에서 Server 활성화
}
}
export { initMSW };
클라이언트 사이드에서는 React의 useEffect
를 활용해 window
객체가 준비되었을 때 MSW의 Worker를 시작합니다.
// MockProvider 컴포넌트
export function MockProvider({ children }: { children: React.ReactNode; }) {
useEffect(() => {
async function enableApiMocking() {
if (typeof window !== "undefined") {
const { worker } = await import("@/app/shared/mocks/browser");
await worker.start();
}
}
enableApiMocking();
}, []);
return <>{children}</>;
}
// RootLayout 컴포넌트에서 MockProvider 사용
export default function RootLayout({ children }: { children: React.ReactNode; }) {
return (
<html lang="en">
<body>
<MockProvider>{children}</MockProvider>
</body>
</html>
);
}
서버
이전과 같은 결과를 얻었습니다.
문제 1:첫시작에는 잘되었고 이 후에는 잘되지 않았습니다.
클라이언트
문제 1: Worker가 비동기적으로 시작되므로 초기 fetch 요청이 모킹되지 않는 문제가 발생했습니다. 이는 Worker가 초기화되기 전에 요청이 실행되기 때문이었습니다.
문제 2: Worker를 2번 실행하는 이슈가 있었습니다.
클라이언트 문제를 해결하기 위해, MSW Worker가 완전히 활성화 후에 컴포넌트가 마운트되도록 조정했습니다.
또한, Worker가 중복해서 시작되지 않도록 추가적인 조건을 적용했습니다.
// MockProvider 컴포넌트
export function MockProvider({ children }: { children: React.ReactNode; }) {
const [isMocking, setIsMocking] = useState(false);
const isWorkerStarted = useRef(false);
useEffect(() => {
async function enableApiMocking() {
if (typeof window !== "undefined" && !isWorkerStarted.current) {
isWorkerStarted.current = true;
const { worker } = await import("../mocks/browser");
await worker.start();
console.log("Worker started");
setIsMocking(true);
}
}
enableApiMocking();
}, []);
if (!isMocking) {
return null; // Worker 활성화 전에는 컴포넌트를 렌더링하지 않음
}
return <>{children}</>; // Worker 활성화 후 컴포넌트 렌더링
}
// RootLayout에서 MockProvider 사용
export default function RootLayout({ children }: { children: React.ReactNode; }) {
return (
<html lang="en">
<body>
<MockProvider>{children}</MockProvider>
</body>
</html>
);
}
이 방식으로 MSW의 Worker가 활성화 후에만 자식 컴포넌트들이 마운트되도록 했습니다. 이는 초기 네트워크 요청이 모킹할 수 있었습니다.
추가적으로, Worker가 중복 실행되는 문제를 방지하기 위해 isWorkerStarted
라는 ref를 사용하여 Worker의 상태를 추적했습니다.
이러한 조치를 통해 브라우저 환경에서 MSW가 성공적으로 작동함을 확인할 수 있었습니다. 하지만, Node.js 환경에서는 Worker를 사용할 수 없으며, Server에 대한 추가적인 해결 방안이 필요했습니다.
Next.js 프로젝트에서 서버 측 모킹은 초기 로드 시에는 문제없이 작동하지만, 페이지 이동이나 리로드 시에는 기대한 대로 동작하지 않는 문제가 있었습니다. 이를 해결하기 위해, MSW 관련 이슈와 kettanaito(MSW 개발자)의 트위터 글을 참고했습니다.
kettanaito는 Next.js의 초기화 과정이 앱 시작 시 한 번만 실행되며, 이후 변경 사항이 반영되지 않는다고 설명합니다. 이에 따라, with-next 예제에서는 next.config.mjs
를 통해 webpack 설정을 조정하는 방식을 사용했습니다.
// next.config.mjs의 webpack 설정 예시
const nextConfig = {
experimental: {
instrumentationHook: true,
},
webpack(config, { isServer }) {
if (isServer) {
config.resolve.alias['msw/browser'] = false;
} else {
config.resolve.alias['msw/node'] = false;
}
return config;
},
}
instrumentationHook
옵션은 Next.js가 서버 측에서 MSW를 초기화할 수 있도록 하는 실험적 기능입니다. 이를 통해 앱 로드 전에 서버 사이드 모킹이 가능해지며, API 모킹을 위한 기반을 마련합니다.
그러나 이 방법은 핫 모듈 리로딩(HMR)을 지원하지 않는다는 한계가 있습니다. 즉, handlers.ts
같은 핸들러 파일에 변경이 있어도, 서버 런타임에서는 이를 반영하지 않습니다. 이는 instrumentationHook
이 한 번만 실행되며, 변경 사항을 감지하지 못하기 때문입니다.
실제로 실험해본 결과, webpack 설정을 조정하는 방식과 window
객체를 확인하는 방식에는 큰 차이는 없었습니다.
위 방법을 통해 서버 측 모킹은 작동했지만, 변경 사항을 실시간으로 확인하기 어려워 아쉬움이 있었습니다.
✅ 결론과 추가 문제
Next.js 환경에서 MSW를 활용한 서버 측 모킹은 실질적으로 어렵습니다. HMR 지원 부재로 인해 개발 편의성이 떨어집니다. 또한, 서버 측 모킹이 활성화됨에 따라 발생하는 추가적인 문제들에 대해서도 고려해야 합니다.
Next.js 환경에서 MSW의 Server와 Worker를 동시에 사용하려 했을 때, 두 환경 간에 데이터 동기화 문제가 발생했습니다. Server에서 변경된 데이터는 Worker에 의해 감지되지 않았고, 반대의 경우도 마찬가지였습니다.
앞서 말했듯이, Hot Module Reloading(HMR) 기능으로 인해 서버 측 데이터의 업데이트가 클라이언트에 반영되지 않는 문제가 있었습니다.
이로 인해, Server와 Worker를 분리해서 사용하는 접근 방식은 실제 프로젝트에 적합하지 않다는 결론에 이르렀습니다. 결국, 모든 모킹 작업을 Worker 또는 Server 중 하나로 통합해야 함을 깨달았습니다.
최종적으로, Next.js 프로젝트에 내장된 Express 서버를 활용하여 MSW를 통합하는 방식을 선택했습니다. 이를 위해 MSW의 http-middleware
를 사용했으며, 이 방법은 Next.js의 생명주기에 영향을 받지 않으면서 안정적인 API 모킹을 가능하게 합니다.
// Mock 서버 설정 예시
import { createMiddleware } from "@mswjs/http-middleware";
import cors from "cors";
import express from "express";
import { handlers } from "./handlers";
const app = express();
const port = 9090; // Mock 서버 포트
app.use(cors({
origin: "http://localhost:3000", // 클라이언트 주소
optionsSuccessStatus: 200,
credentials: true
}));
app.use(express.json());
app.use(createMiddleware(...handlers)); // MSW 핸들러 연결
app.listen(port, () => console.log(`Mock server is running on port: ${port}`));
// package.json에 추가한 스크립트
"mock": "npx tsx watch ./app/shared/mocks/http.ts",
이 설정을 통해, 별도의 Mock 서버를 구동하며 MSW에서 정의한 핸들러들을 통해 API 요청을 모킹할 수 있게 되었습니다.
express를 사용하여서 localhost를 사용해야하므로, 기존 API 주소를 그대로 활용할 수 없는 점은 아쉬웠으나, 프로젝트의 진행을 위해 현실적인 대안을 선택했습니다.
출처 :
https://oliveyoung.tech/blog/2024-01-23/msw-frontend/
https://github.com/mswjs/msw/issues/1644
https://twitter.com/kettanaito/status/1749496339556094316
https://github.com/mswjs/examples/pull/101
https://github.com/mswjs/http-middleware
https://mswjs.io/