MSW로 mock api 구현하기
회사에서 개발 중인 새 서비스의 데모 시연을 위해 현재 개발중인 백엔드 API
없이도 동작하도록 mock API
를 구현했다.
mock API
를 구현한 서비스의 프론트엔드는 다음과 같은 기술 스택으로 구성되어 있었다:
Next.js
v15.0.2
TypeScript
v5
mock API
를 위해 꼭 별도의 라이브러리를 설치해야 할까?
처음에는 JSON
파일 기반의 간단한 DB + Next.js
의 route handler
로 mock API
를 구현했다. 이런 방식으로 하니 초기 설정은 간편했는데, 개발하는 과정에서 몇 가지 불편한 점이 있었다.
-
매 요청마다 파일 읽기/쓰기가 발생한다. 예를 들어
POST
요청의 경우, 기존 데이터를 읽고, 수정한 다음, 다시 파일에 저장하는 과정이 필요하다. 데이터가 커질수록 이러한 I/O 작업으로 인한 메모리 사용량이 증가하여 성능 저하가 우려된다. -
API
응답으로 사용되는JSON
파일을 수정할 때마다Next.js
서버가 재시작되어 개발 흐름이 끊긴다. -
각 엔드포인트마다
api
디렉토리에 별도의 파일을 만들어야 하는 보일러플레이트가 발생한다. 결국 실제API
로 전환할 텐데, 이를 위한 임시 환경을 구축하는 데 너무 많은 수고가 든다.
MSW
알아보기
프론트엔드에서 mock API를 구현하는 법을 구글링하면 MSW
에 대한 글이 대부분이다. 그래서 나도 MSW
를 사용해보기로 했다.
MSW
는 Mock Service Worker
의 줄임말로, 실제 네트워크 요청을 가로채서 모의 응답을 보낼 수 있게끔 해주는 라이브러리다. 때문에 백엔드 API
가 준비되지 않았을 때, 개발환경에서 API
응답을 실제처럼 시뮬레이션할 수 있어 유용하다.
Service Worker
는 웹 브라우저에서 제공하는 Web API
다. MSW
는 브라우저 환경에서 발생하는 요청에 한해 Service Worker API
를 활용하며, Node.js
환경에서 발생하는 요청에 대해서는 또 다른 방식인 node-request-interceptor
를 활용하여 그 요청을 가로채게 된다.
MSW
로 회원가입 및 로그인 mock API
만들기
msw
를 설치한다.
npm install msw --save-dev
msw
를 초기화하고Service Worker
를 생성한다.
npx msw init public/ --save
위 명령어를 실행하고 나면 public
디렉토리에 mockServiceWorker.js
라는 파일이 생성된다.
mock API handler
와 설정파일을 생성한다.
mock
관련 파일들은 root
디렉토리에 mocks
폴더를 만들어서 관리했다. API 요청에 대한 모의 응답을 작성한 handler
파일들은 mocks/handlers
에 작성했다.
다음은 회원가입과 로그인을 위한 handler
들이다.
// mocks/handlers/auth.handlers.tsimport { HttpResponse, http } from "msw";const users = [];export const authHandlers = [http.post("/api/sign-up", async ({ request }) => {const newUser = await request.json();if (users.find((user) => user.username === newUser.email)) {return HttpResponse.json({message_code: 400,message: "이미 가입한 이메일 주소입니다.",data: null,},{ status: 400 });}const user = {id: crypto.randomUUID(),username: newUser.email,fullname: newUser.fullname,password: newUser.password,image: {id: Date.now(),image: newUser.image || "default-profile.jpg",created_at: new Date().toISOString(),},};users.push(user);return HttpResponse.json({message_code: 200,message: "회원가입이 성공적으로 완료되었습니다.",data: user,},{ status: 200 });}),http.post("/api/sign-in", async ({ request }) => {const { email, password } = await request.json();const user = users.find((u) => u.username === email && u.password === password);if (!user) {return HttpResponse.json({message_code: 401,message: "이메일 또는 비밀번호가 일치하지 않습니다.",data: null,},{ status: 401 });}return HttpResponse.json({message_code: 200,message: "성공적으로 로그인되었습니다.",data: user,},{ status: 200 });}),];
// mocks/handlers/index.tsimport { authHandlers } from "./auth.handlers";const handlers = [...authHandlers];export default handlers;
// mocks/browser.tsimport { setupWorker } from "msw/browser";import handlers from "./handlers";export const worker = setupWorker(...handlers);
// mocks/init.tsexport async function initMsw() {if (typeof window !== "undefined") {const { worker } = await import("./browser");await worker.start();}}
msw Provider
를 만들어준다.
// mocks/MSWComponent.tsx"use client";import { useEffect } from "react";export const MSWComponent = ({ children }: { children: React.ReactNode }) => {useEffect(() => {import("./index").then((res) => res.initMsw());}, []);return <>{children}</>;};
// utils/Providersimport { MSWComponent } from "@/mocks/MSWProvider";export function Providers({ children }: { children: React.ReactNode }) {return <MSWComponent>{children}</MSWComponent>;}
이렇게 하고 Next.js
서버를 실행하면 브라우저 개발도구의 콘솔창에 MSW
가 실행되었다는 메시지가 표시된다.

