프론트에서 MSW로 백엔드보다 빨리 개발 시작하기

프론트에서 MSW로 백엔드보다 빨리 개발 시작하기

요약
MSW를 활용해서 백엔드 API가 구축되지 않은 상황에서 개발을 시작해보자
작성일
Feb 28, 2025
태그
MSW
Mock
HTTP

프론트엔드 개발을 능동적으로

프론트엔드 개발에서 백엔드가 준비되기를 기다리는 시간은 종종 개발 일정에 부담을 주기도 합니다. 백엔드 API가 완전히 구현되기 전에 프론트엔드 개발을 시작하는 방법을 찾는다면 그 시간을 단축시킬 수 있을 것입니다. 이번에는 MSW(Mock Service Workder)를 활용하여 API가 구축되기 전에 모의 서버를 만들어 보겠습니다.
💡
Json-server, MirageJS, POST MAN 등을 활용해 Mock서버 구축이 가능하지만 MSW는 Service Worker를 활용해 브라우저의 네트워크 요청을 직접 가로채기 때문에 실제와 유사한 환경을 제공하여 MSW를 선택하였습니다.
 

MSW란

MSW(Mock Service Worker)는 네트워크 요청을 가로채고 개발자가 정의한 Mock 데이터를 반환하는 라이브러리입니다. 이 라이브러리는 실제 API 서버와 통신하는 것처럼 동작하지만 백엔드 개발 없이도 프론트엔드 개발을 진행할 수 있게 도와줍니다.
 

MSW 설치 및 설정

먼저 아래 명령어를 통해 MSW를 설치하고 Service Worker를 생성합니다.
Service Worker는 네트워크 요청을 가로채는 역할을 수행하여 MSW를 활용하기 위해서 꼭 필요합니다.
npm install msw --save-dev npx msw init /public --save ## 생성된 서비스워커 📦public ┗ 📜mockServiceWorker.js
 

API Handler 생성

이제 핸들러를 만들어 모킹할 API를 생성하면 됩니다. 먼저 아래와 같이 핸들러를 생성하고 브라우저에서 MSW를 사용하기 위해 setupWorker 를 사용하여 서비스 워커를 설정합니다.
// src/mocks/handlers.ts import { rest } from 'msw'; // 핸들러 정의 export const handlers = [ // GET 요청에 대한 응답 정의 rest.get('/api/user', (req, res, ctx) => { return res( ctx.status(200), // 상태 코드 ctx.json({ id: 1, name: 'John Doe' }) // 반환할 JSON 데이터 ); }), ];
// src/mocks/browser.ts import { setupWorker } from 'msw'; import { handlers } from './handlers'; // 서비스 워커 설정 export const worker = setupWorker(...handlers);
 
이제 MSW 핸들러와 브라우저 설정을 프로젝트에 적용하려면 src/index.js 또는 src/App.js에서 worker.start()를 호출하여 MSW를 활성화해야 합니다. 운영환경에서는 보통 사용하지 않기 때문에 개발 환경에서만 MSW가 활성화되도록 설정합니다.
import React from 'react'; import { worker } from './mocks/browser'; // 개발 환경에서만 MSW 활성화 if (process.env.NODE_ENV === 'development') { worker.start(); } function App() { // API 요청 예시 const fetchUser = async () => { const response = await fetch('/api/user'); const data = await response.json(); console.log(data); }; return ( <div className="App"> <h1>MSW 예시</h1> <button onClick={fetchUser}>사용자 정보 가져오기</button> </div> ); } export default App;
이제 개발환경으로 프로젝트를 실행해보면 아래와 같이 브라우저 콘솔창에서 확인이 가능합니다.
notion image
 

번외 : msw-auto-mock을 활용해서 핸들러 자동생성하기

MSW를 사용하여 API를 모킹하는 것은 초기에는 매우 유용하지만, API가 많아지고 백엔드의 API를 그대로 MSW로 구현하려면 꾸준한 업데이트와 수정이 필요하여 불편함이 발생할 수 있습니다. 특히, 백엔드 API가 자주 변경되거나 새로운 API가 추가되는 경우, 그에 맞춰 MSW 핸들러를 계속 작성해야 하므로 시간이 많이 소요되고 관리가 복잡해질 수 있습니다.
이를 해결하기 위해, 자동으로 핸들러를 생성할 수 있는 방법을 고민해봤고, 그 과정에서 msw-auto-mock이라는 라이브러리를 발견하게 되었습니다. 이 라이브러리를 사용하면 백엔드 API 스펙에 맞춰 MSW 핸들러를 자동으로 생성할 수 있어, 매번 수동으로 핸들러를 작성하지 않아도 됩니다.
 

MSW 핸들러 수동 작성의 문제점

