[테스트] MSW 기본개념

Woonil·2026년 1월 30일

테스트

목록 보기
3/4

개발을 하다 보면 백엔드 API가 완성되지 않았거나, 테스트 환경에서 네트워크 의존성을 제거해야 하는 순간이 온다. 나의 경우 모킹을 구현한다고 하면 예전에는 json-server , axios-mock-adapter 를 순차적으로 사용했던 것 같다. 하지만 해당 라이브러리들의 한계는 뚜렷했다.

  • json-server의 한계
    • 별도 프로세스 관리: 애플리케이션 외에 별도의 로컬 서버를 띄우고 관리해야 합니다. 팀원들과 환경을 맞추려면 포트 설정부터 DB 파일(db.json) 공유까지 신경 쓸 게 늘어난다.
    • 엔드포인트 불일치: 실제 API 주소(예: api.service.com)와 로컬 서버 주소(localhost:3000)가 다르기 때문에, 코드 내에 환경별로 URL을 교체하는 로직을 넣어야 한다.
    • 복잡한 비즈니스 로직 구현의 어려움: 단순 CRUD는 쉽지만, 특정 헤더에 따른 응답 변경, 정교한 에러 시뮬레이션, 동적인 상태 변화를 구현하려면 결국 별도의 백엔드 코딩을 하는 것과 다름없다.

하지만 "네트워크 단에서 직접 요청을 가로채는" 더욱 효율적인 라이브러리인 MSW를 사용하면 이러한 한계들을 쉽게 극복할 수 있다.

비교 항목json-serveraxios-mock-adapter MSW
작동 위치별도 로컬 서버애플리케이션 내부 (라이브러리 단)브라우저 네트워크 레벨 (Service Worker)
코드 오염낮음 (서버 분리)높음매우 낮음 (완전 분리)
디버깅네트워크 탭 확인 가능확인 불가실제 요청처럼 확인 가능
환경 제약포트/서버 관리 필요Axios 라이브러리에 의존환경/라이브러리 무관

MSW(Mock Service Worker)는 브라우저와 Node.js 환경 모두를 지원하는 API 모킹 라이브러리로, 단순한 가짜 데이터를 넘어서는 '독립적인 네트워크 계층'을 구축하게 해준다. 이름에서 알 수 있듯 서비스 워커를 통해 실제 API 요청을 가로채고 미리 정의된 응답을 반환한다. 이를 통해 빠르고 안정적인 테스트 환경을 구축할 수 있다. 브라우저의 경우 서비스 워커를 사용하고, Node.js 환경에서는 내부적으로 XHR, fetch 등의 요청을 가로채는 인터셉터를 사용한다.

공식문서에 나와있는 MSW의 주요 특징은 다음과 같다.

  • 주요 특징
    • 완벽한 독립성 (Agnostic)
      • 프레임워크, 도구, 환경에 얽매이지 않는다. React, Vue, Angular에 더해 순수 자바스크립트 환경에서도 동일하게 작동한다.
      • window.fetch 같은 네이티브 API뿐만 아니라 Axios, React Query, Apollo 등 어떤 클라이언트를 사용하더라도 추가 설정 없이 즉시 통합된다.
    • 끊김 없는 매끄러움 (Seamless)
      • 브라우저: 실제 서비스 워커(Service Worker) API를 활용해 네트워크 수준에서 요청을 가로챈다. 애플리케이션 코드를 수정하거나 fetch를 오염시키지 않는 '플랫폼 기반'의 접근 방식을 지향한다.
      • Node.js: 표준 인터셉트 수단이 없는 노드 환경에서도 모듈 패칭이 아닌 클래스 확장(Class Extension) 방식을 사용한다. 덕분에 테스트 환경을 실제 운영 환경과 최대한 유사하게 유지할 수 있다.
    • 높은 재사용성 (Reusable)
      • MSW는 API 모킹을 하나의 독립된 레이어로 취급한다. 한 번 작성한 모킹 로직은 개발 단계, 통합 테스트, E2E 테스트, 그리고 Storybook이나 라이브 데모에서도 그대로 재사용할 수 있다.
      • 환경마다 모킹 전략을 새로 짤 필요 없이, '단일 진실 공급원(Single Source of Truth)'으로서 네트워크 동작을 관리할 수 있다.

