
이번 포스팅에선 Mock API, MSW가 무엇인지, 현재 프로젝트에선 어떤 Mock 방식이 어울리는지, Mock API를 사용하기 위한 MSW 세팅 과정에 대해 포스팅 하려 한다.
우선 Mock이 뭘까?
우리가 핸드폰 매장에서 진열된 핸드폰을 본적이 한번쯤은 있을거라 생각한다.
핸드폰의 기종이 적혀있는 화면을 가지고 있는 핸드폰들
이것이 Mock이다. 즉, 어떠한 제품 또는 무언가의 모조품을 의미한다.
그럼 같은 의미로 Mock API는 무엇일까?
실제 API 서버가 준비되지 않았거나 테스트 환경에서 사용할 가짜 API
현재 나의 프로젝트는 프론트엔드와 백엔드가 동시에 진행중이고, 아직 API서버가 나오지 않아 Mock API 도입의 필요성을 느꼈다.
Mock API의 필요성을 정리하면 아래와 같이 할 수 있다.
백엔드와 분리된 프론트엔드 개발:
테스트 환경 구축:
네트워크 독립성:
파일로 데이터를 정의하고, 이를 프로젝트에서 바로 사용하는 방식
export const mockProducts = [
{ id: 1, name: 'Product A', price: 100 },
{ id: 2, name: 'Product B', price: 200 },
];
장점:
단점:
JSON을 기반으로 API 서버을 생성하여 사용하는 방식
{
"products": [
{ "id": 1, "name": "Product A", "price": 100 },
{ "id": 2, "name": "Product B", "price": 200 }
]
}
JSON Server 실행
json-server --watch db.json
products 엔드포인트에서 데이터 제공
GET http://localhost:3000/products
장점:
단점:
네트워크 요청을 가로채서 동적으로 응답을 생성하는 도구이다.
import { rest } from 'msw';
export const handlers = [
rest.get('/api/products', (req, res, ctx) => {
return res(
ctx.status(200),
ctx.json([
{ id: 1, name: 'Product A', price: 100 },
{ id: 2, name: 'Product B', price: 200 },
])
);
}),
];
장점:
단점:
내 프로젝트는 많은 API를 사용하고, 무한 스크롤링 및 여러 카테고리 필터링, 장바구니 기능, 테스틑 코드 작성 등 여러가지 방면에서 대응할수 있어야 한다고 생각했다.
여러 파라미터, 헤더등에 대해 동적으로 처리할 수 있고, 이에 대해 시뮬레이션 및 테스트 할 수 있는 MSW를 채택했다.
MSW(Mock Service Worker)는 네트워크 요청을 가로채고, 이를 기반으로 가짜 응답(Mock Response)을 제공하는 라이브러리이다. 주로 브라우저 환경과 Node.js 기반의 테스트 환경에서 사용한다.
브라우저 환경:
- 브라우저의 Service Worker를 이용해 네트워크 요청을 인터셉트.
- Fetch 또는 XHR 요청을 가로채 응답을 반환.
Node.js 환경:
- 서버 환경에서 HTTP 요청을 인터셉트.
- 테스트 코드에서 Mock 응답을 반환.

