Next.js MSW 2 Mock API 만들기

··설명 약 7분·작성: 해비·블로그 소개

Next.js에서 MSW 2로 브라우저용 Mock API를 어떻게 준비하나요?

MSW 2는 브라우저에서 보낸 요청을 가로채 학습용 응답을 만들어 줍니다. worker.start()가 끝난 뒤 fetch를 실행해야 Mock 응답과 준비 실패를 구분할 수 있습니다.

적용 조건·범위: 브라우저 Service Worker가 가로채는 요청은 브라우저에서 보낸 fetch입니다. Server Component의 서버 fetch가 같은 Mock을 사용한다고 가정하지 않습니다. handler URL이 맞지 않은 요청은 이 예제의 bypass 설정에 따라 실제 서버로 넘어가므로 Mock 성공과 실제 HTTP 실패를 나누어 봅니다.

본문 근거: MSW 브라우저 통합 안내 · MSW

독립 실습: worker 준비 후에만 Mock 요청 보내기

기대 결과: worker 초기화 뒤 Mock 성공을 표시합니다. Network에 서비스 워커가 응답한 요청을 확인합니다. production에서는 개발 모드 전용입니다.

직접 실행: 환경 준비와 전체 파일

React 컴포넌트·TypeScript·async/await를 알고 시작합니다. Next.js 환경 준비의 루트 app/ TypeScript App Router 프로젝트를 별도 연습 폴더에서 사용합니다. 이 글의 기존 실습 기준은 Next.js 16.3.5이며 Cache Components는 끈 구성입니다. 생성된 app/layout.tsx는 유지합니다. 아래 파일은 TaskFlow·인증·DB 없이 실행하는 독립 예제입니다.

npm install -D msw, npx msw init public/ --save 후 npm run dev를 실행합니다. localhost /practice에서 버튼을 누릅니다. /api/practice-mock 실제 Route Handler는 만들지 않습니다.

app/practice/worker.ts 전체 코드

import { http, HttpResponse } from 'msw';
import { setupWorker } from 'msw/browser';
export const worker = setupWorker(
  http.get('/api/practice-mock', () => HttpResponse.json({ message: 'Mock 성공' })),
);

let startPromise: ReturnType<typeof worker.start> | undefined;

export function startMockWorker() {
  if (!startPromise) {
    startPromise = worker.start({ onUnhandledRequest: "bypass" }).catch((error) => {
      startPromise = undefined;
      throw error;
    });
  }
  return startPromise;
}

app/practice/page.tsx 전체 코드

"use client";
import { useState } from 'react';
export default function Page() {
  const [message, setMessage] = useState('대기');
  const [pending, setPending] = useState(false);
  async function load() {
    if (process.env.NODE_ENV !== 'development') { setMessage('개발 모드 전용'); return; }
    setPending(true);
    try {
      const { startMockWorker } = await import('./worker');
      await startMockWorker();
      const response = await fetch('/api/practice-mock');
      if (!response.ok) throw new Error('응답 오류');
      const data: { message: string } = await response.json();
      setMessage(data.message);
    } catch { setMessage('worker 또는 요청 실패'); }
    finally { setPending(false); }
  }
  return <main><button disabled={pending} onClick={load}>Mock 조회</button><p>{message}</p></main>;
}

자가 확인: handler 경로를 요청과 다르게 바꿨을 때 Mock과 실제 404를 구분하고 복구합니다.

설치

npm install -D msw
npx msw init public/ --save

handler 작성

import { http, HttpResponse } from 'msw';

export const handlers = [
  http.get('/api/tasks', () => {
    return HttpResponse.json([
      {
        id: 'task-1',
        title: 'MSW 공부하기',
        completed: false,
      },
    ]);
  }),
];

browser worker

초기화 Promise를 모듈에서 공유해 React StrictMode나 재마운트에서도 worker를 중복 시작하지 않습니다. 초기화가 실패하면 저장한 Promise를 비워 다시 시도할 수 있습니다.

import { setupWorker } from "msw/browser";
import { handlers } from "./handlers";
export const worker = setupWorker(...handlers);

let startPromise: ReturnType<typeof worker.start> | undefined;

export function startMockWorker() {
  if (!startPromise) {
    startPromise = worker.start({ onUnhandledRequest: "bypass" }).catch((error) => {
      startPromise = undefined;
      throw error;
    });
  }
  return startPromise;
}

개발 환경에서만 실행

