[FE] MSW로 API 모킹하기

wldud·2026년 3월 6일

FE

목록 보기
13/14

슬픈 프론트엔드 개발 과정..

대부분의 개발은 프론트와 백엔드가 동시에 개발을 시작한다.
문제는 프론트가 백엔드 API가 있어야 연동을 시작할 수 있다는 것…

UI 먼저 만들게요~ -> API 나오면 붙일게요! -> ...API 아직임
-> 있어도 스펙 바뀜 or 서버 쥬금ㅠㅠ or 내가 필요한 에러 케이스 재현이 어려움

그래서 프론트는 연동을 못 해서 멈추거나, 임시로 const mockData = […] 같은 걸로 UI를 만든다.

그래서 그냥 mock data 만들어서 하면 되는거 아님??

mock data 만들어서 그냥 UI에 때려박으면 당연히 만들 수는 있다. 근데 시간이 지나면 문제점이 생긴다.

  1. 실제 API 붙일 때 또 수정

    • paging/필터/정렬이 붙으면 구조가 완전히 달라짐
    • 처음에 mock data로만 만들면, 나중에 “API 호출/상태 관리 로직”을 통째로 추가
      • 로딩 처리

      • 에러 처리

      • refetch 타이밍, staleTime 등등

        ⇒ UI는 만들었는데 데이터 패칭 구조를 다시 구현해야함

    const mockUsers = [
      { id: 1, name: "연희" },
      { id: 2, name: "혁수" },
    ];
    
    export function UserList() {
      return (
        <ul>
          {mockUsers.map((u) => (
            <li key={u.id}>{u.name}</li>
          ))}
        </ul>
      );
    }
    //  API 연동하면서 로직 추가
    import { useQuery } from "@tanstack/react-query";
    import axios from "axios";
    
    async function fetchUsers() {
      const res = await axios.get("/api/users");
      return res.data;
    }
    
    export function UserList() {
      const { data, isLoading, error, refetch } = useQuery({
        queryKey: ["users"],
        queryFn: fetchUsers,
        retry: 2,
      });
    
      if (isLoading) return <div>로딩중...</div>;
      if (error)
        return (
          <div>
            에러 발생 <button onClick={() => refetch()}>다시 시도</button>
          </div>
        );
    
      return (
        <ul>
          {data.map((u: any) => (
            <li key={u.id}>{u.name}</li>
          ))}
        </ul>
      );
    }
  2. 테스트/QA에서 재현이 어려움

    • 401, 500, 2초 지연, 빈 데이터 같은 케이스를 테스트 해보기 어려움

그래서 API mocking을 사용하면 편하다!!!

API Mocking은 “진짜 서버 대신, 서버가 줄 것 같은 응답(API 명세서 기반)을 가짜로 만들어서 API 연동 흐름을 그대로 개발하는 것”

단순히 데이터를 넣는 것이 아니라, GET /api/users 요청을 보내면 → 서버가 있는 것처럼 로딩도 있고, 실패도 가능하고, 지연도 넣을 수 있고, 응답 형태도 실제 API와 맞춰서 내려줌.

즉, 프론트는 서버가 없어도 연동 개발을 할 수 있음!!!!!!

API Mocking 도구

요청을 어디서 낚아채는지에 따라서 분류됨.

  1. 테스트 도구가 가로채기 (E2E/통합 테스트)
    • ex: Playwright route.fulfill, Cypress intercept
    • 테스트 실행기가 브라우저 옆에서 요청 잡아서 응답 주는 방식
    • 언제 씀?
      • 로그인부터 결제까지 “사용자 시나리오”를 통째로 검증하는 E2E 테스트
      • UI보다는 테스트에 최적화
  2. Node에서 가로채기
    • 브라우저가 아니라 Node 환경에서 HTTP 요청을 낚음
    • 예를 들어 백엔드 코드가 외부 API(결제, 문자 등)를 호출하는데 테스트에서 매번 진짜 외부 API를 부르기 부담스러울때 사용
    • SSR 또는 Node에서 돌아가는 모듈 테스트
    • 프론트 개발용이랑은 다름 (그냥 이런것도 있다~)
  3. 프론트 코드 레벨에서 가로채기 (axios/fetch 전용)
    • ex: axios-mock-adapter, fetch-mock
    • 네트워크를 낚는게 아니라 axios나 fetch 호출을 ‘함수 레벨’에서 바꿔치기
    • 언제 씀?
      • 특정 프로젝트가 axios로 고정이고, 간단히 모킹만 필요할 때
      • 단위 테스트에서 함수 호출만 가볍게 모킹하고 싶을 때
    • axios 붙이면 axios 전용 → 나중에 fetch로 바꾸거나, 다른 클라이언트 쓰면 모킹도 다시 짜야 함
    • 진짜 네트워크 요청처럼 동작시키는 느낌이 약함
  4. 네트워크 레벨에서 가로채기
    • ex: MSW(Mock Service Worker) → 솔직히..swagger에서 json api 응답 복사해서 ai 한테..
    • 요청이 실제로 나가려는 순간을 잡아서, 서버가 있는 것처럼 응답을 만들어줌
    • 언제 쓰면 좋음?
      • 서버가 없을 때 UI/연동 개발을 진짜처럼 하고 싶을 때
      • React Query 같은 데이터 패칭 흐름을 처음부터 설계하고 싶을 때
      • 에러/지연 같은 케이스를 개발 단계에서 반복 재현하고 싶을 때