이제 MSW 기반 Mock API를 내 프로젝트에 적용해보려 한다.
yarn add msw
우선 msw를 설치해주자.
npx msw init ./public
MSW는 mockServiceWorker.js 파일을 사용하여 브라우저의 네트워크 요청을 처리한다.
이 파일은 위 명령어를 통해 생성할 수 있다.
이제 설치한 MSW 라이브러리를 뚝딱뚝딱 만져야한다.
우선, Mock API 핸들러를 작성해보자.
위를 보면 msw 라이브러리가 2.0으로 업데이트 되었다.
참고할만한 블로그 레퍼런스들은 전부 이전 버전이라 공식홈페이지를 참고하여 작성했다.
// src/mocks/handlers.ts
import { http, HttpResponse } from 'msw';
export const handler = [
http.get('/api/products', () => {
return HttpResponse.json([
{ id: 1, name: 'Product A', price: 100 },
{ id: 2, name: 'Product B', price: 200 },
]);
}),
];
우선 간단하게 작성해보았다.
2.0 변경점을 간단히 말하자면, rest대신 http가 사용되고, 콜백함수 인자로 res, req 등 대신 request 하나에 다 온다고 한다.
일단 간단하게 GET만 사용하여 데이터가 잘 오는지 테스트를 해보려고 한다.
브라우저 환경에서 MSW를 사용하도록 초기화도 하자.
// src/mocks/browser.ts
import { setupWorker } from 'msw/browser';
import { handler } from './handler';
export const worker = setupWorker(...handler);
작성한 핸들러를 worker로 setup하여 준비시킨다.
// src/mocks/server.ts
import { setupServer } from 'msw/node';
import { handler } from './handler';
export const server = setupServer(...handler);
server 코드도 작성하여 handler를 연결하였다.
이제 리액트에 MSW를 연결하여 브라우저에서 네트워크를 중간에 가져오도록 해보자.
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import App from './App';
import { worker } from './mocks/browser';
function prepare() {
if (process.env.NODE_ENV === 'development') {
return worker.start();
}
return Promise.resolve();
}
prepare().then(() => {
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>
);
});
위는 main.tsx에 prepare 함수를 추가해주었고, worker가 제대로 준비되면 컴포넌트를 렌더링 하도록 Promise 방식을 사용하였다.
간단하게 ProductList.tsx 파일을 만들어 handler에 작성한 파일이 제대로 수신이 되는지 확인하려고 했다.
import React, { useEffect, useState } from 'react';
import axios from 'axios';
interface Product {
id: number;
name: string;
price: number;
}
const ProductList: React.FC = () => {
const [products, setProducts] = useState<Product[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [clickedMessage, setClickedMessage] = useState('');
useEffect(() => {
const fetchProducts = async () => {
try {
const response = await axios.get('/api/products');
setProducts(response.data);
} catch (error) {
console.error('Error fetching products:', error);
} finally {
setIsLoading(false);
}
};
fetchProducts();
}, []);
const handleClick = (productName: string) => {
setClickedMessage(`${productName} clicked!`);
};
if (isLoading) {
return <p>Loading...</p>;
}
return (
<div>
<h1>Product List</h1>
<ul>
{products.map((product) => (
<li key={product.id} onClick={() => handleClick(product.name)}>
{product.name}: ${product.price}
</li>
))}
</ul>
{clickedMessage && <p>{clickedMessage}</p>}
</div>
);
};
export default ProductList;
간단하게 로딩 상태도 추가하였고, 데이터가 나오면 리스트로 뿌려주도록 하여 잘 작동하는지 확인하려고 했다.

데이터가 깔끔하게 잘 오는걸 확인할 수 있었다.
간단하게 컴포넌트에 대한 테스트도 작성했다.
데이터가 제대로 오는건지 확실히 확인하고 싶었고, 테스트의 초록 화면을 봐야 마음놓고 깃허브에 올릴 수 있을거 같았다.
import { render } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import ProductList from '../ProductList';
import { expect, test } from 'vitest';
test('renders product list from Mock API', async () => {
const { findByText } = render(<ProductList />);
// Mock 데이터 확인
const productA = await findByText(/Product A: \$100/);
const productB = await findByText(/Product B: \$200/);
expect(productA).toBeInTheDocument();
expect(productB).toBeInTheDocument();
});
test('allows user to click a product item', async () => {
const { findByText } = render(<ProductList />);
// Mock 데이터 확인
const productA = await findByText(/Product A: \$100/);
expect(productA).toBeInTheDocument();
// 유저가 Product A를 클릭
await userEvent.click(productA);
// 클릭 후 상태 확인 (예: alert, state 업데이트)
expect(await findByText(/Product A clicked!/)).toBeInTheDocument(); // 예시
});
간단하게 데이터 확인 및 클릭 확인을 하려고 했다.
GPT 고마워요

언제봐도 기분 좋은 초록색 글자가 나를 반겨주었다.
문제: 처음 요청 시 Mock 데이터가 아닌 index.html이 반환.
원인: React 애플리케이션이 MSW 초기화 전에 실행되어 요청이 가로채지 못함.

Mock API의 응답 데이터가 제대로 반환되지 않아 index.html로 반환되었다.
오류 발생할때의 코드
//main.tsx
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import App from './App';
import { worker } from './mocks/browser';
if (process.env.NODE_ENV === 'development') {
worker.start();
}
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>
);
App컴포넌트가 worker 실행 전 렌더링 되어 발생한 오류였다.
해결: worker.start()를 비동기로 처리하여 초기화 후 애플리케이션을 렌더링.
해결 코드
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import App from './App';
import { worker } from './mocks/browser';
function prepare() {
if (process.env.NODE_ENV === 'development') {
return worker.start();
}
return Promise.resolve();
}
prepare().then(() => {
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>
);
});
귯~