Mock이 운영 환경에서 활성화되지 않도록 환경을 분리합니다.

NEXT_PUBLIC_API_MOCKING=enabled

자주 발생하는 오류

  • public/mockServiceWorker.js 생성 누락
  • handler URL과 실제 요청 URL 불일치
  • 운영 환경에서 MSW 활성화
  • 서버 요청과 브라우저 요청 경계를 구분하지 않음

브라우저 Service Worker가 가로채는 요청은 브라우저에서 보낸 fetch입니다. Server Component의 서버 fetch가 같은 Mock을 사용한다고 가정하지 않습니다. handler URL이 맞지 않은 요청은 이 예제의 bypass 설정에 따라 실제 서버로 넘어가므로 Mock 성공과 실제 HTTP 실패를 나누어 봅니다.

원리 확인: MSW 브라우저 통합 안내 (2026-10-12). 아래는 기대 결과이며 이 편집에서 실제 실행은 확인하지 않았습니다.

선택 실습: 할 일 앱에 적용하기

TaskFlow는 작업을 추가·완료·삭제하는 할 일 앱입니다. 이 선택 실습에서는 Next.js로 페이지와 서버 기능을 확장합니다.

MSW를 개발 모드에서만 시작하고 worker.start가 끝난 뒤 API 실습 UI를 렌더링합니다. 기본 서버 목록과 MSW의 브라우저 Mock 저장소는 별개입니다.

선택: 연재 프로젝트와 전체 자료

연재 프로젝트를 이어 만드는 분을 위한 자료입니다. 이 글의 개념과 예제만 찾으셨다면 필요한 파일과 실행 방법만 펼쳐보세요.

TaskFlow Next.js 프로젝트 적용

시작 체크포인트: 이전: Next.js Server Component 데이터 조회와 Streaming의 완료 상태에서 이어갑니다.

이전 회차 완료 ZIP으로 시작하기. 새 폴더에 풀어 현재 회차 완료본과 나란히 비교합니다.

이전 완료본과 달라진 파일: .env.example, app/(app)/tasks/page.tsx, app/ui/api-demo.tsx, app/ui/msw-provider.tsx, package.json, src/mocks/browser.ts, src/mocks/handlers.ts. 아래 변경 설명을 따라 수정하고 전체 코드의 같은 파일과 대조합니다.

독자가 직접 확인할 질문과 결과

질문: 백엔드 없이 /tasks에서 API 요청의 결과를 어떻게 확인할 수 있나요?

  1. npm install 후 npx msw init public/ --save로 worker를 준비하고 .env.local에 NEXT_PUBLIC_API_MOCKING=enabled를 설정한 뒤 개발 서버를 다시 시작합니다. /tasks의 API 실습 영역을 엽니다.
  2. ‘목록 API 조회’는 초기 Mock 기준 GET /api/tasks의 200: 1개 작업, ‘API 작업 추가’는 POST /api/tasks의 201: API 실습 작업을 상태 문구에 표시합니다. 다시 목록을 조회하면 Mock 목록 개수가 늘어납니다. 네트워크 탭에서도 경로·상태·JSON을 대조하세요.
  3. TaskFlow 서버 작업 목록은 별도 저장소입니다. Mock 추가가 그 목록에 자동 반영되지는 않습니다. 현재 handler는 GET/POST와 짧은 제목의 400 응답만 정의하며, 화면 버튼은 정상 제목만 전송합니다. 400은 개발자 도구에서 직접 짧은 제목을 보내야 확인할 수 있습니다. DELETE/500은 이 체크포인트에 구현되지 않았습니다.

오류 점검: .env.local 값, 서버 재시작, public/mockServiceWorker.js, 요청 경로를 확인합니다. 초기화 실패 메시지와 운영 모드의 Mock 비활성화를 구분하세요. 빌드·서버·브라우저 실행 검증은 미확인입니다.

완료 코드 ZIP 다운로드 — next-step-15-msw

이전 체크포인트: next-step-14-server-data

이번 회차의 변경

MSW를 개발 모드에서만 시작하고 worker.start가 끝난 뒤 API 실습 UI를 렌더링합니다. 기본 서버 목록과 MSW의 브라우저 Mock 저장소는 별개입니다. .env.local에서 NEXT_PUBLIC_API_MOCKING을 enabled로 설정하고 개발 서버를 재시작해야 Mock 요청이 동작합니다. 운영 build에서는 MSW가 켜지지 않습니다.

