MSW Mock 작성
E2E 테스트에서 모든 API 호출은 mocks/handlers.ts에서 MSW로 mocking합니다.
실제 DB를 오염시키지 않고 다양한 시나리오를 재현할 수 있습니다.
handlers.ts 작성법
핸들러 하나가 여러 시나리오를 담당합니다. request body에 따라 응답을 분기하면 하나의 핸들러로 정상/에러 케이스를 모두 처리할 수 있습니다.
// mocks/handlers.ts
import {http, HttpResponse} from 'msw';
import {createMSWResolver, e2eRequestBodyDeepEqual} from './utils';
export const e2eMockHandlers = [
http.post(
howmuchhomeApiOrigin + '/api/' + getAPIPath(AuthAPI.createAuthIdentityVerification),
createMSWResolver(async ({request}) => {
const body = (await request.json()) as CreateAuthIdentityVerificationRequest;
if (e2eRequestBodyDeepEqual(body, { name: '이미 인증한 유저', ... })) {
return HttpResponse.json(
{ errorCode: 'IDENTITY_VERIFICATION_DUPLICATION' },
{ status: 400 },
);
}
return HttpResponse.json({ authIdentityVerification: { id: 'abc', ... } });
}),
),
];핸들러에 정의된 분기가 곧 테스트에서 재현 가능한 시나리오의 전체 목록입니다. 테스트 코드를 작성하기 전에 먼저 필요한 분기를 핸들러에 설계하세요.
인증 상태 분기
Authorization 헤더의 토큰 값으로 로그인 여부나 사용자 상태를 분기합니다.
http.get(
howmuchhomeApiOrigin + '/api/' + getAPIPath(UserAPI.getMe),
createMSWResolver(info => {
const token = mswGetAccesstoken(info.request);
switch (token) {
case 'token':
return HttpResponse.json({ me: { id: 1, name: '이름', ... } });
default:
return HttpResponse.json({ me: null });
}
}),
),유틸리티
createMSWResolver
모든 핸들러는 반드시 이 함수로 감싸야 합니다.
http.post(url, createMSWResolver(async ({request}) => { ... }))*.vercel.app / localhost 이외의 도메인에서는 mocking 없이 실제 API를 통과(passthrough)시킵니다.
감싸지 않으면 실서비스 도메인에서도 API가 mocking되어 실제 데이터가 오염될 수 있습니다.
e2eRequestBodyDeepEqual
request body가 특정 형태와 정확히 일치하는지 확인합니다.
if (e2eRequestBodyDeepEqual(body, { name: '이름', carrier: 'SKT', ... })) {
return HttpResponse.json({ ... });
}⚠️ 중요: MSW가 수신하는 request body는 항상 snake_case입니다.
fetcherFactory는 서버로 요청을 보내기 전에 body의 키를 camelCase → snake_case로 변환합니다.
즉, MSW가 인터셉트하는 시점에는 이미 verifyToken → verify_token, registrationKey → registration_key로 변환된 상태입니다.
따라서 **핸들러에서 body.verifyToken 등 camelCase로 직접 접근하면 항상 undefined**가 됩니다.
반드시 e2eRequestBodyDeepEqual을 사용해야 합니다.
이 함수는 두 객체를 모두 snake_case로 변환한 뒤 비교하므로, 핸들러 작성 시 camelCase 타입 그대로 비교 대상을 전달하면 됩니다.
// ❌ 잘못된 예시 — 항상 false (body의 실제 키는 verify_token)
if (body.verifyToken === '123456') { ... }
// ✅ 올바른 예시 — e2eRequestBodyDeepEqual이 내부에서 snake_case 변환 후 비교
if (e2eRequestBodyDeepEqual(body, {verifyToken: '123456'})) { ... }⚠️ path param도 body에 포함됩니다.
API 클래스의 this.fetch(props)는 props 전체를 body로 전송하므로, URL path에 사용되는 필드(예: verificationId)도 실제 request body에 포함됩니다.
e2eRequestBodyDeepEqual은 deepEqual이므로 비교 대상에 body의 모든 필드를 포함해야 합니다.
// ❌ 잘못된 예시 — body에는 verification_id도 포함되어 있어 deepEqual 실패
if (e2eRequestBodyDeepEqual(body, {verifyToken: '123456'})) { ... }
// ✅ 올바른 예시 — path param 포함
const body = (await request.json()) as ConfirmIdentityVerificationRequest;
if (e2eRequestBodyDeepEqual(body, {
verificationId: 'delegate-iv-id',
verifyToken: '123456',
})) { ... }waitForHydration
페이지 이동 후 MSW Service Worker 활성화 + React hydration 완료를 대기합니다.
MSWProvider가 Suspense + use(mswReadyPromise)로 children 렌더를 차단하므로, SW controller가 활성화된 시점에 hydration도 완료된 상태입니다.
import {waitForHydration} from '../mocks/utils';
await page.goto(url);
await waitForHydration(page); // navigator.serviceWorker.controller 활성화 대기location.replace, page.goto, page.evaluate 등 full page navigation 후에 반드시 호출해야 합니다.
SPA navigation(Link 클릭, router.push 등)에서는 불필요합니다.
mswGetAccesstoken
Authorization 헤더에서 Bearer 토큰을 추출합니다.
const token = mswGetAccesstoken(request); // → 'token' | undefinedmswGetRequestBody
request body를 camelCase로 변환하여 반환합니다. snake_case로 오는 응답을 앱 코드 컨벤션에 맞게 읽을 때 사용합니다.
const body = await mswGetRequestBody<MyRequestType>(request);