MSW
로 작성한 mock API
를 호출하는 방식은 실제 API
를 호출하는 방식과 동일하다. 예를 들어 회원가입 요청은 다음과 같이 하면 된다.
// components/SignUpForm.tsx...const handleSubmit: React.FormEventHandler = async (e) => {e.preventDefault()try {const response = await fetch('/api/sign-up', {method: 'POST',headers: {'Content-Type': 'application/json'},body: JSON.stringify({email: formData.email,password: formData.password})})const result = await response.json()if (!response.ok) {// TODO: 에러 처리}router.push('/sign-in')} catch (error) {// TODO: 에러 처리}}
mock API
가 실행되면 마찬가지로 브라우저 콘솔창에서 다음과 같이 메시지를 확인할 수 있다.

또한 네트워크창에서도 확인할 수 있다.

MSW
와 TypeScript
함께 사용하기
msw
공식문서에서 Using with TypeScript를 보면 REST api
를 TypeScript
와 함께 쓰는 방법이 잘 설명되어 있다. 문서를 보면, http
네임스페이스의 모든 요청 핸들러는 세 가지 제네릭 인자를 지원한다.
/*** Params: 요청 경로 매개변수* RequestBodyType: 요청 본문 타입* ResponseBodyType: 응답 본문 타입* Path: 경로*/http.get<Params, RequestBodyType, ResponseBodyType, Path>(path, resolver);
API
로부터 받게 되는 응답 데이터는 message_code
, message
, data
로 구성되고, 엔드포인트에 따라 data
의 타입만 달라지기 때문에, 다음과 같이 ApiResponse<T>
인터페이스를 작성해서 모든 엔드포인트의 응답 데이터 타입 선언에 활용했다.
// types/mock/auth.tsexport interface ApiResponse<T> {message_code: number;message: string;data: T | null;}
위에서 작성한 회원가입 및 로그인을 위한 핸들러에 TypeScript
를 적용한 결과는 다음과 같다.
// mocks/handlers/auth.handlers.tsimport { HttpResponse, http } from "msw";import {ApiResponse,SignInRequestBody,SignUpRequestBody,UserDetail,UserRegister,} from "@/types/mock/auth";const users: UserRegister[] = [];export const authHandlers = [http.post<never,SignUpRequestBody,ApiResponse<UserRegister>,"/api/sign-up">("/api/sign-up", async ({ request }) => {const newUser = await request.json();if (users.find((user) => user.username === newUser.email)) {return HttpResponse.json<ApiResponse<null>>({message_code: 400,message: "이미 가입한 이메일 주소입니다.",data: null,},{ status: 400 });}const user: UserRegister = {id: crypto.randomUUID(),username: newUser.email,fullname: newUser.fullname,password: newUser.password,image: {id: Date.now(),image: newUser.image || "default-profile.jpg",created_at: new Date().toISOString(),},};users.push(user);return HttpResponse.json<ApiResponse<UserRegister>>({message_code: 200,message: "회원가입이 성공적으로 완료되었습니다.",data: user,},{ status: 200 });}),http.post<never,SignInRequestBody,ApiResponse<UserRegister>,"/api/sign-in">("/api/sign-in", async ({ request }) => {const { email, password } = await request.json();const user = users.find((u) => u.username === email && u.password === password);if (!user) {return HttpResponse.json<ApiResponse<null>>({message_code: 401,message: "이메일 또는 비밀번호가 일치하지 않습니다.",data: null,},{ status: 401 });}return HttpResponse.json<ApiResponse<UserRegister>>({message_code: 200,message: "성공적으로 로그인되었습니다.",data: user,},{ status: 200 });}),];
mock API
에서 실제 API
로 갈아끼우기
msw
를 사용하면 실제 API
로의 전환이 매우 수월하다. msw
는 실제 API
와 동일한 엔드포인트를 사용하므로, API
호출을 위해 작성한 프론트엔드 코드를 전혀 수정할 필요가 없기 때문이다.
예를 들어, 회원가입을 위한 실제 API
를 구현할 때는 Next.js App Router
에서 아래와 같이 app/api/sign-up/route.ts
파일을 추가하기만 하면 된다:
// app/api/sign-up/route.tsexport async function POST(request: Request) {// 실제 API 로직 구현const data = await request.json();// 백엔드 서버로 요청 전달const response = await fetch("https://api.example.com/sign-up", {method: "POST",headers: {"Content-Type": "application/json",},body: JSON.stringify(data),});return response;}
개발 과정에서 실제 API
로 테스트해야 할 때는 msw
를 비활성화해야 한다. msw
의 활성화 여부를 쉽게 제어하기 위해 NEXT_PUBLIC_API_MOCKING
이라는 환경변수를 만들고, 이 값이 enabled
일 때만 msw
가 동작하도록 설정할 수 있다:
// mocks/init.tsexport async function initMsw() {if (process.env.NEXT_PUBLIC_API_MOCKING === "enabled") {if (typeof window !== "undefined") {const { worker } = await import("./browser");await worker.start();}}}