변경·추가 파일:

  • app/(app)/tasks/page.tsx
  • src/mocks/handlers.ts
  • src/mocks/browser.ts
  • app/ui/msw-provider.tsx
  • app/ui/api-demo.tsx
  • .env.example
전체 파일·코드 보기

실행과 검증

Node.js 22 LTS 이상 권장(Next.js 최소 20.9). 루트 app/ 구조, CSS 사용.

npm install
npm run dev
npm run build
npm run start

MSW: 개발 시에만 NEXT_PUBLIC_API_MOCKING을 enabled로 설정합니다. npx msw init public/ --save로 worker를 만들 수 있습니다. Mock 데이터와 서버 데이터는 별개이며 17편부터 기본 흐름은 실제 Route Handler입니다.

전체 코드와 ZIP은 같은 완료 체크포인트입니다. 자동 생성 worker는 ZIP에 포함하며 라이브러리 파일은 코드 뷰어에서 생략합니다.

완료 체크포인트: next-step-15-msw

실습 프로젝트 전체 코드

.gitignore

node_modules
.next
.env*
!.env.example
*.tsbuildinfo

.env.example

NEXT_PUBLIC_API_MOCKING=

app/(app)/@modal/(.)tasks/[id]/page.tsx

import { getTask } from "@/app/lib/data";
import { notFound } from "next/navigation";
import Link from "next/link";
import Modal from "@/app/ui/modal";
export default async function TaskModal({
  params,
}: {
  params: Promise<{ id: string }>;
}) {
  const { id } = await params;
  const task = await getTask(id);
  if (!task) notFound();
  return (
    <Modal>
      <h1>{task.title}</h1>
      <p>{task.description}</p>
      <p>상태: {task.completed ? "완료" : "진행 중"}</p>
    </Modal>
  );
}

app/(app)/@modal/[…catchAll]/page.tsx

export default function ResetModal() {
  return null;
}

app/(app)/@modal/default.tsx

export default function Default() {
  return null;
}

app/(app)/layout.tsx

import type { ReactNode } from "react";
import SiteHeader from "@/app/_components/SiteHeader";
export default async function AppLayout({
  children,
  modal,
}: {
  children: ReactNode;
  modal: ReactNode;
}) {
  return (
    <>
      <SiteHeader />
      {children}
      {modal}
    </>
  );
}

app/(app)/settings/page.tsx

import ThemeSetting from "@/app/ui/theme-setting";
export default function Settings() {
  return (
    <main>
      <h1>설정</h1>
      <ThemeSetting />
    </main>
  );
}

app/(app)/tasks/[id]/page.tsx

import { getTask } from "@/app/lib/data";
import { notFound } from "next/navigation";
import Link from "next/link";
export default async function TaskDetail({
  params,
}: {
  params: Promise<{ id: string }>;
}) {
  const { id } = await params;
  const task = await getTask(id);
  if (!task) notFound();
  return (
    <main>
      <h1>{task.title}</h1>
      <p>{task.description}</p>
      <p>상태: {task.completed ? "완료" : "진행 중"}</p>
      <Link href="/tasks">목록</Link>
    </main>
  );
}

app/(app)/tasks/error.tsx

"use client";
export default function ErrorPage({
  reset,
}: {
  error: Error;
  reset: () => void;
}) {
  return (
    <main>
      <h1>작업을 불러오지 못했습니다.</h1>
      <button onClick={reset}>다시 시도</button>
    </main>
  );
}

app/(app)/tasks/loading.tsx

export default function Loading() {
  return (
    <main>
      <p>작업을 불러오는 중...</p>
    </main>
  );
}

app/(app)/tasks/not-found.tsx

export default function NotFound() {
  return (
    <main>
      <h1>작업을 찾을 수 없습니다.</h1>
    </main>
  );
}

app/(app)/tasks/page.tsx

import Link from "next/link";
import { getTasks } from "@/app/lib/data";
import TaskSearch from "@/app/ui/task-search";
import { Suspense } from "react";
import TaskStats from "@/app/ui/task-stats";
import ApiDemo from "@/app/ui/api-demo";
import MswProvider from "@/app/ui/msw-provider";
export default async function TasksPage() {
  const tasks = await getTasks();
  return (
    <main>
      <h1>작업 목록</h1>
      <TaskSearch />
      <ul>
        {tasks.map((task) => (
          <li key={task.id}>
            <Link href={"/tasks/" + task.id}>{task.title}</Link>
            <span> · {task.completed ? "완료" : "진행 중"}</span>
          </li>
        ))}
      </ul>
      <Suspense fallback={<p>통계 로딩 중...</p>}>
        <TaskStats />
      </Suspense>
      <MswProvider>
        <ApiDemo />
      </MswProvider>
    </main>
  );
}