MSW를 처음 사용할 때, 핸들러를 수동으로 작성하는 것은 상대적으로 간단하고 직관적입니다. 예를 들어, 특정 엔드포인트에 대한 요청을 가로채고 가짜 데이터를 반환하는 방식으로 진행합니다.
그러나 백엔드 API가 많아지고, API 스펙이 자주 변경되면서 핸들러를 계속 수정해야 하는 일이 발생합니다. 이 과정에서 다음과 같은 불편함을 느낄 수 있습니다:
  • API 스펙 변경 시 핸들러 수정: 백엔드에서 API의 요청/응답 구조가 변경되면, 이를 반영하기 위해 MSW 핸들러를 계속 수정해야 합니다.
  • 많은 엔드포인트 처리: API 엔드포인트가 많아지면 핸들러 코드도 많아지는데, 이를 관리하는 데 어려움이 생깁니다.
  • 코드 중복: 유사한 구조를 가진 여러 API에 대해 핸들러를 작성하다 보면 코드 중복이 발생하고, 유지보수성도 떨어집니다.

자동으로 핸들러 생성하는 방법

msw-auto-mock은 백엔드 API 스펙을 기반으로 자동으로 MSW 핸들러를 생성해주는 라이브러리입니다. 이 라이브러리를 사용하면 수동으로 핸들러를 작성할 필요 없이, API 스펙만 제공하면 자동으로 핸들러를 만들어주는 기능을 제공합니다. 이를 통해, API가 많거나 자주 변경되는 프로젝트에서 핸들러 관리의 부담을 크게 줄일 수 있습니다.
💡
msw-auto-mock은 Swagger, OpenAPI, GraphQL 스펙 파일을 기반으로 핸들러를 생성합니다. 이 스펙 파일들은 API의 요청 및 응답 구조를 정의하는 데 사용됩니다.
 

msw-auto-mock 사용법

msw-auto-mock을 사용하면 API 스펙 파일을 기반으로 핸들러를 자동으로 생성할 수 있습니다. 이 과정에서 데이터도 자동으로 생성할 수 있습니다. 추가로 Faker.jsAI를 활용하여 Mock 데이터를 채울 수 있는 기능도 제공합니다.
"scripts": { "generate:msw": "npx msw-auto-mock openapi/api-spec.yaml -o src/mocks" }

생성된 hadler 예시

/** * This file is AUTO GENERATED by [msw-auto-mock](https://github.com/zoubingwu/msw-auto-mock) * Feel free to commit/edit it as you need. */ /* eslint-disable */ /* tslint:disable */ import { HttpResponse, http } from "msw"; import { faker } from "@faker-js/faker"; faker.seed(1); const baseURL = ""; const MAX_ARRAY_LENGTH = 20; let i = 0; const next = () => { if (i === Number.MAX_SAFE_INTEGER - 1) { i = 0; } return i++; }; export const handlers = [ http.post(`${baseURL}/api/auth/login`, async () => { const resultArray = [ [await getPostApiAuthLogin200Response(), { status: 200 }], [await getPostApiAuthLogin401Response(), { status: 401 }], ]; return HttpResponse.json(...resultArray[next() % resultArray.length]); }), http.get(`${baseURL}/api/auth/profile`, async () => { const resultArray = [ [await getGetApiAuthProfile200Response(), { status: 200 }], [undefined, { status: 401 }], ]; return HttpResponse.json(...resultArray[next() % resultArray.length]); }), http.get(`${baseURL}/api/users`, async () => { const resultArray = [[await getGetApiUsers200Response(), { status: 200 }]]; return HttpResponse.json(...resultArray[next() % resultArray.length]); }), http.post(`${baseURL}/api/users`, async () => { const resultArray = [[await getPostApiUsers201Response(), { status: 201 }]]; return HttpResponse.json(...resultArray[next() % resultArray.length]); }), http.get(`${baseURL}/api/users/:id`, async () => { const resultArray = [ [await getGetApiUsersId200Response(), { status: 200 }], [undefined, { status: 404 }], ]; return HttpResponse.json(...resultArray[next() % resultArray.length]); }), ]; export function getPostApiAuthLogin200Response() { return { user: { id: faker.number.int(), name: faker.person.fullName(), email: faker.internet.email(), }, token: faker.lorem.words(), }; } export function getPostApiAuthLogin401Response() { return { message: faker.lorem.words(), }; } export function getGetApiAuthProfile200Response() { return { id: faker.number.int(), name: faker.person.fullName(), email: faker.internet.email(), phone: faker.lorem.words(), address: faker.lorem.words(), }; } export function getGetApiUsers200Response() { return [ ...new Array(faker.number.int({ min: 1, max: MAX_ARRAY_LENGTH })).keys(), ].map((_) => ({ id: faker.number.int(), name: faker.person.fullName(), email: faker.internet.email(), })); } export function getPostApiUsers201Response() { return { id: faker.number.int(), name: faker.person.fullName(), email: faker.internet.email(), }; } export function getGetApiUsersId200Response() { return { id: faker.number.int(), name: faker.person.fullName(), email: faker.internet.email(), phone: faker.lorem.words(), address: faker.lorem.words(), }; }

참고문헌