Next.js App Router에서 MSW 사용하기

2025. 9. 16. 19:08

Mock API를 만들기 위해 MSW(Mock Service Worker)세팅하던 중에 알게 된 이슈가 있어서 정리하게 되었다.

Next.js 개발 모드의 프로세스 분리 구조 때문에 MSW의 설정이 공유되지 않고, HMR 발생 시마다 설정 유실이나 포트 충돌이 일어나기 때문에 외부 Express 서버를 두는 방식으로 적용하게 되었다.
브라우저 요청은 msw/browser를, 서버 사이드 요청은 express + @mswjs/http-middleware를 사용함.

원래는 msw/node를 사용해서 서버에서 발생하는 http 요청을 가로채야 하는데,
Next.js App Router 버전의 "개발모드"에서는 작동하는 프로세스가 2가지이기 때문에(지속 프로세스 & 임시 프로세스) msw만으로 글로벌 모듈 패치를 한번에 적용할 수 없다고 한다

관련 이슈: https://github.com/mswjs/msw/issues/1644

 

Support Next.js 13 (App Router) · Issue #1644 · mswjs/msw

Scope Adds a new behavior Compatibility This is a breaking change Feature description As of 1.2.2, MSW does not support the newest addition to Next.js being the App directory in Next.js 13. I'd lik...

github.com

"MSW는 http/https 같은 Node.js core 모듈을 패치하여 서버 사이드에서 호출을 가로채는 방식으로 작동하는데, Next.js App Router 구조에서는, 이런 패치가 한 프로세스에서만 작동할 수 있고, 다른 임시 프로세스는 새로 생성되므로 그 안에서는 패치된 상태가 유지되지 않는다. 즉, 각 프로세스에 대해 동일한 글로벌 패칭을 보장할 수 없다"
"루트 레이아웃과 개별 페이지 레이아웃을 평가하는 프로세스가 다르기 때문에, 한 프로세스에서의 패치가 다른 프로세스에는 영향을 주지 않는 문제, 또는 페이지 단위 프로세스는 임의의 포트에서 계속 생성되고 종료되므로 개발 페이지에서 발생하는 서버 사이드 요청을 모킹하기 위해 이 프로세스에 모듈 패칭을 적용할 방법이 없어 보인다"

1. 원인: Next.js의 이중 프로세스 구조와 HMR

Next.js App Router의 개발 모드는 성능 최적화를 위해 코드를 실행하는 방식을 두 가지 영역으로 나눈다고 한다.

  • 지속 프로세스 (Persistent Process): 서버가 시작될 때 한 번 생성되며, 루트 레이아웃(layout.tsx) 등을 처리함
  • 임시 프로세스 (Intermittent/Runtime Process): 페이지나 중첩 레이아웃이 수정(HMR)될 때마다 해당 코드를 다시 읽기 위해 수시로 재생성되거나 모듈 캐시를 초기화함

MSW의 Node.js 환경 동작 방식은 http, https 같은 글로벌 모듈을 가로채는(Patching) 방식이다. 하지만 위와 같은 이중 구조 때문에 다음과 같은 문제가 발생한다.

  • 패치 유실: 루트 레이아웃에서 MSW를 실행해도, 별도의 프로세스/컨텍스트에서 돌아가는 개별 페이지의 fetch 요청은 MSW의 영향권 밖에 있게 됨.
  • HMR 충돌: 코드가 수정되어 프로세스가 재생성될 때마다 MSW 서버를 다시 띄우려고 시도하게 되고, 이 과정에서 포트 충돌이 나거나 모킹 로직이 초기화되어 버림.

2. 해결방안

따라서, 대안으로 express 를 두고 서버 사이드 요청을 express에서 목킹하는 방식이 최선이라고 한다.
브라우저 -> Next.js -> Express -> 실제 API(or Mock)
이렇게 하면 Next.js 내부 프로세스 구조에 의존하지 않고, 안정적으로 서버사이드 요청을 제어할 수 있다.

3. 적용하기

1. msw 설치

pnpm install msw --save-dev

 

2. 서비스워커 초기화

npx msw init public/ --save
  • /public/mockServiceWorker.js 파일이 생성됨
    • 그러면 브라우저에 서비스워커가 설치됨
    • 이제 브라우저에서 실제 요청을 보낼 때 mockServiceWorker가 이를 가로채서, 미리 설정해놓은 응답을 내려줄 수 있음(mocking)
  • --save 옵션을 추가하여 package.json에 자동 등록되고 msw를 업데이트 할 때마다 해당 항목을 업데이트 함
{
  "name": "test",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint",
    "mock": "npx tsx watch ./src/mocks/http.ts"
  },
  "dependencies": {
    ...
  },
  "devDependencies": {
    ...
  },
  "msw": {
    "workerDirectory": "public" // 자동 생성됨
  }
}

(이후 목 서버 실행할 때 pnpm mock 명령어를 사용하기 위해 script를 추가하였음)

 

3. src/mocks 하위 디렉토리 세팅 파일 추가

3.1 browser.ts

브라우저에서 발생한 요청을 이 파일에서 가로채서 처리한다.

import { setupWorker } from 'msw/browser'
import { handlers } from './handlers'

// This configures a Service Worker with the given request handlers.
const worker = setupWorker(...handlers)

export default worker;

 

3.2 http.ts

서버에서 MSW 동작시키기 위해, 서버에서의 요청을 이 파일에서 모킹한다. 우선 패키지를 추가 설치하고,

pnpm i @mswjs/http-middleware express cors @types/express @types/cors --save-dev

(msw로 mock server를 만들기 위해 필요한 패키지)

import { createMiddleware } from "@mswjs/http-middleware";
import express from "express";
import cors from "cors";
import handlers from "./handler";

const app = express();
const port = 9090;

app.use(cors({ origin: "http://localhost:3000", optionsSuccessStatus: 200, credentials: true }));
app.use(express.json());
app.use(createMiddleware(...handlers));
app.listen(port, () => console.log(`Mock server is running on port: ${port}`));

 

3.3 handlers.ts

각 API 요청별 어떤 응답을 내려줄지 세팅하는 파일 예시

import { http, HttpResponse } from "msw";

const User = [
  { id: "mnmhbbb", nickname: "min-hee baek" },
  { id: "test", nickname: "테스트" },
];

export const handlers = [
  http.get("/api/users/:userId", ({ params }) => {
    const { userId } = params;
    const found = User.find(v => v.id === userId);
    if (found) {
      return HttpResponse.json(found);
    }
    return HttpResponse.json(
      { message: "no_such_user" },
      {
        status: 404,
      }
    );
  }),
  http.get("/api/list", () => {
    return HttpResponse.json([
      { id: 1, title: "안녕하세요?", count: 2323 },
      { id: 2, title: "안녕하세요?2", count: 2323 },
	  // ...
    ]);
  }),
];

export default handlers;

 

4. MSW 전용 컴포넌트 생성

클라이언트 환경에서만 MSW browser가 돌아가게 하기 위해 다음과 같이 최초 마운트 이후에 불러오는 컴포넌트를 만들고 
상황에 맞게 필요한 위치(ex. /app/layout.tsx)에 적용하면 된다.

"use client";
import { useEffect } from "react";

export default function MSWComponent() {
  useEffect(() => {
    if (typeof window !== "undefined") {
      if (process.env.NEXT_PUBLIC_API_MOCKING === "enabled") {
        import("@/mocks/browser")
      }
    }
  }, []);

  return null;
}

 

5. 환경변수 추가

NEXT_PUBLIC_API_MOCKING=enabled

 

코드 참조: 제로초님 next-app-router-z(msw)