개인의 기록
  • 소개
  • 프로젝트
  • 글
  • 링크

© 2026 newgirok

← 글 목록

MSW — 브라우저와 Node에서 API를 Mocking하는 라이브러리

2026년 4월 19일
테스트API MockingMSW

MSW(Mock Service Worker)는 Service Worker를 활용해 네트워크 요청 자체를 가로채 가짜 응답을 반환합니다.

Service Worker: 브라우저와 네트워크 사이에 위치하는 스크립트 — 요청을 프록시처럼 가로채거나 캐시를 제어할 수 있습니다

기존 API Mock 방식들은 fetch나 axios 같은 클라이언트 라이브러리 자체를 Mock으로 교체했습니다. MSW는 그보다 낮은 레이어인 네트워크 수준에서 동작하므로, 어떤 HTTP 클라이언트를 사용하든 동일하게 동작합니다. 애플리케이션 코드를 수정하지 않아도 됩니다.

npm install -D msw

핵심 개념

1. Handler

Handler는 특정 URL 패턴과 HTTP 메서드에 대한 응답을 정의하는 함수입니다. "이 URL로 이 메서드 요청이 오면, 이 응답을 반환한다"를 선언합니다.

// src/mocks/handlers.ts
import { http, HttpResponse } from "msw";

export const handlers = [
  // GET /api/users → 유저 목록 반환
  http.get("/api/users", () => {
    return HttpResponse.json([
      { id: 1, name: "Alice", email: "alice@example.com" },
      { id: 2, name: "Bob", email: "bob@example.com" },
    ]);
  }),

  // GET /api/users/:id → 특정 유저 반환
  http.get("/api/users/:id", ({ params }) => {
    const { id } = params;
    return HttpResponse.json({ id: Number(id), name: "Alice" });
  }),

  // POST /api/users → 유저 생성
  http.post("/api/users", async ({ request }) => {
    const body = await request.json();
    return HttpResponse.json(
      { id: 3, ...body },
      { status: 201 }
    );
  }),
];

Handler는 배열로 묶어 브라우저와 Node 환경 모두에서 재사용합니다. 동일한 Handler를 개발 서버와 테스트 양쪽에 쓸 수 있는 것이 MSW의 큰 장점입니다.


2. Http

http는 GET, POST, PUT, DELETE 등 HTTP 메서드별 Handler를 정의하는 헬퍼 객체입니다. msw 패키지에서 named export로 제공됩니다.

import { http } from "msw";

// 지원하는 메서드
http.get(path, resolver)
http.post(path, resolver)
http.put(path, resolver)
http.patch(path, resolver)
http.delete(path, resolver)
http.head(path, resolver)
http.options(path, resolver)

// 메서드 무관하게 모든 요청을 가로챔
http.all(path, resolver)

경로는 문자열 또는 정규식으로 지정할 수 있습니다.

// 정확한 경로
http.get("/api/users", resolver)

// 경로 파라미터
http.get("/api/users/:id", resolver)