🤔개념

브라우저 환경에서 모킹

React와 같이 브라우저 환경에서 모킹이 필요한 경우, 서비스 워커 등록이 필요하다. msw에서는 서비스 워커 관련 스크립트(mockServiceWorker.js)를 제공한다. 이는 애플리케이션 바깥으로 나가는 네트워크 트래픽을 가로채는 역할을 수행한다.

환경 구성

  1. mockServiceWorker.js 등록

    npx msw init <프로젝트의 public 디렉터리의 위치> --save
  2. 브라우저용 워커 세팅

    // _mocks/browser.ts
    import { setupWorker } from "msw/browser";
    import { roomHandlers } from "./handlers/room";
    
    export const worker = setupWorker(...roomHandlers);
  3. 핸들러 정의: 프로젝트에서 사용할 핸들러를 정의한다. http에 대한 응답 등이 될 수 있다.

    // 예시: _mocks/handlers/room.ts
    import { http, HttpResponse } from "msw";
    
    export const roomHandlers = [
      http.post("/api/v1/rooms", () => {
        return HttpResponse.json(
          {
            timeStamp: "2026-01-29T14:23:45.123+09:00",
            // ...
          },
          { status: 201 },
        );
      }),
    ];

    모킹 로직을 각 테스트 케이스나 컴포넌트 내부에 흩어놓는 것이 아니라, 실제 API 라우터 구조와 유사하게 /mocks/handlers 폴더 내에 중앙화하여 관리하였다. 이렇게 하면, 추후 API 명세가 변경되었을 때 다른 코드의 수정 없이 중앙의 핸들러 파일만 수정하면 모든 테스트와 개발 환경에 변경 사항이 일괄적으로 반영된다.

  4. 워커 시작 비동기 함수 정의

    export async function enableMocking() {
      if (import.meta.env.MODE !== "development") {
        return;
      }
    
      const { worker } = await import("./browser"); // browser.ts가 위치한 상대 경로
    
      // 서비스 워커가 준비되어 요청을 가로칠 준비가 완료되면,
      // worker.start()는 resolve된 Promise를 반환함.
      return worker.start();
    }
  5. 앱 진입점에 적용

    import { createRoot } from "react-dom/client";
    import Router from "./routes";
    import { enableMocking } from "./_mocks";
    
    enableMocking().then(() => {
      createRoot(document.getElementById("root")!).render(
         <Router />
      );
    });

결과

앱 구동 시 개발자 도구 콘솔 탭을 확인하면, 아래와 같이 모킹이 성공적으로 세팅됨을 확인할 수 있다.

모킹한 api로 요청을 보내고 네트워크 탭을 확인하면, 서비스 워커가 성공적으로 요청을 가로챈 것을 확인할 수 있다. 콘솔 탭에서도 응답을 확인할 수 있다.

HTTP 응답 모킹하기

네트워크 에러

테스트 환경에서 꼭 정상적인 응답이 오란 법은 없다. 예를 들어, 일시적인 네트워크 에러 등 예측할 수 없는 에러에 대해서도 올바른 UI가 표시되는지 확인해야 하는 경우도 생긴다. MSW는 Response.error() 를 제공하여 이런 상황을 모킹할 수 있게 지원한다.

http.get('/resource', () => {
  return HttpResponse.error()
})
  • 적용 예시
    • DNS 에러
    • 커넥션 타임아웃
    • 오프라인 상태의 클라이언트

WebSocket 모킹하기


import { ws } from "msw";

const WS_URL = "ws://localhost:15674/ws-test";