app/(app)/template.tsx

import type { ReactNode } from "react";
export default function Template({ children }: { children: ReactNode }) {
  return <>{children}</>;
}

app/(public)/page.tsx

import Link from "next/link";
export default function HomePage() {
  return (
    <main>
      <h1>TaskFlow Next.js</h1>
      <p>App Router 기반 작업 관리 서비스입니다.</p>
      <Link href="/tasks">작업 목록 보기</Link>
    </main>
  );
}

app/_components/SiteHeader.tsx

import Link from "next/link";
export default function SiteHeader() {
  return (
    <header>
      <nav aria-label="주 메뉴">
        <Link href="/">홈</Link>
        <Link href="/tasks">작업</Link>
        <Link href="/settings">설정</Link>
      </nav>
    </header>
  );
}

app/globals.css

* {
  box-sizing: border-box;
}
body {
  margin: 0;
  color: #171717;
  background: #fafafa;
  font-family: Arial, sans-serif;
  line-height: 1.6;
}
header,
main {
  width: min(880px, calc(100% - 32px));
  margin: 24px auto;
}
nav {
  display: flex;
  flex-wrap: wrap;
  gap: 16px;
  align-items: center;
}
a {
  color: inherit;
  text-underline-offset: 3px;
}
button,
input,
select {
  min-height: 44px;
  padding: 8px 12px;
  font: inherit;
  border: 1px solid #aaa;
  background: #fff;
  color: #171717;
  max-width: 100%;
}
button {
  cursor: pointer;
}
button:disabled {
  opacity: 0.6;
  cursor: wait;
}
input {
  display: block;
  width: 100%;
}
label {
  display: block;
}
form,
section,
aside {
  margin: 20px 0;
}
ul {
  padding: 0;
  list-style: none;
}
li {
  padding: 12px;
  margin: 12px 0;
  border: 1px solid #ccc;
  overflow-wrap: anywhere;
}
.controls {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  align-items: center;
}
.controls form {
  margin: 0;
}
.modal {
  width: min(640px, calc(100% - 32px));
  border: 1px solid #777;
  padding: 24px;
  background: #fff;
  color: #171717;
}
.modal::backdrop {
  background: rgb(0 0 0 / 55%);
}
:focus-visible {
  outline: 3px solid #555;
  outline-offset: 3px;
}
[data-theme="dark"] {
  background: #202020;
  color: #fafafa;
  padding: 16px;
}
[data-density="compact"] li {
  padding: 8px;
  margin: 8px 0;
}
img {
  max-width: 100%;
  height: auto;
}

app/layout.tsx

import type { Metadata } from "next";
import type { ReactNode } from "react";
import "./globals.css";
export const metadata: Metadata = {
  title: { default: "TaskFlow Next.js", template: "%s | TaskFlow" },
  description: "서버와 브라우저 역할을 배우는 TaskFlow 학습 프로젝트",
};
export default function RootLayout({ children }: { children: ReactNode }) {
  return (
    <html lang="ko">
      <body>{children}</body>
    </html>
  );
}

app/lib/data.ts

import type { Task } from "./types";
const tasks: Task[] = [
  {
    id: "task-1",
    title: "App Router 공부",
    description: "서버와 브라우저의 역할을 구분합니다.",
    completed: false,
    ownerId: "demo",
  },
  {
    id: "task-2",
    title: "TaskFlow 만들기",
    description: "누적 실습을 완성합니다.",
    completed: true,
    ownerId: "demo",
  },
];
export async function getTasks() {
  return tasks;
}
export async function getTask(id: string) {
  return tasks.find((t) => t.id === id);
}
export async function getTaskStats() {
  const ts = await getTasks();
  return { total: ts.length, completed: ts.filter((t) => t.completed).length };
}

app/lib/types.ts

export type Task = {
  id: string;
  title: string;
  description: string;
  completed: boolean;
  ownerId: string;
};

app/ui/api-demo.tsx

