[Next.js] Next 14 App Router에서 msw로 API mocking 하기
next: 14.2.22, msw: 2.7.0 기준으로 작성되었습니다.
msw 초기 설정
msw 설치 및 ServiceWorker 등록하는 파일 생성
npm i -D msw
npx msw init ./public --save
Next.js 에서 설정 시 유의할 점
Next.js의 App Router의 경우 기본적으로 서버 컴포넌트
사용
- 서버 컴포넌트는 서버에서 렌더링 → 브라우저 환경 ❌
MSW는 브라우저 환경에서만 동작하며, 클라이언트 측 네트워크 요청을 가로채고 이를 처리하는 방식으로 동작
- 브라우저 환경에서 네트워크 요청을 가로채기 위해 서비스 워커를 등록해야함
- 클라이언트에서 한 번만 이루어져야함
문제 1: msw module not found
next 14에서 msw를 실행했을 때 msw 패키지를 못찾는 오류가 발생한다.
✅ 해결: MSW 패키지의 특정 경로를 무시하도록 설정 변경
서버 환경과 클라이언트 환경에서 모두 빌드하는데, 각 환경에 맞는 파일을 탐색하기 위해 사용하지 않는 패키지의 경로를 무시하는 방식
- 서버 측에서 빌드할 땐
msw/browser
무시 - 클라이언트 측에서 빌드할 땐
msw/node
무시
next config
next.config.mjs 파일에서 webpack 커스텀 설정을 할 수 있다.
이 webpack 함수는 세 번 실행되며, 서버(nodejs/edge 런타임)에서 두 번, 클라이언트에서 한 번 실행됨
이를 통해 isServer
속성을 통해 클라이언트와 서버를 구별할 수 있음
// next.config.mjs
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
compiler: {
emotion: true,
},
webpack: (config, { isServer }) => {
if (isServer) {
// next server build => ignore msw/browser
if (Array.isArray(config.resolve.alias)) {
config.resolve.alias.push({ name: "msw/browser", alias: false });
} else {
config.resolve.alias["msw/browser"] = false;
}
} else {
// browser => ignore msw/node
if (Array.isArray(config.resolve.alias)) {
config.resolve.alias.push({ name: "msw/node", alias: false });
} else {
config.resolve.alias["msw/node"] = false;
}
}
return config;
},
};
export default nextConfig;
문제 2: SSR fetch mocking
msw는 브라우저의 서비스 워커를 통해 브라우저에서 서버로 보내는 네트워크 요청을 가로챈다.
SSR의 경우 브라우저를 통한 네트워크 요청이 아니므로 mocking이 되지 않는다.
export default async function Home() {
const res = await fetch("/api/test");
const result = await res.json();
console.log(result);
return (
<div>
<span>test</span>
</div>
);
}
✅ 해결: 별도의 목서버를 띄워 해결
msw middleware를 이용하여 별도의 mock server
를 띄우고, next 서버가 보내는 네트워크 요청 mocking
→ 서버 컴포넌트에서 호출하는 API 요청도 msw로 mocking
pnpm i -D express @types/express @mswjs/http-middleware
// http.ts
import express from "express";
import { createMiddleware } from "@mswjs/http-middleware";
import { handlers } from "./handlers";
const app = express();
const PORT = 3001;
app.use(express.json());
app.use(createMiddleware(...handlers));
app.listen(PORT, () => console.log(`Mock server is running on port: ${PORT}`));
// page.tsx
const BASE_URL =
process.env.NEXT_PUBLIC_API_MOCKING === "enable"
? process.env.NEXT_PUBLIC_MOCK_BASE_URL
: process.env.NEXT_PUBLIC_API_BASE_URL;
const getData = async () => {
const response = await fetch(`${BASE_URL}/api/test`);
return await response.json();
};
export default async function Home() {
const res = await getData();
return (
<div>
<span>{res.id}</span>
</div>
);
}
추가적인 오류 해결
오류 1: Found a redundant worker.start() call
첫 렌더링에 worker를 생성하고, initMSW 호출 후 setState가 실행되면 worker를 2번 생성한다.
불필요한 호출을 없애기 위해 ref에 worker 생성 여부를 저장하고 이미 생성했을 경우 호출을 막는다.
[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.
오류 2: Uncaught Error: Failed to parse URL from /api/test
상대경로는 브라우저의 현재 URL을 기준으로 동작하므로, 서버 측에서 실행되는 fetch
요청은 현재 호스트 URL을 알 수 없음
→ http 또는 https 로 시작하는 BASE_URL을 설정하여 절대 경로로 fetch 요청
SSR fetch mocking 성공
최종 반영 코드
// mocks/browser.ts
import { setupWorker } from "msw/browser";
import { handlers } from "./handlers";
export const worker = setupWorker(...handlers);
// mocks/server.ts
import { setupServer } from "msw/node";
import { handlers } from "./handlers";
export const server = setupServer(...handlers);
// mocks/index.ts
export const initMSW = async () => {
const isServer = typeof window === "undefined";
if (isServer) {
const { server } = await import("./server");
server.listen({ onUnhandledRequest: "bypass" });
} else {
const { worker } = await import("./browser");
await worker.start({ onUnhandledRequest: "bypass" });
}
};
mocks/MSWProvider.tsx
"use client";
import { useEffect, useRef, useState } from "react";
const isMSWEnabled = process.env.NEXT_PUBLIC_API_MOCKING === "enable";
const MSWProvider = ({ children }: { children: React.ReactNode }) => {
const [isMSWReady, setIsMSWReady] = useState(!isMSWEnabled);
const isStartedRef = useRef(!isMSWEnabled);
useEffect(() => {
const init = async () => {
if (isStartedRef.current) return;
isStartedRef.current = true;
const initMSW = await import("./index").then((res) => res.initMSW);
await initMSW();
setIsMSWReady(true);
};
if (!isMSWReady) {
init();
}
}, [isMSWReady]);
if (!isMSWReady) return null;
return <>{children}</>;
};
export default MSWProvider;
app/layout.tsx
import Providers from "./providers";
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body>
<MSWProvider>{children}</MSWProvider>
</body>
</html>
);
}
next.config.mjs
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
compiler: {
emotion: true,
},
webpack: (config, { isServer }) => {
if (isServer) {
// next server build => ignore msw/browser
if (Array.isArray(config.resolve.alias)) {
config.resolve.alias.push({ name: "msw/browser", alias: false });
} else {
config.resolve.alias["msw/browser"] = false;
}
} else {
// browser build => ignore msw/node
if (Array.isArray(config.resolve.alias)) {
config.resolve.alias.push({ name: "msw/node", alias: false });
} else {
config.resolve.alias["msw/node"] = false;
}
}
return config;
},
};
export default nextConfig;
mocks/http.ts
import express from "express";
import { createMiddleware } from "@mswjs/http-middleware";
import { handlers } from "./handlers";
const app = express();
const PORT = 3001;
app.use(express.json());
app.use(createMiddleware(...handlers));
app.listen(PORT, () => console.log(`Mock server is running on port: ${PORT}`));
app/page.tsx
const BASE_URL =
process.env.NEXT_PUBLIC_API_MOCKING === "enable"
? process.env.NEXT_PUBLIC_MOCK_BASE_URL
: process.env.NEXT_PUBLIC_API_BASE_URL;
const getData = async () => {
const response = await fetch(`${BASE_URL}/api/test`);
return await response.json();
};
export default async function Home() {
const res = await getData();
return (
<div>
<span>{res.id}</span>
</div>
);
}
📘 reference
- msw module not found 해결 - github issue
- SSR fetch mocking 문제 해결
- 불필요한 worker 호출 해결
- 참고 블로그
댓글남기기