const chat = ws.link(WS_URL);

export const wsHandlers = [
  chat.addEventListener("connection", ({ client }) => {
    // 연결 실패 테스트 위한 플래그
    const SHOULD_FAIL = false;

    if (SHOULD_FAIL) {
      throw new Error("Connection failed");
    }

    client.addEventListener("message", event => {
      console.log("Intercepted message from the client", event);
      const message = event.data;
    });
    
    client.send("hello from server!"); // 텍스트
    client.send(new Blob(["hello world"], { type: "text/plain" })); // Blob
    client.send(new TextEncoder().encode("hello world")); // ArrayBuffer
    
    client.addEventListener("close", event => {
      console.log("Client is closing the connection", client);
    });
  }),
];

테스트 환경과의 통합

setupServer()

통합 테스트 시, setup과 teardown을 사용해 테스트 실행 전과 후에 API를 모킹하고 해제하는 작업을 수행한다.

// setupTests.js
import { setupServer } from 'msw/node';

import { handlers } from '@/__mocks__/handlers';

/* msw */
export const server = setupServer(...handlers);

beforeAll(() => {
  server.listen();
});

afterEach(() => {
  server.resetHandlers(); // 런타임에 변경한 MSW의 모킹을 초기화
});

afterAll(() => {
  server.close();
});

resetHandlers()

한편 프로젝트 내에서 공유하는 단일 인스턴스를 변경했으니, 이를 원래의 핸들러로 복구해줘야 한다. 이를 위해서는 server.resetHandlers() 를 Teardown에서 호출해줘야 한다. 즉, 초기화 작업을 통해 일관된 모킹 환경으로 안정성 있는 테스트 작성이 가능하게 된다.

import { setupServer } from 'msw/node';

import { handlers } from '@/__mocks__/handlers';

export const server = setupServer(...handlers);

afterEach(() => {
  server.resetHandlers();
  vi.clearAllMocks();
});

아래와 같이 새로운 핸들러를 제공하는 것도 가능하다.

const worker = setupWorker(http.get('/resource', resolver))
worker.use(http.post('/user', resolver))
 
worker.resetHandlers(http.patch('/book/:bookId', resolver))
// 런타임 "POST /user"와 초기 "GET /resource" 모두 제거되고 
// "PATCH /book/:bookId" 요청 핸들러만 유효함.

use()

특정 API에 대해 상황에 따라 다른 모킹을 적용하려면 어떻게 해야 할까? 기존 핸들러를 바꾸지 못하는 상황에서는 특정 테스트 환경 내에서만 이를 동적으로 변경할 수 있으면 좋을 것 같다.

다행히도 msw에서는 use 라는 함수를 통해서 이런 기능을 제공한다. 이때, 초기에 구동을 위해 설정한 msw 서버 인스턴스와 동일한 인스턴스를 사용해야 기존에 모킹된 API의 응답을 변경할 수 있다.

import { http } from 'msw'
import { setupServer } from 'msw/node'
 
const server = setupServer()

// 요청 핸들러를 현재 서버 인스턴스에 추가 
server.use(http.get('/resource', resolver), http.post('/resource', resolver))
// handlers.js
// 사용자 정보가 없는 비로그인 상태
rest.get(`/user`, (req, res, ctx) => {
  return res(ctx.status(200), ctx.json(null));
}),
// 사용자 정보가 있는 로그인 상태를 테스트 해야 하는 상황
beforeEach(() => {
  // 기존 handlers 응답을 use 함수 내의 응답을 기준으로 테스트를 실행
  server.use(
    rest.get('/user', (_, res, ctx) => {
      return res(
        ctx.status(200),
        ctx.json({
          id: 1,
          email: 'maria@mail.com',
          name: 'Maria',
          password: '12345',
        }),
      );
    }),
  );
});
profile
프론트 개발과 클라우드 환경에 관심이 많습니다:)

0개의 댓글