"use client";
import { useState } from "react";
export default function ApiDemo() {
  const [message, setMessage] = useState("API 응답 확인 전");
  const [busy, setBusy] = useState(false);
  async function request(method: "GET" | "POST") {
    setBusy(true);
    try {
      const response = await fetch("/api/tasks", {
        method,
        headers: { "Content-Type": "application/json" },
        ...(method === "POST"
          ? { body: JSON.stringify({ title: "API 실습 작업" }) }
          : {}),
      });
      const data = await response.json();
      setMessage(
        response.ok
          ? `${response.status}: ${Array.isArray(data) ? data.length + "개 작업" : data.title}`
          : `${response.status}: ${data.message}`,
      );
    } catch {
      setMessage("API 요청 실패");
    } finally {
      setBusy(false);
    }
  }
  return (
    <section>
      <h2>API 실습</h2>
      <p>
        Mock 저장소와 서버 저장소는 별개입니다. 17편부터 실제 Route Handler를
        사용합니다.
      </p>
      <div className="controls">
        <button disabled={busy} onClick={() => request("GET")}>
          목록 API 조회
        </button>
        <button disabled={busy} onClick={() => request("POST")}>
          API 작업 추가
        </button>
      </div>
      <p role="status">{message}</p>
    </section>
  );
}

app/ui/modal.tsx

"use client";
import { useEffect, useRef, type ReactNode } from "react";
import { useRouter } from "next/navigation";
export default function Modal({ children }: { children: ReactNode }) {
  const ref = useRef<HTMLDialogElement>(null);
  const router = useRouter();
  useEffect(() => {
    const dialog = ref.current;
    if (dialog && !dialog.open) dialog.showModal();
    return () => {
      dialog?.close();
    };
  }, []);
  return (
    <dialog
      className="modal"
      ref={ref}
      aria-label="작업 상세"
      onCancel={(event) => {
        event.preventDefault();
        router.back();
      }}
    >
      <button type="button" onClick={() => router.back()}>
        닫기
      </button>
      {children}
    </dialog>
  );
}

app/ui/msw-provider.tsx

"use client";
import { useState, useEffect, type ReactNode } from "react";
export default function MswProvider({ children }: { children: ReactNode }) {
  const enabled =
    process.env.NODE_ENV === "development" &&
    process.env.NEXT_PUBLIC_API_MOCKING === "enabled";
  const [ready, setReady] = useState(!enabled);
  const [error, setError] = useState("");
  useEffect(() => {
    if (!enabled) return;
    let active = true;
    import("@/src/mocks/browser")
      .then(({ startMockWorker }) => startMockWorker())
      .then(() => {
        if (active) setReady(true);
      })
      .catch(() => {
        if (active)
          setError("Mock API 초기화에 실패했습니다. worker 파일을 확인하세요.");
      });
    return () => {
      active = false;
    };
  }, [enabled]);
  return error ? (
    <p role="alert">{error}</p>
  ) : ready ? (
    <>{children}</>
  ) : (
    <p>Mock API 준비 중...</p>
  );
}

app/ui/task-search.tsx

"use client";
import { useState } from "react";
export default function TaskSearch() {
  const [keyword, setKeyword] = useState("");
  return (
    <label>
      작업 검색
      <input
        value={keyword}
        onChange={(event) => setKeyword(event.target.value)}
        placeholder="검색어 입력"
      />
    </label>
  );
}

app/ui/task-stats.tsx

import { getTaskStats } from "@/app/lib/data";
export default async function TaskStats() {
  await new Promise((resolve) => setTimeout(resolve, 600));
  const s = await getTaskStats();
  return (
    <aside aria-label="작업 통계">
      <p>
        전체: {s.total} · 완료: {s.completed}
      </p>
    </aside>
  );
}

app/ui/theme-setting.tsx

"use client";
import { useState, useEffect } from "react";
export default function ThemeSetting() {
  const [theme, setTheme] = useState<"light" | "dark">("light");
  useEffect(() => {
    try {
      const saved = localStorage.getItem("taskflow-theme");
      if (saved === "light" || saved === "dark") setTheme(saved);
    } catch {}
  }, []);
  function change(value: "light" | "dark") {
    setTheme(value);
    try {
      localStorage.setItem("taskflow-theme", value);
    } catch {}
  }
  return (
    <section data-theme={theme}>
      <p>현재 테마: {theme}</p>
      <div className="controls">
        <button onClick={() => change("light")}>밝게</button>
        <button onClick={() => change("dark")}>어둡게</button>
      </div>
    </section>
  );
}

