MSW (Mock Service Worker) 테스트
생성일 : 2026.09.28 17:35
- MSW는 함수가 아닌 네트워크 레이어를 가로채서 실제 fetch 호출과 같은 흐름을 그대로 테스트해, 컴포넌트가 실제 서버와 상호작용하는 것을 검증한다.
- 에러 객체를 정밀하게 재현할 수 있어, 401, 429, 503 등 HTTP 상태 코드별 UI 반응과 네트워크 끊김 같은 상황을 정확히 테스트한다.
- react‑query와 같은 라이브러리의 캐싱·재시도·placeholderData 동작까지 실제로 동작하도록 하여, queryFn이 제대로 호출되는지 검증할 수 있다.
- 핸들러를 한곳에 모아 두면 API 계약이 바뀔 때 수정이 명확해지고, Storybook과 공유해 일관된 목업 데이터를 사용하며, API‑first 병렬 개발 시 프론트엔드 개발이 순조롭게 진행된다.
MSW (Mock Service Worker) 테스트 도입 가이드
(iNote – 개인 기록 앱용)
1️⃣ 왜 MSW를 도입했나요?
기존 테스트 방식은 함수를 가짜로 갈아치우는 방식이였다. 하지만 이거에 대한 한계가 있다.
1)vi.mock으로 함수를 통째로 대체해버리면 이 로직들은 테스트 커버리지에서 완전히 빠져. 여기 버그가 생겨도 유닛테스트는 전부 통과.
- "내가 손으로 적어둔 가짜 객체를 컴포넌트가 잘 렌더링하는가"일 뿐, "실제 서버 응답 형태와 컴포넌트가 기대하는 형태가 일치하는가"는 아무도 확인 안 하거든.
2) 에러 객체의 "진짜 모양"이 검증 안 됨
- "올바른 요청을 보냈는가"를 간접적으로만 검증 가능.
> vi.mock 방식:
mockedApiGet.mockRejectedValue(new Error("fail"))— 그냥 평범한Error..status도.code도 없음. 이 에러로는error.status === 401분기를 애초에 테스트할 수가 없어서, 개발자가 매번new ApiError(401, "...", "AUTH_EXPIRED")를 손으로 만들어야 하는데 이걸 깜빡하기 쉬움.> MSW 방식:
HttpResponse.json({ message: "...", code: "AUTH_EXPIRED" }, { status: 401 })만 주면, 실제api.ts코드가 그걸 파싱해서 진짜ApiError인스턴스를 만들어. 프로덕션에서 실제로 만들어지는 것과 똑같은 모양의 에러 객체가 테스트에도 그대로 등장해.2) react-query의 "실제 동작"을 우회해버림 — query key 버그를 못 잡는 예
react-query의 실제 동작(캐싱,
retry,placeholderData)이 검증 안 됨. queryFn 자체를 mock으로 바꿔치기하면 react-query가 실제로 그 옵션대로 동작하는지 확인할 방법이 없음.vi.mock 방식:
mockedApiGet이 애초에 react-query를 거치지 않고 직접 값을 리턴하도록 세팅하는 경우가 많아서(혹은 hook 자체를 mock), 이 버그가 "queryFn이 실제로 다시 호출됐는가"라는 react-query 고유의 판단 로직과 무관하게 테스트가 통과해버릴 수 있다.MSW 방식: 내가 쓴 AdminMembersPage.test.tsx의 "마지막 페이지로 이동" 테스트가 정확히 이 버그를 잡아.
다음클릭 후screen.findByText("회원21")을 기다리는데, queryKey에page가 빠져 있으면 react-query가 refetch를 안 하니 화면엔 여전히회원1이 떠 있고, 이 assertion이 타임아웃으로 실패해. 실제 react-query 캐싱 로직이 살아서 동작하기 때문에 잡히는 버그.
3)MSW:
MSW는 함수가 아니라 네트워크 레이어(fetch가 나가는 지점)를 가로챈다. 그래서 컴포넌트 → useAPI 쿼리 → api.get → fetch까지 실제 코드가 전부 실행되고, MSW는 그 fetch가 나가는 순간 "서버인 척" 응답만 대신 해준다.
테스트가 검증하는 게 "이 함수가 이렇게 불렸다"가 아니라 "이 화면을 렌더링했을 때 실제로 서버에 이런 요청이 가고, 이런 응답이 오면 화면이 이렇게 바뀐다"로 바뀐다. [실제 프로덕션 동작에 훨씬 가까워짐]
MSW라고 이 문제를 100% 없애주진 않아 — handlers.ts도 결국 사람이 손으로 쓴 응답이니까. 다만 MSW는 응답 지점이 한 곳(handlers.ts)으로 모여 있어서, BE 계약이 바뀌면 고쳐야 할 곳이 명확하고(테스트 파일마다 흩어진 mock 리턴값을 다 찾아다닐 필요 없음), 팀에 따라 OpenAPI 스키마로 handlers 응답을 검증하는 식으로 확장하기도 훨씬 쉬워.
2. 실무에서 MSW를 사용하는 이유.
1) API-first 병렬 개발 (BE 완성 전에 FE 먼저 진행)
BE 스펙(엔드포인트, 요청/응답 형태)만 먼저 합의되면, FE는 그 형태로 handlers.ts를 채워두고 실제 BE가 하나도 없어도 기능 전체를 구현+테스트할 수 있어. 이번에 우리가 독서 기록 만들 때는 BE를 실제로 먼저 짜고 FE가 따라갔지만, 팀이 커지면 보통 반대 순서(계약 먼저 → FE/BE 동시 착수)로 가고, 그때 MSW가 FE를 막힘없이 진행시켜주는 핵심 도구가 돼. MSW는 setupWorker로 실제 로컬 개발 서버(브라우저)에서도 켤 수 있어서, "BE 없이 프론트만 실행해서 데모하기"도 가능해 — 테스트 전용이 아니야.
2) Storybook과 목업 데이터를 공유
msw-storybook-addon이라는 공식 애드온이 있어서, 컴포넌트의 Storybook 스토리(로딩 상태, 에러 상태, 빈 목록 상태 등)에 테스트에서 쓴 것과 같은 handlers를 그대로 붙일 수 있어. 그러면 "스토리북에서 보여주는 모습"과 "테스트가 검증하는 모습"이 항상 같은 소스를 쓰게 돼서, 둘이 서로 다른 가짜 데이터를 쓰다가 어긋나는 일이 없어져.
3) 에러 시나리오 매트릭스를 정확하게 재현
실무에서 진짜 자주 필요한 게 "401(로그인 만료) → 로그인 페이지로 리다이렉트", "429(rate limit) → 잠시 후 재시도 토스트", "503(서버 다운) → 오프라인 배너", "네트워크 자체가 끊김(TypeError) → 재시도 버튼" 이런 정확한 상태 코드별 UI 대응을 검증하는 거야. MSW는 HttpResponse.json({}, {status: 429})처럼 실제 상태 코드를 그대로 흉내 낼 수 있고, HttpResponse.error()로 "네트워크 자체가 끊긴 상황"(응답 자체가 없는 것)까지 구분해서 시뮬레이션할 수 있어 — 이 두 개는 실제 fetch 코드에서 완전히 다른 코드 경로(하나는 res.ok가 false, 하나는 fetch() 자체가 reject)라, 함수 mock으로는 이 둘을 세심하게 구분해서 테스트하기 번거로워.
4) 업계 표준화
MSW는 Testing Library(React Testing Library) 생태계에서 사실상 표준 API 모킹 도구로 자리잡았어 — Kent C. Dodds(Testing Library 제작자)가 자신의 블로그/워크샵에서 "fetch를 직접 mock하지 말고 네트워크 레벨에서 가로채라"는 원칙을 MSW로 전파했고, 그 이후 Remix·Next.js 관련 다수 스타터 템플릿이 테스트 셋업에 기본 포함하는 수준으로 퍼졌어. "구현 디테일이 아니라 사용자가 보는 동작을 테스트하라"는 Testing Library의 철학과 MSW의 "네트워크 계약을 테스트하라"는 접근이 같은 뿌리라서 세트로 묶여 쓰이는 경우가 많아.
문제 | 기존 접근 | MSW 도입 시 장점 |
|---|---|---|
실제 API가 없거나 비공개 | API 호출을 | 브라우저/Node 모두에서 같은 네트워크 스택을 사용 → 개발/테스트 환경과 동일한 동작 |
통합 테스트 시 네트워크 지연/불안정 | 직접 서버 띄우거나 테스트용 서버 사용 |
|
Mock 코드가 분산 | 각 테스트 파일에 | 하나의 |
프론트엔드와 백엔드가 다른 언어/프로젝트 | 프론트엔드에서 직접 mock | MSW는 백엔드와 동일한 경로에 대해 인터셉트 → 프론트·백엔드 간 계약 테스트 가능 |
테스트 실행 속도 | 실제 서버 호출(지연) | 응답이 즉시 반환 → 테스트 속도 2~3배 빨라짐 |
핵심 : “실제 API처럼 동작하지만 테스트 환경에서 완벽하게 제어” → 안정적인 테스트와 빠른 피드백.
2️⃣ MSW 테스트를 어떻게 해야 하나?
2‑1. 프로젝트에 MSW 설치
# npm
npm i msw --save-dev
# yarn
yarn add msw --dev
2‑2. 핸들러(handlers.ts) 정의
// src/mocks/handlers.ts
import { rest } from 'msw';
export const handlers = [
// 예시: GET /api/tasks
rest.get('/api/tasks', (req, res, ctx) => {
return res(
ctx.status(200),
ctx.json([
{ id: 1, title: '할 일 1', completed: false },
{ id: 2, title: '할 일 2', completed: true },
])
);
}),
// 예시: POST /api/tasks
rest.post('/api/tasks', async (req, res, ctx