-
언제까지 MSW? MirageJS 알아보기Tip 2025. 8. 8. 19:10728x90반응형
MirageJS 실무에서 프론트엔드 개발을 하다 보면 생각보다 백엔드 개발이 늦어지는 경우가 많습니다.
이전에 MSW에 대해 소개했었는데요.
2024.11.30 - [Tip] - 실무에서 유용한 MSW(Mock Service Worker) 활용 가이드
실무에서 유용한 MSW(Mock Service Worker) 활용 가이드
프런트엔드 개발을 하다 보면 백엔드 API가 완성되지 않은 상태에서 작업해야 하는 API 의존성 문제는 항상 개발 속도와 품질에 큰 영향을 미칩니다.이런 상황에서 모킹(Mocking)은 필수적이지만,
kir93.co.kr
이번 글에서는 최근 괜찮다고 생각되는 MirageJS를 MSW와 비교하며 소개해보겠습니다.
MirageJS란?
MirageJS는 브라우저 내에 가상의 API 서버를 생성하여 프론트엔드 코드와 함께 동작하는 목킹 라이브러리입니다.
원래 Ember.js 커뮤니티에서 시작되었으나, 이제는 프레임워크에 독립적으로 React, Vue 등 어디에서나 사용 가능합니다.
MirageJS의 핵심 개념은 프론트엔드에 실제 백엔드와 동일한 형태의 API를 흉내 내주는 것입니다.
개발자는 MirageJS로 가상 서버를 생성(createServer)하고 라우트 핸들러(this.get, this.post 등)를 정의하여 엔드포인트별 응답을 지정합니다.
그러면 애플리케이션이 fetch나 XHR로 해당 엔드포인트에 요청할 때 MirageJS가 이를 가로채고(Intercept) 미리 정의된 응답을 반환합니다.
MirageJS의 특징은 클라이언트 측에 ORM이 포함된 인메모리 데이터베이스(DB)를 제공한다는 점입니다.
즉, MirageJS로 가짜 데이터 모델(Model)과 관계(Relationship)를 정의하고, 가짜 레코드를 추가/조회하여 CRUD 시나리오를 모두 시뮬레이션할 수 있습니다.
이러한 데이터 모델링 중심의 접근 방식 덕분에, MirageJS는 비교적 복잡한 서버 동작까지도 한 곳에서 구현하고 관리할 수 있습니다.
예를 들어 MirageJS에는 factory, fixture, serializer 등의 개념이 있어 여러 유형의 테스트 데이터를 쉽게 만들어내고, 실제 백엔드 JSON 응답의 구조를 모방할 수 있습니다.
요약하면, MirageJS는 "프론트엔드에 심는 작은 백엔드"에 가깝습니다.
실제 네트워크 요청 없이도 프론트엔드 앱이 백엔드와 상호작용하는 것처럼 개발할 수 있게 해 주죠.
이러한 설계 철학 때문에 MirageJS는 테스트 주도 개발이나 스토리북(Storybook) 환경에서 백엔드 대용으로 많이 활용됩니다.
MSW(Mock Service Worker)란?
MSW에 대해서는 이미 이전 글에서 소개했기 때문에 이번 글에서는 간략하게 소개하고 넘어가겠습니다.
MSW는 이름 그대로 서비스 워커(Service Worker) 기술을 활용하여 웹 애플리케이션의 네트워크 요청을 가로채는 목킹 라이브러리입니다.
MSW는 클라이언트와 서버 사이드 모두에서 동작하는(isomorphic) API 모킹을 지향합니다.
MSW는 "서비스 워커를 활용한 HTTP 트래픽 요격기"라고 볼 수 있습니다.
브라우저 환경의 장점을 활용하여 실제 네트워크를 흉내 내고, 동일한 모킹 코드를 Node.js 테스트나 SSR 환경에 재사용할 수 있다는 점에서, 최근 프론트엔드 개발자들 사이에서 큰 인기를 끌고 있습니다.
MirageJS vs MSW: 구조와 동작 원리 비교
두 라이브러리는 모두 Next.js 같은 SPA/SSR 혼합 환경에서 동작할 수 있지만, HTTP 가로채기 방식과 내부 구조에 상당한 차이가 있습니다.
개발 시 편의성과 제한사항에 직접 영향을 주므로, 먼저 기술적 원리를 비교해 보겠습니다.
요청 가로채기 방식 (Interceptor Mechanism)
MirageJS는 브라우저에서 window.fetch와 XMLHttpRequest를 몽키패치(monkey-patch)하는 방식으로 모든 AJAX 호출을 가로챕니다.
MirageJS 내부에서는 Pretender.js라는 라이브러리를 사용해 저수준에서 XHR을 흉내 내는데, 이를 통해 애플리케이션 코드에는 수정 없이도 네트워크 계층을 가로채 응답을 처리합니다.
한 마디로, MirageJS를 켜두면 브라우저 내에서 이루어지는 fetch/XHR 요청이 실제 네트워크로 나가기 전에 MirageJS의 라우트 핸들러로 전달되는 것입니다.
이러한 동작은 Same-origin 요청뿐 아니라 외부 도메인으로 나가는 요청(fetch)에 대해서도 적용됩니다.
(Mirage 설정에 따라 특정 도메인을 패스스루(passthrough) 하지 않는다면.)
반면 MSW는 서비스 워커(Service Worker)를 이용하여 브라우저의 네트워크 레벨에서 요청을 가로채는 구조입니다.
서비스 워커는 브라우저에 등록되어 프록시 서버 역할을 하는 스크립트로, 페이지에서 일어나는 HTTP(S) 요청을 가로챌 수 있습니다.
MSW는 이 메커니즘을 활용하여, 개발자가 정의한 요청 핸들러에 매칭되는 네트워크 요청이 있을 경우 이를 가로채 가짜 응답을 만듭니다.
MSW의 가로채기는 브라우저 수준에서 일어나므로, DevTools Network 패널에 요청과 응답이 실제로 보이는 것처럼 표시된다는 장점이 있습니다.
반면 MirageJS는 fetch를 가로채기 때문에 Network 패널에는 요청이 나타나지 않고, 대신 MirageJS가 콘솔에 로그를 출력하거나(오류 시) 개발자가 수동으로 로그를 남겨야 요청 흐름을 추적할 수 있습니다.
이 부분이 두 도구의 중요한 차이점으로, MSW 측이 디버깅 면에서 보다 현실적인 경험을 줍니다.
기술적으로 보면, MirageJS는 애플리케이션 스레드 내에서 네트워크 호출을 가로채지만, MSW는 별도 워커 스레드에서 가로챕니다.
따라서 MirageJS의 응답 생성 로직은 메인 쓰레드에서 동작하므로 복잡한 로직이나 큰 데이터를 처리하면 UI 렌더링과 자원을 공유하게 됩니다.
MSW는 워커 스레드가 응답을 처리하기 때문에 주 쓰레드와 격리되어 있고, 이론적으로는 메인 UI 쓰레드를 덜 점유하게 될 수 있습니다.
하지만 일반적인 모킹 작업은 가벼운 JSON 처리 수준이므로, 이러한 구조 차이가 성능에 미치는 영향은 미미한 편입니다.
요약하자면: MirageJS는 fetch/XHR 단계에서 인터셉트하고, MSW는 네트워크 단계에서 인터셉트합니다.
이로 인해 MSW는 브라우저가 실제 HTTP 요청을 주고받는 것처럼 동작하며, MirageJS는 브라우저를 완전히 속여 네트워크를 우회하는 방식이라고 볼 수 있습니다.데이터 모델링과 응답 구성 방식
MirageJS와 MSW의 모킹 방식에도 철학적 차이가 있습니다.
MirageJS는 가상 서버인 만큼, 데이터 생성/조회/저장 등의 상태 관리 기능을 제공합니다.
MirageJS의 createServer 설정에서 models, fixtures, factories 등을 정의하면 Mirage가 내부에 인메모리 DB를 만들고 API 엔드포인트에서 이를 조회하거나 조작할 수 있게 해 줍니다.
예를 들어 MirageJS에서 this.post("/users") 핸들러가 schema.users.create를 통해 유저를 하나 생성하면, 이후 this.get("/users") 요청에서 방금 생성된 유저까지 포함한 목록을 쉽게 돌려줄 수 있습니다. 관계가 있는 데이터도 Mirage의 ORM이 알아서 foreign key 등을 관리해 주므로, 여러 엔드포인트 간의 데이터 일관성(referential integrity)이 자동으로 유지됩니다.
MirageJS를 이용하면 마치 진짜 데이터베이스가 있는 서버처럼 여러 요청 간에 상태가 유지되는 것입니다.
단, 페이지 리로드 시 Mirage의 가상 DB도 초기화되는 것이 기본 동작입니다.
개발 중 특정 시나리오에서는 이 초기화가 오히려 도움이 되기도 하지만, 필요하면 localStorage 등을 이용해 임시로 데이터 지속성을 부여할 수도 있습니다.
반대로, MSW는 기본적으로 상태를 내장하고 있지 않습니다.
각 요청 핸들러는 호출될 때마다 독립적으로 동작하여 응답을 생성합니다.
가령 GET /users 요청과 POST /users 요청에 대해 각각 핸들러를 만들었다면, POST /users 핸들러에서 전역 변수나 클로저에 새로운 유저를 추가하지 않는 한, GET /users 결과에는 반영되지 않습니다.
즉, 개발자가 수동으로 상태를 관리해야 합니다.
MSW 팀도 이러한 요구를 인지하여 @mswjs/data라는 별도 패키지를 제공하고 있는데, 이것은 MirageJS처럼 데이터 모델을 정의하고 쿼리 할 수 있는 작은 ORM/DB입니다.
선택적으로 이 라이브러리를 도입하면 MirageJS에 가까운 패턴으로 모킹을 구성할 수 있지만, MSW 자체에는 의존성이 없으므로 사용 여부는 개발자 자유입니다.
결국 MirageJS는 "자료(data)를 중심으로 API를 흉내 내는" 방식이고, MSW는 "요청(transaction)을 중심으로 API를 흉내 내는" 방식이라고 할 수 있습니다.
MirageJS는 전체 API의 동작을 한데 모아 시나리오를 만들기 쉬운 반면, MSW는 엔드포인트별로 개별적인 처리를 명시적으로 작성해야 합니다.
예컨대 MirageJS에서는 아래와 같이 한 곳에 서버 로직을 몰아서 작성합니다.
// MirageJS 예시 - 가상 서버 설정 createServer({ models: { user: Model, // 사용자 모델 정의 }, routes() { this.namespace = "/api"; // API 경로 프리픽스 // GET /api/users 리스트 가져오기 this.get("/users", (schema) => { return schema.users.all(); // 가상 DB에서 모든 user 조회 }); // POST /api/users 새로운 사용자 추가 this.post("/users", (schema, request) => { let attrs = JSON.parse(request.requestBody); return schema.users.create(attrs); // 가상 DB에 user 레코드 생성 }); }, seeds(server) { // 초기 가상 데이터 시드 server.create("user", { name: "Alice" }); server.create("user", { name: "Bob" }); } });
위처럼 MirageJS는 라우트별 응답 로직과 데이터 저장 로직을 한 군데 묶어서 정의합니다.
이에 비해 MSW로 동일한 행위를 하려면
// MSW 예시 - 요청 핸들러 정의 let usersData = [ // 가짜 데이터 저장용 변수 { id: 1, name: "Alice" }, { id: 2, name: "Bob" } ]; const handlers = [ rest.get('/api/users', (req, res, ctx) => { return res(ctx.status(200), ctx.json(usersData)); }), rest.post('/api/users', (req, res, ctx) => { return req.json().then(newUser => { newUser.id = Date.now(); usersData.push(newUser); // 전역 배열에 추가하여 상태 변화 반영 return res(ctx.status(201), ctx.json(newUser)); }); }) ];
MSW에서는 이처럼 전역 usersData 배열을 두고 POST 핸들러에서 수동으로 배열을 수정해 주었습니다.
데이터 일관성을 유지하려면 개발자가 이런 처리를 해줘야 한다는 점이 MirageJS와 다릅니다.
다행히 자바스크립트 환경이라 전역 변수나 클로저를 통해 상태 유지가 가능하고, 서비스 워커는 페이지 라이프사이클과 분리되어 페이지 리로드를 해도 서비스 워커 (및 그 안의 변수)는 유지되는 특성이 있습니다.
따라서 위 예시에서 usersData는 앱이 새로고침돼도 (또는 페이지 전환해도) 서비스 워커가 살아있는 한 계속 유지되지만, 서비스 워커가 재로드 되거나 탭을 새로 열면 초기화됩니다.
MirageJS의 가상 서버 역시 페이지 새로고침 시 초기화되는 점은 비슷하지만, MSW는 워커가 백그라운드에서 지속될 수 있다는 차이가 있습니다.
어쨌든 일반적인 개발 흐름에서는 두 경우 모두 애플리케이션을 완전히 재시작하면 모킹 상태도 초기화되는 것으로 이해하면 됩니다.
한편, GraphQL API 모킹 지원은 MSW가 더 친절합니다.
MSW에는 graphql.query와 graphql.mutation 헬퍼가 있어, 실제 GraphQL 스키마의 쿼리 이름을 지정하고 응답 데이터를 쉽게 정의할 수 있습니다.
MirageJS도 GraphQL 요청을 처리할 수는 있으나, 별도 헬퍼 없이 this.post("/graphql") 등의 엔드포인트로 받아서 요청 바디의 쿼리를 파싱/분기하는 식으로 직접 구현해야 합니다.
MirageJS 팀도 공식 문서에 @miragejs/graphql를 사용하면 가능하다고 언급은 하지만, REST만큼 편리하진 않습니다.
따라서 GraphQL 기반 프로젝트에서는 MSW 쪽에 손을 들어주고 싶습니다.
WebSocket 모킹도 MSW는 실험적인 지원이 있는 반면, MirageJS는 WebSocket까지는 다루지 않습니다.(주로 HTTP 기반)
다른 라이브러리와의 통합
Next.js 프로젝트에 MirageJS나 MSW를 적용할 때는 각각 유의해야 할 점이 있습니다.
Next.js App Router는 React 서버 컴포넌트와 클라이언트 컴포넌트를 혼용하는 구조이며, SSR(Server-Side Rendering)과 클라이언트사이드 데이터 패칭이 공존합니다.
이러한 특성 때문에, 어느 환경에서 목킹을 동작시킬지 결정해야 합니다.
MirageJS는 앞서 설명했듯 브라우저에서만 동작합니다.
window 객체와 fetch를 가로채는 방식이므로, 서버 사이드에서 실행되는 코드 (예: Next.js의 getServerSideProps나 App Router의 서버 컴포넌트 fetch)에는 영향을 주지 못합니다.
MirageJS 공식 문서에서도 "Mirage는 현재 브라우저에서만 동작하므로, getServerSideProps 같은 서버 측 네트워크 호출은 모킹 하지 못한다"라고 명시하고 있습니다.
따라서 Next.js 환경에서 MirageJS를 사용하려면, 클라이언트에서 발생하는 요청만 Mirage로 처리하고 SSR 단계에서는 실제 API를 호출하게 두거나, SSR 단계에서는 데이터를 미리 패칭 하지 않고 클라이언트 컴포넌트에서 패칭 하도록 유도해야 합니다.
예를 들어 Next.js 13 App Router에서 fetch를 서버 컴포넌트 안에서 호출하면, MirageJS는 아래의 요청을 볼 수 없습니다.
export default async function Page(){ const data = await fetch(...).then(res=>res.json()); // ... })
반면 클라이언트 컴포넌트의 useEffect나 클라이언트 커스텀 훅(SWR 등)에서 fetch 하면 MirageJS가 이를 가로챌 수 있습니다.
정리하면, MirageJS는 Next.js 애플리케이션의 브라우저 구동 시점에서만 동작합니다.
MirageJS를 Next.js에 통합하는 일반적인 방법은, 앱 초기화 시점에 Mirage 서버를 생성하는 것입니다.
과거 pages 라우터에서는 pages/_app.js에서 if (process.env.NODE_ENV === "development") { makeServer(); } 식으로 Mirage 서버를 띄우곤 했습니다.
App Router에서는 app/layout.tsx나 최상위 Client Component에서 비슷한 처리를 해야 합니다.
한 가지 주의점은 MirageJS 서버는 앱당 하나만 생성되어야 하므로, 컴포넌트가 여러 번 렌더링 된다고 해서 서버를 중복 생성하면 안 됩니다.
이를 피하려면 전역 스코프(예: src/mirage/server.js 모듈)에 createServer 호출을 딱 한 번만 실행하고, 이를 임포트 하여 초기화하거나, 또는 useRef 등을 이용해 한 번만 실행하도록 해야 합니다.
Next.js App Router에서는 <Component />를 감싼 상위에 'use client'를 선언한 Provider 컴포넌트를 만들어 Mirage 세팅을 넣는 방식도 쓰입니다.
실제 MirageJS 예제를 보면, Next.js 페이지 컴포넌트 파일 상단에서 곧바로 createServer({...})를 호출하는 방식도 소개되어 있는데, 이는 그 페이지가 import 될 때 서버가 만들어지는 것입니다.
여러 페이지에 같은 Mirage 설정을 쓰려면 설정을 모듈로 분리한 뒤 공통 import 하면 되지만, App Router에서는 구조가 달라 다소 시행착오가 있을 수 있습니다.
MirageJS를 Next.js 13+에서 사용할 때 개발자가 겪는 대표적인 이슈로 Next의 내부 요청과 충돌 가능성을 들 수 있습니다.
Next App Router는 페이지 전환이나 Prefetch 시에 _next/data나 내부 API를 fetch 할 수 있는데, 이 경로들이 Mirage에 걸리면 문제가 됩니다.
이를 해결하려면 Mirage 서버 설정에서 this.passthrough("/_next/*") 나 this.passthrough(request => request.url.includes("__next")) 등으로 Next가 사용하는 정적 자원이나 데이터 패칭 경로를 예외 처리해야 합니다.
MirageJS는 기본적으로 정의되지 않은 경로 요청이 오면 404를 응답하므로, Next.js 내부 요청 경로들은 Mirage에 정의하지 않고 그대로 네트워크로 빠져나가도록 허용해야 App Router가 정상 동작합니다.
이런 설정을 모르거나 간과하면 Mirage 사용 시 Next 내비게이션이 고장 나는 경험을 할 수 있습니다.
(Mirage를 Next와 연동할 때 설정 난이도가 있다는 경험적인 증거이기도 합니다)
요약하면, MirageJS를 Next.js에서 쓸 때는 브라우저 환경에서만 동작함을 유념하고, Next의 고유한 요청은 패스스루 설정을 해야 하며, 서버 생성 타이밍과 범위를 잘 조절해야 한다는 것입니다.
MSW와 Next.js의 통합은 개념적으로 Mirage보다 범용적이지만, 설정할 부분이 조금 다릅니다.
MSW를 Next.js 프로젝트에 넣으려면 먼저 서비스 워커 스크립트를 생성 및 등록해야 합니다.
npx msw init public/ 명령으로 public/mockServiceWorker.js 파일을 만들어두고, 애플리케이션 시작 시 worker.start()를 호출하여 서비스 워커를 등록합니다.
Next.js에서는 이 작업을 보통 클라이언트 측에서 한 번만 수행해야 합니다.
App Router 기준으로, layout 또는 자체 작성한 Client Component에서 아래와 같이 호출하면 됩니다.
if (process.env.NODE_ENV === "development") { worker.start() }
Next.js 13에서는 개발 모드에서 Turbopack 관련 이슈로 MSW가 잘 동작하지 않았던 적도 있었지만 (MSW 패키지를 transpile 해야 하는 등), 현재(Next 15 기준)는 큰 문제없이 동작합니다.
오히려 MSW 쪽에서 Next App Router의 SSR을 지원하기 위해 서버용 MSW (setupServer)를 활용하는 패턴이 등장했습니다.
Next.js 13~14 시기에는 MSW 서비스 워커가 SSR 단계의 요청을 가로채지 못하는 한계가 있었습니다.
예를 들어, 서버 컴포넌트에서 await fetch('https://api.example.com/data')를 호출하면 이 요청은 Node.js 서버에서 발생하므로, 브라우저의 MSW 서비스 워커는 관여할 수 없습니다.
이를 우회하기 위해 MSW는 setupServer API로 Node.js 환경에서 요청을 intercept 하는 기능을 제공해 왔는데, Next.js App Router의 서버 컴포넌트는 Web/Edge API 기반으로 동작하여 기존 Node intercept와 호환이 어려웠습니다.
다행히 Next.js 15 버전에서 이러한 제약이 개선되어, MSW 측에서도 Next SSR을 지원하도록 업데이트가 이루어졌습니다.
구체적으로, Next 15부터는 서버 컴포넌트의 fetch가 MSW와 호환되도록 스레드 구조가 변경되었고, MSW 2.x 버전에서는 worker와 server를 함께 활용하는 예제가 문서화되었습니다.
설정 방법은 MSW의 가이드에 잘 나와 있는데, 요약하면 개발 모드에서 서버 측에선 setupServer(...).listen()를 호출하고, 브라우저 측에선 기존대로 worker.start()를 호출하는 식입니다.
Next.js 앱에서는 이를 깔끔하게 하기 위해 initMocks() 같은 유틸 함수를 만들어 typeof window로 분기하여 서버/브라우저 각각 MSW를 켜는 접근을 권장합니다.
이렇게 설정하면, Next.js 서버 컴포넌트나 API Route에서 발생하는 fetch 요청도 MSW (Node) 서버가 가로채고, 클라이언트 요청은 MSW 워커가 가로채는 완벽한 목킹이 가능합니다.
Server Side Mocking for Playwright in NextJS (App Router) using Mock Service Worker
Mock Service Worker (MSW) could not mock Server Side calls in NextJS 14 when using App Router. NextJS...
dev.to
정리를 하자면, MSW는 MirageJS에 비해 Next.js와의 통합 범위가 넓습니다.
클라이언트 사이드 모킹은 물론이고, 원한다면 SSR 단계까지 가로채기를 구현할 수 있습니다.
다만 설정해야 할 파일이 Mirage보다 많습니다. 예를 들어 서비스 워커 파일 (MSW CLI로 생성), 브라우저용 MSW 설정, 서버용 MSW 설정, 그리고 이를 초기화하는 코드 등을 프로젝트 구조에 맞게 만들어야 하죠.
위 섹션의 MirageJS 예제 코드와 MSW 예제 코드를 비교해 보면, Mirage는 한 파일에서 단번에 설정이 끝났지만, MSW는 handlers.js, browser.js, server.js, index.js 등 몇 개의 파일로 설정을 분리해서 작성하고 있습니다.
이러한 초기 설정은 한 번만 하면 되는 일이지만, 진입장벽 측면에서는 MirageJS보다 조금 더 복잡한 것이 사실입니다.
MirageJS 측 문서에서도 "Mirage는 한 줄 import와 간단한 설정으로 바로 쓸 수 있지만, MSW는 서비스 워커 파일 생성 등의 추가 단계가 필요하다"라고 언급하고 있습니다.
React Native 환경에서는 상황이 다소 반전됩니다.
MSW는 Service Worker를 기반으로 동작하기 때문에, 브라우저가 아닌 환경인 React Native에서는 기본적으로 사용할 수 없습니다.
React Native는 서비스 워커나 브라우저 네트워크 계층을 지원하지 않기 때문에 MSW의 장점인 요청 가로채기가 동작하지 않습니다.
물론 일부 대안 라이브러리나 mock adapter(fetch-mock 등)를 MSW 스타일로 래핑해 사용하는 방법은 있으나, 이는 복잡하고 별도 설정이 필요합니다.
반면 MirageJS는 React Native에서도 fetch와 XMLHttpRequest를 가로채는 방식이기 때문에, 브라우저가 아닌 환경에서도 동작이 가능합니다.실제로 MirageJS는 브라우저 환경 외에도 Jest 테스트나 React Native 환경에서도 유효한 방식으로 API를 모킹 할 수 있기 때문에, React Native 앱 개발 시 빠르게 가짜 서버를 세팅하고, 실제처럼 데이터 흐름을 테스트하고 싶은 경우 유용하게 사용됩니다.
React Native에서는 디버깅 환경이 제약적이고 네트워크 요청 추적이 어려운 경우가 많은데, MirageJS는 자체적으로 fetch를 후킹 하여 네트워크 경로를 제어할 수 있기 때문에 디버깅과 상태 기반 목킹 시나리오 구성에서 MSW보다 유리합니다.
따라서 React Native 기반 프로젝트라면 MirageJS가 오히려 더 자연스럽고 생산적인 선택이 될 수 있습니다.
결론: 어떤 상황에서 무엇을 선택할까?
MirageJS와 MSW는 각기 다른 강점을 가진 모킹 도구입니다.
두 라이브러리 모두 Next.js 같은 현대 프론트엔드 프레임워크에서 백엔드 없이 개발을 진행하게 해 주지만, 그 접근 방식과 사용감에는 차이가 있습니다.
- 개발자 경험(DX): MSW는 실제 네트워크와 유사한 경험 제공(Network 패널 표시 등)으로 디버깅에 강점이 있고, MirageJS는 데이터 모델링과 시나리오 관리 측면에서 복잡한 모킹을 쉽게 구조화하는 강점이 있습니다. 간단한 모킹은 MSW가 편하고, 규모 있는 모킹은 MirageJS가 체계적입니다.
- 성능: 실시간 개발 환경에서 둘 다 충분히 빠르며 큰 차이는 없습니다. MirageJS는 기본 지연이 있어 현실적인 느림을 보여주고, MSW는 즉각적 응답으로 빠른 피드백을 줍니다. 동시 요청 처리나 스레드 구조상 이점은 MSW에 약간 있지만, 일반 사용에는 큰 영향 없습니다.
- 설정 난이도: MirageJS는 한두 줄로 바로 시작할 정도로 쉽지만 SSR 지원이 안 되고 Next 통합 시 몇 가지 주의가 필요합니다. MSW는 초기 설정 파일이 몇 개 필요하고 서비스 워커 개념을 알아야 하지만, 클라이언트/SSR 모두 대응할 수 있고 한번 설정하면 편하게 유지됩니다.
- 통합 및 범용성: Next.js App Router 환경에서는 MSW가 SSR까지 포함하는 유연성으로 좀 더 완벽한 모킹 시나리오를 구성할 수 있습니다. MirageJS는 SSR 부분은 모킹 하지 못하므로, 해당 부분은 실서버를 호출하거나 다른 방식으로 해결해야 합니다. 브라우저에서의 클라이언트 패칭 위주로 개발한다면 MirageJS로 충분하지만, 모든 환경을 isomorphic 하게 모킹 하려면 MSW가 답입니다.
- 장기 유지보수: MSW는 커뮤니티가 크고 활발하여 앞으로도 개선과 지원이 기대되는 반면, MirageJS는 현재 기능 범위 내에서 안정적으로 사용되는 경향입니다. 팀의 성향이나 프로젝트 요구에 따라, 더 관습적인 방식(MirageJS) vs. 유연한 레고식 방식(MSW)을 선택하게 될 것입니다.
어떤 상황에서 MirageJS가 적합한가?
- 복잡한 데이터 관계를 가진 가상의 백엔드를 만들어야 할 때
MirageJS는 엔티티 간 연관이 많은 도메인을 가진 앱에서 유용합니다. 예를 들어 제품, 주문, 사용자, 리뷰 등 여러 개체가 얽힌 상황에서, MirageJS의 ORM으로 일관성 있는 가짜 데이터를 생성하고 CRUD 흐름을 쉽게 관리할 수 있습니다. - 모든 엔드포인트를 통째로 모킹 하여 프론트엔드와 백엔드 작업을 병행하고자 할 때
MirageJS로 실제 API와 동일한 인터페이스의 모킹 서버를 만들어두면, 프론트엔드 개발이 완료된 뒤 MirageJS 설정을 백엔드팀과 공유해 명세서처럼 활용할 수도 있습니다. - React Native 앱 개발 시
MSW는 브라우저의 서비스 워커에 의존하기 때문에 React Native에서는 기본적으로 사용할 수 없습니다. 반면 MirageJS는 fetch와 XMLHttpRequest를 가로채는 방식이라, React Native 환경에서도 정상적으로 동작하며, 개발자가 백엔드 없이도 네트워크 요청 흐름을 완전히 시뮬레이션할 수 있습니다. 따라서 React Native 프로젝트에서는 MirageJS가 더 자연스럽고 효과적인 선택지가 됩니다. - 프론트엔드 개발 단계에서 빠르게 UI를 구축하고 싶을 때
MirageJS는 JSON Server처럼 별도 프로세스를 띄울 필요 없이 브라우저에서 바로 동작하므로, 새로운 UI를 만들며 필요한 API를 Mirage에 추가해 가면서 UI와 API를 맞물려 설계할 수 있습니다. - Cypress 등 E2E 테스트에서 일관된 환경을 유지하고 싶을 때
MirageJS를 앱과 함께 구동하면 E2E테스트 실행 시에도 항상 같은 데이터 초기상태에서 시작할 수 있어 테스트 간 간섭 없이 진행할 수 있습니다.
어떤 상황에서 MSW가 적합한가?
- Next.js와 같이 SSR을 활용하는 프로젝트에서
SSR 단계의 데이터 패칭까지 목킹해야 하거나, 브라우저와 Node 환경 모두에서 테스트를 원하면 MSW가 사실상 유일한 선택지입니다. 예를 들어 페이지를 서버사이드 렌더링하면서 외부 API를 호출하는 경우, MSW로 해당 호출을 Node 레벨에서 가로채도록 할 수 있습니다. MirageJS로는 불가능합니다. - 디버깅 편의와 개발자 학습 곡선을 중시할 때
MSW는 Network 패널에 기록이 남고 사용법이 비교적 직관적이라, 팀원 모두가 쉽게 모킹을 작성하고 이해할 수 있습니다. MirageJS의 모델/펙토리 개념이 낯선 팀이라면 MSW 쪽이 진입장벽이 낮습니다. 일부 API만 부분적으로 모킹 하거나, 실제 API와 병행해서 쓸 때: MSW는 특정 요청만 가로채고 나머지는 실제 네트워크로 흘려보내기가 쉽습니다 (정의되지 않은 요청은 자동으로 패스스루). MirageJS도 passthrough가 가능하지만, MSW는 기본 동작이 필요한 것만 인터셉트라 설정이 덜 번거롭습니다. 따라서 실제 백엔드가 구현된 부분은 그대로 쓰고, 없는 부분만 모킹 하는 시나리오에 적합합니다. - 테스트 코드에서 모킹을 제어하고 싶을 때
Jest+MSW 조합은 테스트 파일에서 필요한 핸들러만 활성화하거나 동적으로 변경하기 쉬워 유연한 테스트 작성이 가능합니다. MirageJS를 테스트에 쓰면 전역 서버 상태를 관리하거나 후처리가 필요하지만, MSW는 테스트별로 server.use()로 핸들러를 주입하고 server.resetHandlers()로 정리하는 식이 표준화되어 있습니다.
결국 "MirageJS vs MSW 누구가 우위냐"를 일반화하기는 어렵고, 프로젝트 요구사항과 팀 선호에 따른 선택이 될 것입니다.
그리고 모든 프로젝트에서 꼭 필요하지 않을 수도 있습니다.
"Mirage를 Next에 시도했다가 포기하고 fetch를 직접 모킹 했다."라는 댓글도 있을 정도입니다.
마지막으로 저의 의견을 덧붙이자면, Next.js App Router를 사용하면서 SSR을 적극 활용하는 프로젝트라면 MSW가 더 무난한 선택으로 느껴집니다.
반대로 클라이언트 사이드 렌더링 위주의 SPA이고, 특히 상당히 복잡한 데이터 모델의 API를 백엔드 완료 전에 프론트에서 구현해야 하는 상황이라면 MirageJS로 시작하는 게 개발 생산성에 도움이 될 것입니다.
개발자 경험 측면에서는 MSW가 처음엔 편하나, 규모가 커지면 MirageJS의 체계가 그리워질 수 있고, MirageJS가 처음엔 생소해도 쓰다 보면 백엔드 흉내 내는 재미와 편리함에 익숙해질 수 있습니다.
둘 모두 프론트엔드 개발을 보다 즐겁게 해주는 유용한 무기입니다.
MirageJS vs MSW라는 비교 구도는 마치 "구조화된 완결형 vs 유연한 조립형"의 대결처럼 보이는데, 프로젝트에 맞게 선택하면 어느 쪽이든 "백엔드가 늦어져도 프론트엔드는 늦지 않게!" 해주는데 큰 도움을 줄 것입니다.
반응형'Tip' 카테고리의 다른 글
이제는 TipTap? 아직도 CKEditor? (5) 2025.08.01 웹 애니메이션 성능을 끌어올리는 requestAnimationFrame(rAF) 활용법 (3) 2025.07.24 Module Federation을 활용한 마이크로 프론트엔드 구현 (0) 2025.07.14 과연 언제부터 프론트엔드에 테스트가 필요할까? (1) 2025.07.11 Portal만 쓰시나요? HTML 표준 Dialog, Popover API 알아보기 (1) 2025.06.07