next-env.d.ts

/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/types/routes.d.ts";
import "./.next/types/root-params.d.ts";


// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

next.config.ts

import type { NextConfig } from "next";
const config: NextConfig = { cacheComponents: false };
export default config;

package.json

{
  "name": "taskflow-next",
  "version": "1.0.15",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build --webpack",
    "start": "next start",
    "typecheck": "tsc --noEmit"
  },
  "dependencies": {
    "next": "16.3.5",
    "react": "19.3.0",
    "react-dom": "19.3.0"
  },
  "devDependencies": {
    "typescript": "5.9.3",
    "@types/node": "24.13.6",
    "@types/react": "19.3.0",
    "@types/react-dom": "19.3.0",
    "msw": "2.15.0"
  }
}

src/mocks/browser.ts

import { setupWorker } from "msw/browser";
import { handlers } from "./handlers";
export const worker = setupWorker(...handlers);

let startPromise: ReturnType<typeof worker.start> | undefined;

export function startMockWorker() {
  if (!startPromise) {
    startPromise = worker.start({ onUnhandledRequest: "bypass" }).catch((error) => {
      startPromise = undefined;
      throw error;
    });
  }
  return startPromise;
}

src/mocks/handlers.ts

import { http, HttpResponse } from "msw";
let tasks = [
  {
    id: "task-1",
    title: "Mock API 연결",
    completed: false,
  },
];
export const handlers = [
  http.get("/api/tasks", () => {
    return HttpResponse.json(tasks);
  }),
  http.post("/api/tasks", async ({ request }) => {
    const body = (await request.json()) as {
      title?: string;
    };
    if (typeof body.title !== "string" || body.title.trim().length < 2) {
      return HttpResponse.json(
        {
          message: "제목을 입력해 주세요.",
        },
        {
          status: 400,
        },
      );
    }
    const newTask = {
      id: crypto.randomUUID(),
      title: body.title.trim(),
      completed: false,
    };
    tasks = [newTask, ...tasks];
    return HttpResponse.json(newTask, {
      status: 201,
    });
  }),
];

tsconfig.json

{
  "compilerOptions": {
    "target": "ES2020",
    "lib": [
      "dom",
      "dom.iterable",
      "esnext"
    ],
    "allowJs": true,
    "skipLibCheck": true,
    "strict": true,
    "noEmit": true,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "bundler",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "react-jsx",
    "incremental": true,
    "plugins": [
      {
        "name": "next"
      }
    ],
    "paths": {
      "@/*": [
        "./*"
      ]
    }
  },
  "include": [
    "next-env.d.ts",
    "**/*.ts",
    "**/*.tsx",
    ".next/types/**/*.ts",
    ".next/dev/types/**/*.ts"
  ],
  "exclude": [
    "node_modules"
  ]
}

글 소개·학습 준비·전체 목차

Next.js MSW 2 Mock API 만들기 학습 표지

Next.js · 15/38 · TaskFlow 누적 실습

Next.js MSW 2 Mock API 만들기

Next.js · 15 / 38 · 필수 실무 · MSW 2

백엔드 없이 실제 fetch 흐름으로 API UI를 시험하려면 어떻게 할까요? MSW 2 handler가 브라우저 요청을 가로채 응답을 만들게 하고 worker.start()가 끝난 뒤 요청합니다. 아래 한 버튼 예제로 Mock 성공과 worker·요청 실패를 구분한 뒤, 하단에서 GET·POST·400 응답을 TaskFlow에 연결합니다.

이번 글에서 배우는 내용

  • MSW 설치
  • Service Worker 초기화
  • handler
  • GET/POST Mock API
  • 성공·400 응답과 미구현 오류 범위 구분
  • 환경변수로 mock 활성화

목차

다음 학습

다음 글에서는 브라우저에서 외부 API를 요청해야 하는 경우 Axios를 처음 설치하고 서버 fetch와 역할을 구분합니다.

공식 문서

이 글이 도움이 되었나요?

조회 중

읽음 기록

읽음 — / 전체 39개 · 필수 38개

이 글을 쓴 해비

웹사이트를 제작·운영하고, 개인 서비스와 자동화·AI 도구를 만듭니다. 직접 만들며 배운 내용과 문제 해결 과정을 BlogFlow에 기록합니다.

새 글 받아보기

RSS 리더에서 BlogFlow의 새 글을 확인할 수 있습니다.

RSS 피드 구독하기

댓글 남기기