프론트 테스트가 스테이징 인터페이스 상태에 묶이면, 백엔드 점검 하루에도 초록불이 꺼진다. MSW(Mock Service Worker)는 브라우저 서비스 워커와 노드 인터셉터로 요청을 가로채 핸들러 응답을 돌려준다. 실제 서버를 띄우지 않고 목록·에러·지연·권한 실패를 재현할 수 있다.
아래는 설치 → 핸들러 작성 → 브라우저 워커 → 단위 테스트용 노드 서버 → 오버라이드·에러 시나리오 → 배포 파이프라인 주의점 순 실습이다. 러너 비교는 Vitest와 Jest 비교, 컴포넌트 테스트 뼈대는 단위 테스트 실전 튜토리얼, 브라우저 끝단 자동화는 플레이라이트 가이드와 같이 보면 역할이 겹치지 않는다. 국내 팀에서 흔히 쓰는 한글 안내 문구·에러 배너도 같은 핸들러로 고정해 검증한다.
왜 MSW인가 | fetch를 패치하지 않는 이유
예전에는 전역 요청 함수를 스파이로 감싸거나 클라이언트 어댑터로 응답을 가짜로 만들었다. 동작은 하지만 앱이 실제로 쓰는 네트워크 경로와 어긋나기 쉽다. 목 서비스 워커 방식은 네트워크 경계에서 요청을 가로채므로, 화면·훅·페이지 코드의 요청 호출을 거의 그대로 둔 채 시험할 수 있다. “테스트용으로만 다른 클라이언트를 쓴다”는 우회가 줄어든다.
방식
가로채는 위치
잘 맞는 경우
주의
목 서비스 워커
브라우저 워커 / 노드 인터셉터
화면·훅·통합 시험, 로컬 데모
핸들러 누락 시 실제 네트워크로 나갈 수 있음
요청 함수 스파이
전역 함수
한 함수 단위 빠른 단위 시험
클라이언트 라이브러리 교체 시 깨짐
로컬 목 서버 프로세스
별도 프로세스
여러 서비스 연동 스모크
포트·기동 순서·파이프라인 비용
끝단 시험 라우트 가로채기
브라우저 자동화 라우팅
사용자 플로우 끝단만 모킹
단위·화면 시험과 핸들러 공유 어려움
데이터 가져오기 라이브러리와 같이 쓸 때는 캐시 키가 가짜 응답과 맞는지 확인한다. 쿼리 캐시 쪽 패턴은 쿼리 라이브러리 가이드를 참고하고, 이 글의 실습은 핸들러 작성·시험 부트스트랩·에러 화면 고정에 둔다. 한글 로딩 문구와 빈 목록 안내가 디자인 시안과 같은지도 같이 본다.
MSW는 앱 코드의 fetch 경로를 유지한 채 네트워크 경계에서 응답을 바꾼다
1단계 | 패키지 설치와 폴더 뼈대
프로젝트 루트에서 개발 전용 패키지로 설치한다. 버전은 문서 작성 시점 기준이며, 설치 직후 패키지 파일에 찍힌 메이저 번호를 기준으로 공식 시작 가이드를 한 번 더 확인한다. 메이저가 바뀌면 임포트 경로(msw/browser, msw/node) 표기가 달라진 적이 있다.
권장 폴더 예시는 요청 핸들러 파일, 개발용 브라우저 워커 파일, 단위 시험용 노드 서버 파일, 응답 샘플(JSON) 폴더를 나란히 두는 형태다. 핸들러를 시험 파일마다 복사해 붙이지 말고 한곳에서 내보낸다. 그래야 목록 필드가 바뀔 때 수정 지점이 하나로 남는다.
초기화 명령은 서비스 워커 스크립트를 정적 경로에 복사한다. 앱 라우터 기반 프레임워크라면 보통 공개 정적 폴더가 맞고, 다른 번들러도 기본 정적 디렉터리를 쓴다. 이 파일을 저장소에 올리지 않으면 동료 기기와 배포 파이프라인에서 워커 등록이 실패한다. 코드 리뷰 체크리스트에 “워커 스크립트 변경 여부” 한 줄을 넣어 두면 빠뜨리기 어렵다.
2단계 | 핸들러 작성 | REST 목록·상세·POST
핸들러는 “이 메서드·이 경로면 이 응답 본문”을 선언한다. 주소는 앱이 실제로 호출하는 것과 동일해야 한다. 상대 경로를 쓰면 현재 출처(origin) 기준으로 맞추고, 절대 주소를 쓰면 호스트까지 맞춘다. 로컬 인터페이스 기본 경로가 /api로 시작한다면 핸들러도 같은 접두사를 쓴다. 개발 서버 포트만 다른 경우가 많아, 포트 숫자를 핸들러에 박아 넣기보다 상대 경로를 우선하는 편이 안전하다.
응답 본문 필드 이름은 화면이 읽는 키와 같아야 한다. 백엔드는 productName인데 목 데이터만 name이면 시험은 통과하고 연동만 깨진다. 가능하면 타입 정의나 샘플 응답 한 건을 공유 폴더에 두고 핸들러가 그걸 재사용하게 한다.
자주 막히는 지점: 앱은 /api/products를 치는데 핸들러만 외부 호스트 절대 경로로 두면 매칭이 안 된다. 개발자 도구 네트워크 패널(또는 시험 로그)에 찍힌 전체 주소를 그대로 옮긴 뒤, 공통 접두사는 상수로 빼 둔다. “한 글자 슬래시” 차이로 하루를 날리는 경우가 많다.
3단계 | 브라우저 워커 | 로컬 화면 개발
로컬에서 백엔드 없이도 목록 화면을 그리려면 브라우저 워커를 켠다. 공개 배포 빌드에는 절대 넣지 않는다. 환경 변수나 개발 모드 가드로 앱 진입점을 감싼다. 실수로 켜진 채로 배포되면 실제 사용자 트래픽이 가짜 응답을 받을 수 있어, 플래그 기본값은 “끔”이 안전하다.
디자이너·기획자가 로컬 미리보기만 필요할 때도 같은 워커가 유용하다. 스테이징 계정이 없거나 점검 중일 때 한글 UI 카피와 빈 상태 일러스트를 먼저 맞출 수 있다. 다만 목 데이터 가격·재고 숫자를 그대로 스크린샷에 쓰면 안 되는 경우가 있으니, 데모 전용 더미 문구를 쓴다.
src/mocks/browser.ts + 앱 진입점 연결
// src/mocks/browser.ts
import { setupWorker } from 'msw/browser'
import { handlers } from './handlers'
export const worker = setupWorker(...handlers)
// 예: src/main.tsx (Vite) 또는 클라이언트 전용 bootstrap
async function enableMocking() {
if (import.meta.env.MODE !== 'development') return
if (import.meta.env.VITE_ENABLE_MSW !== 'true') return
const { worker } = await import('./mocks/browser')
await worker.start({
onUnhandledRequest: 'warn', // 또는 'error' / 'bypass'
})
}
enableMocking().then(() => {
// createRoot(...).render(<App />)
})
미처리 요청 옵션을 경고로 두면 핸들러에 없는 호출이 콘솔에 남는다. 초기에는 경고로 빈 핸들러를 찾고, 파이프라인·엄격 모드에서는 오류로 올려 누락을 시험 실패로 바꾼다. 우회(bypass)는 실제 네트워크로 흘리므로 “일부만 목” 전략일 때만 쓴다. 전부 우회로 두면 목 도입 효과가 거의 없다.
풀스택 프레임워크에서는 서버에서 그리는 영역과 브라우저에서 그리는 영역의 요청 경로가 갈린다. 클라이언트 전용 화면 목은 위 워커로, 서버 모듈·라우트 단위 시험은 다음 절의 노드 서버를 쓴다. 라우트 구현 검증과 페이지 화면 검증을 한 파일에 섞지 않는 편이 원인 추적이 쉽다. “서버에서 이미 데이터를 넣어 주는지, 클라이언트에서 다시 치는지”를 먼저 그림으로 그려 보라.
개발 플래그가 켜진 환경에서만 워커를 등록하고 프로덕션 번들에서는 제외한다
4단계 | Vitest Node 서버 | 컴포넌트·훅 테스트
가상 돔 환경의 화면 시험은 브라우저 서비스 워커 대신 노드용 서버 셋업을 쓴다. 전역 준비 파일에서 한 번 올리고, 각 시험 전후에 핸들러를 초기화한다. 이렇게 해야 이전 케이스의 일시 오버라이드가 다음 케이스로 새지 않는다. 초기화를 빼먹으면 “파일 실행 순서에 따라 초록/빨강이 바뀌는” 고질이 생긴다.
시험 러너 설정 파일에 준비 스크립트 경로를 등록했는지 확인한다. 등록을 잊으면 로컬 단일 파일 실행에서는 우연히 통과하고, 전체 스위트에서만 실패하는 식으로 보이기도 한다.
src/mocks/server.ts + vitest.setup.ts
// src/mocks/server.ts
import { setupServer } from 'msw/node'
import { handlers } from './handlers'
export const server = setupServer(...handlers)
// vitest.setup.ts
import { beforeAll, afterEach, afterAll } from 'vitest'
import { server } from './src/mocks/server'
beforeAll(() => {
server.listen({ onUnhandledRequest: 'error' })
})
afterEach(() => {
server.resetHandlers()
})
afterAll(() => {
server.close()
})
화면 시험 유틸의 대기·찾기 헬퍼를 쓰면 비동기 로딩 표시 이후 문서 구조를 안정적으로 잡는다. 타임아웃 숫자만 늘리고 원인을 안 보면 간헐 실패가 남는다. 실패 시 핸들러 매칭 로그와 컴포넌트가 실제로 친 주소를 먼저 대조한다. “대기 시간 부족”으로 단정하기 전에 404 목 응답이나 키 이름 불일치부터 의심하는 편이 빠르다.
5단계 | 오버라이드 | 500·401·빈 목록 시나리오
기본 핸들러는 성공 경로만 두고, 오류 화면은 시험 안에서 일시 핸들러로 덮어쓴다. 매 케이스 뒤 초기화가 기본값으로 되돌린다. 이 패턴이 없으면 파일 순서에 따라 시험이 깨진다. 팀 컨벤션으로 “성공 기본 + 실패는 케이스 지역 오버라이드”를 문서 한 줄로 고정해 두면 신규 입사 후에도 실수가 줄어든다.
권한 만료·결제 실패·재고 없음처럼 비즈니스 문구가 다른 오류는 상태 코드만 바꾸지 말고 본문 메시지도 실제와 비슷하게 맞춘다. 한글 문장이 길면 줄바꿈·아이콘 여백이 깨지는 버그가 잘 드러난다.
실무 팁: 오류 문구를 시험에 그대로 박기 전에 디자인 시스템·번역 키와 맞춘다. 문구가 자주 바뀌면 알림 역할(role)이나 안정적인 시험용 식별자로 셀렉터를 고른다. 카피 수정 PR마다 시험이 연쇄 실패하는 비용을 줄일 수 있다.
6단계 | 쿼리·헤더·지연 | 현실적인 요청 조건
목록 인터페이스는 페이지·검색어·정렬을 질의 문자열로 받는 경우가 많다. 핸들러에서 요청 주소를 읽어 분기한다. 인증 헤더가 없으면 권한 오류를 주는 처리를 기본에 넣을지, 특정 시험에서만 넣을지 팀 규칙을 정한다. 기본에 넣으면 “로그인 전 화면” 시험이 편하고, 빼 두면 성공 경로 작성이 짧다. 둘 다 틀리진 않지만 혼용하면 온보딩 비용이 커진다.
지연을 모든 핸들러에 길게 넣으면 시험 스위트 전체가 느려진다. 스피너·뼈대 화면 검증이 필요한 케이스에만 인위 대기를 넣고, 기본 핸들러는 거의 즉시 응답하게 둔다. 파이프라인 제한 시간과 로컬 피드백 속도 사이의 타협이다. 전체 스위트가 5분을 넘기면 개발자가 시험을 건너뛰기 시작한다.
지연·401·500 오버라이드로 로딩·권한·장애 UI를 각각 고정해 검증한다
7단계 | Next.js·Playwright·CI에서 깨지지 않게
역할 분리를 명확히 한다. 목 서비스 워커는 화면·훅·일부 통합 시험과 로컬 데모, 브라우저 끝단 자동화는 실제 사용자 클릭 흐름, 스테이징 인터페이스는 배포 직전 스모크다. 끝단 시험까지 목만으로 덮으면 “목 세계”와 운영 계약이 어긋나도 초록불이 유지될 수 있다.
클라이언트 화면: 단위 러너 + 화면 시험 유틸 + 노드 목 서버
서버 라우트·유틸: 모듈 단위 시험 또는 상위 통합에서 실제 함수 호출
끝단 자동화: 핵심 구매·로그인 흐름은 스테이징 또는 픽스처 서버. 필요 시 한두 경로만 라우트 가로채기
파이프라인: 미처리 요청을 오류로 처리, 워커 스크립트 커밋, 환경 플래그로 공개 배포에서 비활성
운영 중 예외 관측은 목과 별개다. 배포 후 오류는 런타임 모니터링 가이드처럼 소스맵·릴리스와 함께 본다. 시험 초록불과 운영 알림은 같은 화면이 아니다. 목이 가려 준 장애를 모니터링이 나중에 보여 주는 구조를 전제로 둔다.
도입 체크리스트 | 팀이 자주 빠지는 구멍
항목
확인
핸들러 단일 출처
브라우저·단위 시험이 같은 핸들러를 가져옴
워커 스크립트 파일
정적 폴더에 생성·커밋, 경로와 시작 옵션 일치
공개 배포 가드
개발 플래그 없으면 워커 시작 호출 없음
핸들러 초기화
매 케이스 후 연결, 일시 오버라이드 누수 방지
미처리 요청
시험은 오류, 로컬 초기만 경고
계약 동기화
명세·백엔드 변경 시 샘플 필드 갱신 요청
끝단 시험 경계
핵심 흐름은 스테이징 또는 별도 자동화 전략
샘플 응답이 오래된 필드명을 들고 있으면 화면 시험은 통과하고 운영만 깨진다. 가능하면 백엔드 스키마 샘플을 핸들러 타입에 연결하거나, 주 1회 스모크에서 실제 응답 키 존재 여부만 비교한다. 필드 이름이 영어 식별자여도 사용자에게 보이는 라벨은 한글로 두는 팀 규칙을 문서에 한 줄 적어 두면 리뷰가 수월하다.
화면·훅·로컬 미리보기 목은 서비스 워커 방식이 다루기 쉽다. 실제 브라우저에서 여러 페이지를 넘는 사용자 흐름은 끝단 자동화 도구가 맞다. 끝단 시험 전체를 목에만 의존하면 계약 어긋남을 놓치기 쉬워, 핵심 경로는 스테이징 인터페이스 또는 최소 경로 모킹만 쓰는 편이 안전하다.
풀스택 앱 라우터 구조에서도 동작하나?
클라이언트 화면의 브라우저 요청은 워커로 가로챌 수 있다. 서버에서 실행되는 요청은 브라우저 워커 범위 밖이므로, 그 계층은 노드 목 서버로 단위·통합 시험을 구성한다. 페이지 한 파일에 서버·클라이언트를 섞어 시험하지 말고 경계를 나눈다. “데이터가 어디서 채워지는지”를 먼저 합의하면 중복 목이 줄어든다.
핸들러가 있는데도 실제 서버로 나간다. 왜 그런가?
주소·메서드 불일치, 워커 미기동, 기본 경로 누락, 또는 미처리 요청 우회 설정이 흔한 원인이다. 개발자 도구 네트워크 패널의 전체 주소와 핸들러 문자열을 나란히 놓고, 시험이라면 미처리 요청을 실패로 바꾼 뒤 로그를 본다. 프록시 설정이 경로를 다시 쓰는 팀도 많아, 로컬 설정 파일을 함께 확인한다.
그래프QL도 같은 방식으로 모킹하나?
같은 라이브러리가 그래프QL 핸들러 인터페이스를 제공한다. 엔드포인트 주소와 연산 이름(또는 질의 문서)으로 분기한다. 일반 자원 경로와 달리 본문 질의 문자열이 길어서, 샘플을 연산 단위로 나누고 공통 오류 확장을 한곳에서 정의하는 편이 유지보수에 유리하다.
공개 배포 번들에 목 코드가 들어가면 어떻게 막나?
동적 불러오기와 개발 전용 환경 변수로만 워커 시작을 호출한다. 번들 분석으로 목 패키지가 클라이언트 공개 청크에 포함되는지 확인하고, 실수로 포함되면 조건 분기 불러오기를 고친다. 공개 사이트에 워커가 뜨면 사용자 트래픽이 가짜 응답을 받을 수 있다. 배포 전 체크리스트에 “목 플래그 끔” 한 줄을 넣는다.
팀 샘플 응답이 백엔드와 어긋나지 않게 하려면?
핸들러 응답 타입을 인터페이스 타입(또는 명세에서 생성한 타입)과 공유하고, 스키마 변경 요청에 샘플 수정을 같은 커밋으로 넣는다. 주간으로 스테이징 응답 샘플을 받아 필수 필드 존재 여부만 스모크해도 어긋남이 줄어든다. 한글 라벨 카피 변경과 필드 키 변경을 이슈 유형으로 구분해 두면 담당이 명확해진다.
MSWMock Service WorkerAPI 모킹Vitest프론트엔드개발자APITypeScriptReact테스트PlaywrightNext.js