MSW 장점

서버가 꺼져도 UI 개선 가능

사실 프로젝트 끝나고 서버 꺼버리면 현실적으로 프론트는 코드 개선하기가 쉽지가 않다..ㅠㅠ 하지만 MSW를 작성해두면, mock data로 UI 확인할 수 있기 때문에 UI나 코드 개선 가능!

  • 서버 불안정해도 프론트 기능 검증 가능
  • 에러 UX 같은 걸 서버 없이도 반복 재현 가능
  • 로딩 2초 걸릴 때 Skeleton 괜찮은가? 등 UX를 마음대로 테스트 가능

MSW 사용 후 개발 흐름의 변화

사용 전

  • 서버 나올 때까지 기다림
  • 아니면 컴포넌트에 mock data 사용
  • 나중에 API 붙일 때 또 수정

사용 후

  • 처음부터 useQuery() + API 호출 코드 작성
  • 서버 대신 MSW가 응답
  • 백엔드 API가 준비 됨 → MSW 끄면 됨!

MSW 동작 원리

브라우저에서 MSW를 하나 등록한다. 이 SW는 브라우저 안에서 “프록시”처럼 동작하면서 페이지에서 나가는 네트워크 요청을 가로챌 수 있음

흐름:

  1. 프론트 코드가 axios.get(”/api/users”) 호출
  2. 브라우저는 네트워크로 요청을 내보내려 함
  3. MSW의 Service Worker가 중간에서 그 요청을 intercept
  4. 등록해둔 handler랑 매칭되면 → 가짜 응답을 즉시 반환
  5. 매칭 안 되면 → 설정에 따라
    • 그냥 실제 서버로 보냄
    • 경고 띄움
    • 에러 냄

MSW 사용법

  1. 설치
pnpm add msw
  1. Servicce Worker 파일 생성
npx msw init public /--save

이거 하면 public/mockServiceWorker.js가 생김. 이 파일이 있어야 브라우저에서 SW가 뜸.

// 폴더구조
src/
  mocks/
    handlers.ts
    browser.ts
  main.tsx
  1. 브라우저에서 MSW 켜기
//src/mocks/browser.ts

import { setupWorker } from "msw/browser";
import { handlers } from "./handlers";

export const worker = setupWorker(...handlers);
// src/main.tsx

import React from "react";
import ReactDOM from "react-dom/client";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import App from "./App";

async function enableMocking() {
  if (import.meta.env.MODE !== "development") return;
  const { worker } = await import("./mocks/browser");
  await worker.start({
  onUnhandledRequest: "warn", // 핸들러 없을 때 처리
});
}

const queryClient = new QueryClient();

enableMocking().then(() => {
  ReactDOM.createRoot(document.getElementById("root")!).render(
    <React.StrictMode>
      <QueryClientProvider client={queryClient}>
        <App />
      </QueryClientProvider>
    </React.StrictMode>
  );
});
  1. 핸들러 작성 예시
// src/mocks/handlers.ts
import { http, HttpResponse, delay } from "msw";

export const handlers = [
  http.get("/api/users", async () => {
    await delay(400); // 로딩 UX 보이게 일부러 지연
    return HttpResponse.json([
      { id: 1, name: "연희" },
      { id: 2, name: "혁수" },
    ]);
  }),
];

실제로 API 호출하는 컴포넌트

// src/App.tsx
import { useQuery } from "@tanstack/react-query";

type User = { id: number; name: string };

async function fetchUsers(): Promise<User[]> {
  const res = await fetch("/api/users");
  if (!res.ok) throw new Error("Failed to fetch users");
  return res.json();
}

export default function App() {
  const { data, isLoading, isError, error, refetch } = useQuery({
    queryKey: ["users"],
    queryFn: fetchUsers,
    retry: 1,
  });

  if (isLoading) return <div>로딩중...</div>;

  if (isError)
    return (
      <div>
        에러: {(error as Error).message}
        <button onClick={() => refetch()}>재시도</button>
      </div>
    );

  return (
    <div>
      <h1>Users</h1>
      <ul>
        {data!.map((u) => (
          <li key={u.id}>
            {u.id}. {u.name}
          </li>
        ))}
      </ul>
    </div>
  );
}

0개의 댓글