// 정규식 (URL에 /api/가 포함된 모든 GET 요청)
http.get(/\/api\//, resolver)

// 절대 URL (외부 API 목킹)
http.get("https://api.example.com/data", resolver)

3. Browser Worker

브라우저 환경에서는 Service Worker가 네트워크 요청을 가로챕니다. 개발 서버 실행 중에도 실제 API 없이 UI를 개발할 수 있습니다.

# public 디렉토리에 Service Worker 파일 생성
npx msw init public/ --save
// src/mocks/browser.ts
import { setupWorker } from "msw/browser";
import { handlers } from "./handlers";

export const worker = setupWorker(...handlers);
// src/main.tsx (개발 환경에서만 활성화)
async function enableMocking() {
  if (process.env.NODE_ENV !== "development") return;

  const { worker } = await import("./mocks/browser");
  return worker.start({
    onUnhandledRequest: "warn", // 핸들러 없는 요청은 경고 출력
  });
}

enableMocking().then(() => {
  ReactDOM.createRoot(document.getElementById("root")!).render(<App />);
});
Browser 동작 방식

  React App
      |
      | fetch("/api/users")
      v
  Service Worker       <-- msw가 여기서 요청을 가로챔
      |
      | Handler 매칭
      v
  MockResponse         <-- 실제 서버 없이 응답 반환
      |
      v
  React App (응답 수신)

브라우저 DevTools의 Network 탭에서 요청이 (Service Worker) 출처로 표시되므로, 실제 네트워크 요청과 구분할 수 있습니다.


4. Node Server

테스트 환경(Vitest, Jest)에서는 Service Worker를 사용할 수 없습니다. 대신 Node.js용 서버를 설정합니다. Node 환경에서는 undici를 활용해 fetch를 인터셉트합니다.

// src/mocks/server.ts
import { setupServer } from "msw/node";
import { handlers } from "./handlers";

export const server = setupServer(...handlers);
// src/test/setup.ts
import { server } from "../mocks/server";
import "@testing-library/jest-dom";

// 테스트 시작 전 서버 활성화
beforeAll(() => server.listen({ onUnhandledRequest: "error" }));

// 각 테스트 후 핸들러를 초기 상태로 리셋 (런타임 오버라이드 제거)
afterEach(() => server.resetHandlers());

// 모든 테스트 완료 후 서버 종료
afterAll(() => server.close());

테스트 파일에서 특정 케이스만 다른 응답을 반환하도록 핸들러를 런타임에 추가할 수 있습니다.

test("서버 에러 시 에러 메시지를 표시한다", async () => {
  // 이 테스트에서만 에러 응답을 반환
  server.use(
    http.get("/api/users", () => {
      return HttpResponse.json(
        { message: "Internal Server Error" },
        { status: 500 }
      );
    })
  );

  render(<UserList />);
  expect(await screen.findByText("서버 오류가 발생했습니다.")).toBeInTheDocument();
});
// afterEach의 server.resetHandlers()로 이 오버라이드는 자동 제거됨

5. Response Resolver

Response Resolver는 Handler가 요청을 받았을 때 실행되는 함수입니다. 요청 정보를 읽고 HttpResponse로 응답을 반환합니다.

Resolver는 세 가지 인자를 구조 분해해서 사용합니다.

http.get("/api/posts/:id", async ({ request, params, cookies }) => {
  // request: 원래 Request 객체 (URL, 헤더, 본문 등)
  // params:  경로 파라미터 ({ id: "42" })
  // cookies: 요청 쿠키
});

HttpResponse 사용법:

import { http, HttpResponse } from "msw";

export const handlers = [
  // JSON 응답
  http.get("/api/data", () => {
    return HttpResponse.json({ message: "success" });
  }),

  // 상태 코드와 헤더 지정
  http.post("/api/login", async ({ request }) => {
    const { email, password } = await request.json();

    if (password !== "correct") {
      return HttpResponse.json(
        { message: "Unauthorized" },
        { status: 401 }
      );
    }

    return HttpResponse.json(
      { token: "mock-token-abc123" },
      {
        status: 200,
        headers: { "Set-Cookie": "session=mock; Path=/" },
      }
    );
  }),

  // 텍스트 응답
  http.get("/api/health", () => {
    return new HttpResponse("OK", { status: 200 });
  }),

  // 네트워크 에러 시뮬레이션
  http.get("/api/unstable", () => {
    return HttpResponse.error();
  }),

  // 응답 지연 (로딩 상태 테스트)
  http.get("/api/slow", async () => {
    await new Promise((resolve) => setTimeout(resolve, 2000));
    return HttpResponse.json({ data: "늦게 도착한 데이터" });
  }),
];

요청 본문을 읽는 방법은 Content-Type에 따라 다릅니다.

http.post("/api/upload", async ({ request }) => {
  // JSON 본문
  const json = await request.json();

  // 폼 데이터
  const formData = await request.formData();
  const file = formData.get("file");

  // 텍스트
  const text = await request.text();

  return HttpResponse.json({ received: true });
});
← 이전 글React Philosophy — React가 해결하려는 문제
다음 글 →Prettier — 코드 스타일을 자동으로 맞추는 포맷터