ServiceWorker를 활용해서 네트워크 요청을 클라이언트 측에서 가로채고 요청을 모킹하는, JS를 위한 API Mocking Service Library 입니다.
다음과 같은 경우에 사용하면 유용합니다.
기본적으로 클라이언트에서 ServiceWorker를 사용해서 동작하는 것을 염두에 두고 설계되었습니다.
그렇기 때문에 서버환경에서도 MSW를 사용하려면 추가적인 설정이 필요합니다.
브라우저에서 ServiceWorker를 등록해서 클라이언트가 서버에게 API 요청을 보낼 때, 이 요청을 가로채고 모킹된 응답을 브라우저에게 전달합니다.
이런 경우 브라우저가 Mock API를 처리하기 때문에 서버의 실행없이 브라우저 내부에서 모킹이 이루어집니다.
💡 ServiceWorker
브라우저가 네트워크 요청을 하거나 네트워크로부터 응답을 받을 때, 브라우저와 네트워크 사이에 있는 ServiceWorker가 이러한 요청과 응답을 가로채는 웹 API입니다.
웹 페이지와 독립적으로 동작하며 백그라운드에서 페이지가 새로고침되거나 닫혀도 계속 실행할 수 있기 때문에, 백그라운드에서 push알림을 처리하거나, 앱에서 자주 사용되는 Resource를 저장해서 오프라인 상태에서도 앱이 작동되도록 지원합니다.
MSW는 이러한 ServiceWorker의 가로채기 기능을 이용해서 모킹처리를 수행합니다.
Next.js의 API라우트나 getServerSideProps를 사용하는 등 서버 환경에서 MSW는 Node.js의 HTTP 요청 Interceptor 기능을 사용해서 네트워크 요청을 가로채고 모킹 데이터를 반환합니다.
💡 API Mocking Library란?
실제 API를 호출하지 않고, 미리 정의된 가짜 응답을 반환해서 테스트와 개발 과정을 간소화하는 도구입니다.
이 라이브러리는 프론트엔드와 백엔드 개발 사이의 의존성을 줄이고, 백엔드가 준비되지 않은 상황에서도 프론트엔드 개발과 테스트를 가능하게 합니다.
Facker.js: 가짜 데이터를 생성해서 다양한 시나리오를 테스트할 수 있는 라이브러리로, 랜덤 데이터 생성에 특화되어 있어서 테스트 데이터를 유연하게 생성할 수 있지만, faker.js 라이브러리 단독으로는 네트워크 요청을 모킹하지 않고 다른 라이브러리와 결합해서 사용합니다.
Polly.js: 브라우저와 Node.js 환경 모두 지원해서 네트워크요청을 모니터링하고, 요청과 응답을 녹음하거나 모킹하는 기능을 제공합니다. 하지만, 요청과 응답을 직접 녹음하는 방식에 중점을 두기 때문에 단순히 모킹 작업을 하기에는 설정이 단순히 복잡합니다.
Mockyeah: Node.js 환경이지만, 클라이언트와 서버 간의 통합 테스트에서도 사용할 수 있습니다. 하지만, 브라우저 네트워크 요청을 직접 가로채는 기능은 부족합니다.
WireMock: 클라이언트와 서버 모두 테스트가 가능하지만, 클라이언트에서는 프록시 설정이 필요해서 다소 복잡하고, 브라우저 환경에서 MSW의 간결한 ServiceWorker에 비해 사용성이 떨어집니다.
ServiceWorker를 사용해서 다른 라이브러리보다 간단하고 우수한 성능으로 브라우저에서 네트워크 요청을 가로채는 방식이고, Node.js 환경에서 setupServer를 사용해서 동일한 handler 정의 방식으로 요청을 가로채기 때문에, 서버와 클라이언트 환경 간의 일관성을 유지할 수 있기 때문입니다.
기본적으로 Node.js 런타임 위에서 동작하며, Expresssk Koa와 같은 Node.js 서버 프레임워크 없이 자체적으로 HTTP 요청을 처리합니다.
Next.js의 Node.js 서버는 요청을 처리하는 방식에 따라 여러 역할을 수행합니다.
정적 파일 서비스 Static Site Generation
: public 폴더 안에 있는 파일들을 클라이언트에 별도의 렌더링 과정 없이 정적으로 직접 제공할 수 있습니다.
페이지 요청 처리: Next.js 라우팅은 파일 기반으로 특정 라우트에 매핑되는데 클라이언트에서 특정 URL에 따라 다른 페이지를 렌더링(CSR, SSR)하거나 특정 로직을 수행할 수 있습니다.
따라서, Next.js는 CSR, SSG, SSR의 렌더링 방식을 제공합니다.
RSC는 실행 자체가 서버에서 이루어지지만, RSC에서 필요한 데이터가 서버의 다른 API 혹은 외부 서비스로부터 필요할 때 서버 간의 요청을 보냅니다.
RSC에서 DB에 바로 접근한다면 서버에 별도로 요청을 보낼 필요가 없지만, RSC와 데이터를 제공하는 API가 분리되어있다면 요청을 통해 통신해야 합니다.
이런 경우 데이터를 제공하는 API에 직접 요청해서 데이터를 가져온다면, RSC 내부의 코드 복잡성을 줄이고 유지보수 효율성을 높일 수 있습니다.
혹은, 서버와 클라이언트 모두 동일한 방식으로 데이터를 가져오도록 표준화하거나, RSC에서 데이터를 요청하면 캐싱, 로깅, 핸들링을 쉽게 통합할 수 있습니다.
서버 간 요청으로 동일한 데이터를 반복해서 요청해야 하는 경우, 캐싱을 통해 효율성을 높일 수 있고, DB의 부하를 줄이고 응답 속도를 높일 수 있습니다.
예를 들어, 인기 상품 목록을 매번 DB에서 가져오는 대신 캐시에 저장해서 빠르게 응답할 수 있습니다.
요청과 응답 데이터를 각각 로깅하면, API 호출의 흐름을 추적하고 문제가 발생했을 때 디버깅에 활용할 수 있습니다.
예를 들어, 사용자가 특정 데이터 요청을 할 때 서버 간의 요청의 성공이나 실패 여부를 기록하면서, 운영 중인 시스템에서 발생하는 문제를 빠르게 파악할 수 있습니다.
외부 API에서 데이터가 제대로 반환되지 않거나, 예상치 못한 오류가 발생한 경우, 이를 서버에서 처리해서 사용자에게 적절한 에러 메시지를 전달하거나 대체 데이터를 제공할 수 있습니다.
예를 들어, 사용자 리뷰를 가져오는 API가 실패하면 오류 메시지를 반환할 수 있습니다.
getServerSideProps를 통한 Next.js의 SSR은 서버 측에서 실행되며 API 요청을 직접 수행하기 때문에, 클라이언트의 브라우저가 관여하지 않고 서버에서 모든 과정을 처리합니다.
MSW의 setupServer는 Node.js 환경에서 동작하며, 기본적으로 node-fetch 라이브러리를 통해 HTTP 요청을 인터셉트해서 Mock 데이터를 반환합니다.
그러나 Next.js의 서버 측 코드에서 네이티브 fetch API, undici, 또는 axios 같은 네트워크 구현체를 사용한다면, MSW가 해당 요청을 처리하도록 설정하지 않은 경우 네트워크 요청을 가로채지 못해 실제 서버에 요청이 전달될 수 있습니다.
기존의 Node.js 기반의 Next.js 서버에 MSW의 setupServer를 직접 적용하는 대신, 별도의 Node.js 서버를 만들어서 MSW를 실행하는 방식을 사용할 수 있습니다.
이러한 프록시 역할을 하는 임시 서버는 Next.js의 SSR 요청을 처리하거나, 서버와 클라이언트 간의 API 요청 및 응답을 가로채어 MSW가 Mock 데이터를 반환할 수 있도록 동작 방식을 통합합니다.
이를 통해 Next.js 서버와 MSW 서버 간의 네트워크 요청 처리 차이를 줄이고, SSR와 클라이언트의 요청 모두 일관된 Mock 데이터를 제공할 수 있습니다.
서버와 클라이언트 사이에 위치해서 요청과 응답을 대신 처리하는, 중간 다리 역할을 하는 중간 서버입니다.
예를 들어, 클라이언트가 API 데이터를 요청할 때, 클라이언트는 프록시 서버에 요청을 보내고 프록시 서버가 진짜 서버에 데이터를 요청하고 & 받은 데이터를 다시 클라이언트로 보냅니다.
이를 통해, 클라이언트는 직접 진짜 서버와 통신하지 않고도 데이터를 주고 받을 수 있습니다.
msw 라이브러리 설치
$ npm i msw --save-dev
public/ 폴더 하단에 msw 세팅
save 옵션dmfh package.json에 등록하고 msw를 업데이트할 때마다 해당 항목을 업데이트함.
$ npx mas init public --save
MSW의 ServiceWorker 동작을 구현한 자동 생성파일로, 클라이언트와 통신하여 요청을 모킹하거나 실제 네트워크 요청을 처리합니다. 파일의 내용을 수정하지 말고, MSW 설정을 통해 동작을 제어하는 것이 좋습니다.
// eslint, tslint 비활성화
/* eslint-disable */
/* tslint:disable */
/**
* Mock Service Worker.
* @see https://github.com/mswjs/msw
* - Please do NOT modify this file. 이 파일을 수정하지말라
* - Please do NOT serve this file on production. production 환경에서 이 파일 실행하지 말라
*/
const PACKAGE_VERSION = '2.3.0' // 패키지 버전
const INTEGRITY_CHECKSUM = '26357c79639bfa20d64c0efca2a87423' // ServiceWorker 파일의 무결성을 확인할 때 사용됨
const IS_MOCKED_RESPONSE = Symbol('isMockedResponse') // 모킹된 응답을 식별하기 위한 심볼
const activeClientIds = new Set() // 모킹이 활성화된 클라이언트 ID를 저장하는 Set객체
self.addEventListener("install", function(){
// serviceWorker가 설치될 때 실행됩니다.
// self.skipWaiting() 메서드를 호출해서 바로 serviceWorker를 활성화합니다.
self.skipWaiting()
})
self.addEventListener("activate", function(event){
// serviceWorker가 활성화될 때 실행됩니다.
// 모든 클라이언트를 제어하도록 설정합니다.
event.waitUntil(self.clients.claim())
})
self.addEventListener("message", async function(event){
// 클라이언트에서 보내는 메시지를 처리합니다.
})
self.addEventListener("fetch", function(){
// 네트워크 요청을 가로채고 처리합니다.
})
// .. 이하생략
browser.ts
MSW는 브라우저 환경에서 실행되며, 브라우저에서 네트워크 요청을 가로채기 위해 setupWorker를 사용해서 ServiceWorker를 설정합니다.
browser.ts 파일은 브라우저에서 사용할 ServiceWorker를 초기화하는 코드로, Application의 요청을 모킹하기 위한 중심 역할을 합니다.
import { setupWorker } from 'msw/browser';
import { handlers } from './handlers';
// 주어진 요청 핸들러로 서비스 워커를 설정합니다.
const worker = setupWorker(...handlers);
export default worker;
setupWorker
: MSW의 브라우저 전용 API로, 요청 핸들러를 받아 ServiceWorker를 설정합니다.handlers
: 모킹할 API 요청에 대한 규칙이 정의된 배열로, 각 요청의 응답을 정의합니다.worker
: ServiceWorker의 Instance로, 브라우저 환경에서 실행될 때 네트워크 요청을 가로채고 모킹한 응답을 제공합니다.browser.ts 파일은 클라이언트에서 모킹 설정에 사용되며, worker instance를 필요한 곳에서 불러와서 (MSW를) 초기화하면, 브라우저 환경에서 MSW를 간편하게 사용할 수 있습니다.
MSWComponent
MSW는 클라이언트에서 모든 API 요청을 가로채기 위해 전역적으로 초기화해야 합니다.
따라서 Next.js Application에서 모든 페이지가 렌더링되기 전에 실행되고,
공통 레이아웃과 설정을 관리하는 페이지에서 MSW 초기화를 수행해야 합니다.
MSW는 브라우저 환경에서 실행되기 때문에 v2로 버전이 업그레이드되면서 window 객체가 undefined가 아닌지 확인하는 분기처리가 필요해졌습니다.
if(typeof window !== 'undefined' && process.env.NEXT_PUBLIC_API_MOCKING === 'enabled') {
import('../src/mocks/browser').then(({worker}) => worker.start());
}
위와 같이 초기화하는 코드를 src/app/pages/_app.js
혹은 src/app/layouy.tsx
와 같은 페이지에서 직접 초기화하는 코드를 작성해도 되지만, 가독성을 위하 MSWComponent
를 만들어 분리해서 사용할 수도 있습니다.
// src/app/_component/MSWComponent.tsx
// 로그인 전후 상관없이 동일하게 사용하기 때문에 app 하위에 컴포넌트 생성
'use client';
import { useEffect } from 'react';
export const MSWComponent = () => {
useEffect(()=>{
if(typeof window !== 'undefined' && process.env.NEXT_PUBLIC_API_MOCKING === 'enabled') {
require('@/mocks/browser').worker.start();
}
},[]);
// 실제로 렌더링되는 DOM은 없기 때문에 null을 반환합니다.
return null;
};
💡 모듈을 불러오는 방식의 차이: 정적로드 vs 동적로드
setupWorker를 사용한 ServiceWorker 관련 코드는 브라우저 환경에서만 지원되기 때문에, 서버에서 사용하지 않아야 합니다.
하지만, Next.js에서 정적 로드로 worker 모듈을 가져온다면, 빌드 시점에서 서버가 해당 모듈을 평가하려고 시도하면서, 오류를 발생시킬 수 있는 문제가 있습니다.
동적로드로 worker 모듈을 조건에 따라 가져오기 때문에 서버 환경에서 브라우저 전용 코드가 실행되지 않도록 방지해줄 뿐만 아니라, 메모리와 실행 비용을 줄일 수 있습니다.정적로드
- 컴파일 단계에서 모듈의 의존성을 분석하고, 해당 모듈을 코드의 시작 시점에서 가져옵니다.
- 동기적으로 동작하며, 필요한 모든 모듈을 한 번에 가져올 수 있지만, 필요 없는 모듈도 가져올 수 있습니다. 이러한 문제를 해결하기 위해 webpack 같은 bundler가 정적 의존성을 분석해서 최적화할 수 있습니다.
동적로드
- 코드 실행 중에 필요한 시점에서만 가져오는 지연로드와 특정 시점에서 필요한 모듈만을 가져오는 조건부 로드를 하기 때문에, 번들 크기의 최적화가 가능합니다.
- 비동기적으로 동작하기 때문에, Promise를 반환합니다.
=> 특정 환경에서만 필요한 모듈을 로드하거나, 초기 번들 크기를 줄이거나, 런타임에 의존성을 동적으로 결정해야 할 때, 동적로드를 사용하면 좋습니다.
💡 worker 모듈을 가져오는 방식:
import()
require()
MSWComponent에서 worker 모듈을 가져오는 방법이 import() 메서드를 활용하는 ES6과, require() 메서드를 활용하는 CommonJS 모듈, 두 가지 방법으로 동적으로 모듈을 가져왔습니다.
import() 메서드를 활용하는 ES6
ES6 모듈시스템으로 동적으로 가져오면 Promise를 반환하기 때문에, 비동기 작업을 수행해야되서 코드가 복잡해질 수 있습니다. 하지만, 코드가 실행될 때 해당 모듈을 조건부러 가져오면서 초기 번들 크기를 줄일 수 있습니다.
require() 메서드를 활용하는 CommonJS
script가 실행되는 시점에 즉시 모듈을 가져오고 실행하기 때문에 모듈 로드 시점이 더 빠를 수 있지만 초기 번들 크기가 커질 수 있습니다.
https.ts
CORS 설정과 Mock API handler를 사용해 Express 기반의 HTTP 서버를 실행해서, Mock API의 응답을 제공합니다.
port 9090으로 Express Application을 초기화해서 다양한 HTTP 요청을 처리할 수 있는 라우팅 및 미들웨어 구조를 설정할 수 있습니다.
const app = express();
const port = 9090;
CORS를 설정해서 다른 도메인에서 오는 네트워크 요청을 허용합니다.
여기서는 다음과 같은 환경에서의 요청을 허용합니다.
app.use(cors({
origin: 'https://localhost:3000',
optionSuccessStatus: 200,
credentials: true,
}));
💡 CORS (Cross-Origin Resource Sharing)
기본적으로 브라우저는 보안상의 이유로 출처가 다른 요청을 차단합니다.
프로토콜http, https
, 도메인localhost, example.com
, 포트3000, 9090
의 조합이 다른, 다른 출처에서 오는 요청할 때, 출처가 다르기 때문에 기본적으로 브라우저의 네트워크 요청이 차단되지만, 차단하지 않고 Next.js 클라이언트의 요청을 허용하기 위해 CORS 설정을 해줘야 합니다.
💡 PreFlight
일부 구형 브라우저에서는 기본값 204 상태코드를 받으면, 응답을 잘못 처리하는 경우가 있기 때문에 인증정보가 포함된 요청을 처리하고 구형 브라우저에서도 동작하게 하기 위해 PreFlight를 설정합니다.
브라우저는 다음과 같은 요청을 보낼 때, 서버에서 OPTIONS 메서드를 사용해서 해당 요청을 허용할 지 먼저 확인하는 PreFlight 작업을 수행합니다.
- GET, POST 이외의 HTTP 메서드
- Authorization, Content-Type이 application/json 이 아닌 다른 값인 특별한 헤더
그리고 서버에서 응답을 허용하겠다는 200대의 상태 코드를 받으면, 브라우저는 실제 요청을 수행합니다.
POST 요청을 JSON 데이터가 전송될 때, JSON 데이터를 객체를 변환하는 것처럼 요청 본문에서 JSON 데이터를 파싱합니다.
app.use(express.json());
@mswjs/http-middleware
라이브러리의 createMiddleware 메서드를 사용해서 네트워크 요청을 가로채고, 미리 정의된 응답을 반환하는 함수들이 포함된 배열인 handlers를 mock handler로 추가합니다.
app.use(createMiddleware(...handlers));
서버를 앞서 지정된 포트에서 실행하며, 성공적으로 서버를 실행하면 콘솔을 출력합니다.
app.listen(port, ()=>console.log("서버 실행완료"));
클라이언트에서의 MSW는 브라우저에서 자동으로 실행하지만, 앞서 만든 Node.js는 임시 서버의 경우 다음 명령어를 통해 직접 실행해야 합니다.
watch 옵션을 사용하면 https.ts 혹은 handlers.ts 파일이 수정될 때 MSW 임시 서버를 자동으로 재시작할 수 있습니다.
npx tsx watch ./src/mocks/http.ts
💡 tsx 명령어
번들링 없이 TypeScript 파일을 빠르게 실행할 수 있습니다.