로잇 프로젝트를 개발하면서 원래는 API 개발이 늦어져도 UI만 먼저 간단히 작업하면 된다고 생각했지만, 회원가입 페이지가 3단계로 나뉘면서 상태 관리해야 할 요소가 많아져 코드 작성에 시간이 걸릴 것으로 예상됐습니다.
API가 완성될 때까지 기다리면, 프로젝트 막바지에 급하게 코드를 작성하는 일이 발생할 가능성이 높아 보였고, 이를 방지하기 위해 백엔드를 흉내내 Mocking API를 요청을 하는 방식으로 개발을 진행하기로 했습니다.
처음에는 JSON Server를 사용해 간단한 API 응답을 만들려고 했지만, 실제 서비스처럼 에러 처리(예: 모달 표시, 에러 메시지 출력)까지 구현 하기에는 한계가 있었습니다.
그래서 해결책을 찾던 중, Nock과 MSW(Mock Service Worker)를 발견했습니다.
결과적으로 향후 개발할 WebSocket API 기능까지 지원하고, 브라우저 환경에서도 API 요청을 처리할 수 있는 MSW를 선택했습니다.
출처: https://velog.io/@khy226/msw로-모의-서버-만들기
MSW는 API 요청을 가로채서 미리 정의된 응답을 반환하는 도구입니다.
브라우저 환경에서는 Service Worker(SW)를 이용해 네트워크 요청을 중간에서 가로채고, Node.js 환경에서는 Request Interceptor를 활용하여 요청을 처리합니다.
위 사진을 통해 설명하면
- (요청 발생)
- 사용자가 fetch 또는 axios 같은 HTTP 요청을 보냄
- 원래는 이 요청이 실제 백엔드 서버로 가야 함
- (MSW의 서비스 워커가 요청을 가로챔)
- 브라우저에서 Service Worker로 동작
- 프론트엔드에서 보내는 API 요청을 네트워크 단계에서 가로채서 Mock 응답을 반환함
- 실제 서버와 통신하지 않고, 미리 정의된 핸들러를 기반으로 응답을 제공
- (요청 핸들러에서 요청을 확인)
- http.get(), http.post() 등의 핸들러를 설정하여 요청 URL과 메서드에 따라 특정 응답을 반환
- 예를 들어, /api/user에 대한 GET 요청이 오면 { name: "Haeun Kim" } 같은 응답을 줄 수 있음
- (요청에 맞는 응답을 반환)
- 요청이 핸들러 목록과 매칭되면, 사전에 정의된 응답을 찾음
- 브라우저는 실제 서버에서 응답을 받은 것처럼 처리함
- (클라이언트가 응답을 받음)
- 최종적으로 브라우저는 MSW가 반환한 Mock 응답을 실제 서버 응답처럼 받게 됨
- 이 과정에서 HTTP 상태 코드(200, 400, 500 등)와 JSON 데이터를 반환할 수 있음
- 예를 들어, HttpResponse.json({ userId: 1, name: "Haeun Kim" }, { status: 200 }) 같은 응답이 반환됨
- 따라서 백엔드 개발 없이도 프론트엔드 개발과 테스트가 가능해짐
공식 문서를 참고해서 Next.js 프로젝트에서 MSW를 설정하고 사용하는 방법을 정리했습니다.
단계 | 브라우저 환경 | Node.js 환경 (SSR, Jest) |
---|---|---|
1. 초기화 | setupWorker().start()로 서비스 워커 등록 | setupServer().listen()으로 요청 인터셉트 |
2. 요청 발생 | 브라우저에서 API 요청 (fetch, axios) | 서버 코드에서 API 요청 (getServerSideProps, test 등) |
3. 요청 가로채기 | 서비스 워커가 요청을 가로채고 핸들러를 확인 | HTTP 인터셉터가 요청을 가로채고 핸들러를 확인 |
4. Mock 응답 반환 | 미리 정의된 응답을 반환 | 테스트 코드에서 미리 정의된 응답을 반환 |
npm install msw --save-dev
Next.js에서는 public 폴더 내 정적 파일을 브라우저에서 직접 접근할 수 있기 때문에, MSW의 서비스 워커 파일을 public 폴더에 생성해야 합니다.
npx msw init public --save
생성된 파일
📂 public
┗ 📜 mockServiceWorker.js ✅ (MSW 서비스 워커 파일)
서비스 워커 등록 과정
API 요청을 가로채고 응답을 반환하는 핸들러를 작성합니다. 이 단계는 일단 틀만 만들어놓고 세부 내용은 가장 나중에 작성했습니다.
// mocks/handlers.js
import { http, HttpResponse } from "msw";
async function checkUserExists(email) {
const dummyUserDatabase = ["test@example.com", "user@domain.com"];
return dummyUserDatabase.includes(email);
}
export const handlers = [
http.post("/api/send-email-code", async ({ request }) => {
const { email } = await request.json();
if (!email) {
return HttpResponse.json(
{ error: "이메일이 누락되었습니다." },
{ status: 400 },
);
}
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
return HttpResponse.json(
{ error: "잘못된 이메일 형식입니다." },
{ status: 400 },
);
}
const existingUser = await checkUserExists(email);
if (existingUser) {
return HttpResponse.json(
{ error: "이미 가입된 이메일입니다." },
{ status: 409 },
);
}
return HttpResponse.json(
{ message: "인증 코드가 전송되었습니다." },
{ status: 200 },
);
}),
// 중략
];
Next.js에서는 브라우저에서 worker를 실행하고, 서버에서 server를 실행해야 합니다.
// mocks/browser.ts
import { setupWorker } from 'msw/browser'
import { handlers } from './handlers'
export const worker = setupWorker(...handlers)
// mocks/server.ts
import { setupServer } from "msw/node";
import { handlers } from "./handlers";
export const server = setupServer(...handlers);
Next.js에서 MSW를 자동으로 실행하기 위한 파일을 생성합니다.
// mocks/index.ts
export async function initMsw() {
if (typeof window === "undefined") {
// 서버에서 실행
const { server } = await import("./server");
server.listen();
} else {
// 클라이언트에서 실행
const { worker } = await import("./browser");
await worker.start();
}
}
MSW를 Next.js에서 사용하려면 app.tsx 또는 providers.tsx에서 실행해야 합니다.
if (process.env.NODE_ENV === "development") {
import("../mocks").then(({ initMsw }) => initMsw());
}