지난 시간에 이어서 이번엔 msw 라이브러리를 사용해서 Mocking을 해보려 한다.
mock-apollo-client의 기술검증을 해보고 있던 중 모킹할 쿼리문에 일일이 mockclient를 생성해서 넣어줘야 하는 부분이 너무 귀찮고 불편해서 더 나은 방법을 찾다가 msw를 알게 되었다.
개발환경
next v14 (page 라우팅)
apolloClient v3.6
msw v2
Mock Service Worker의 약자
service worker는 별도로 하는 역할이 있는데 이에 대해서는 별도로 정리해볼 예정이다 (처음 본 개념이여서 신기했다)
브라우저가 네트워크를 통해 서버로 보내는 요청을 Service Worker가 중간에 가로채서 미리 생성해둔 응답값을 다시 클라이언트로 보내준다.
...
pages
ㄴmocks
ㄴ handlers.ts
ㄴ MSWComponent.tsx
public
ㄴ mockServiceWorker.js (init시 자동생성)
먼저 msw설치
yarn add -D msw
next는 정적파일디렉토리가 /public임으로 worker스크립트에게 별도로 디렉토리를 지정해줘야 한다
npx msw init <public_dir> --save
—save 플래그는?
package.json파일에 주어진PUBLIC_DIR을 저장하여 워커 스크립트의 자동 업데이트를 위해 다음과 같이 설정할 수 있습니다.
msw를 실행시키면 ./public 디렉토리를 저장할것이다
package.json 파일에 msw.workerDirectory 속성을 추가하여 msw 패키지를 설치할 때마다 워커 스크립트를 자동으로 지정된 디렉토리로 복사하도록 설정할 수 있습니다. 이렇게 하면 현재 설치된 라이브러리 버전과 워커 스크립트가 동기화됩니다.
// package.json
{
"name": "my-app",
"msw": {
"workerDirectory": "./public"
}
}
MSWComponent를 만들어서 브라우저일때 serviceWorker를 실행시키고 서버일때는 serviceWorker가 실행되지 못하기에 핸들러와의 bridge역할을 해서 모의응답값을 보내주도록 한다.
//pages/mocks/MSWComponent.tsx
import {setupServer} from "msw/node"
import {setupWorker} from "msw/browser"
const MSWComponent=()=>{
const setupHandler = () => {
if (typeof window === "undefined") {
const server = setupServer(...handlers);
server.listen();
} else {
const worker = setupWorker(...handlers);
worker.start();
}
setupHandler()
return null
}
export default MSWComponent
가장 최상단 파일에서 호출해주면 된다.
//pages/_app.tsx
const App=()=>{
return (
<div>
<MSWComponent/>
{children}
</div>
}
Module not found: Package path ./node is not exported from
package /.../node_modules/msw
(see exports field in /.../node_modules/msw/package.json)
...
14 | import { setupWorker } from "msw/browser";
> 15 | import { setupServer } from "msw/node";
| ^
설명을 읽어보면 클라이언트에서 setupServer가 실행되고
서버에서 setupWorker가 실행된다는 의미이다
typeof window로 분기를 쳐줬는데도 저 에러가 뜨는건 이해가 안된다
❌ 방법1) 웹팩에서 서버일때는 msw/browser 모듈을 읽지 않고 클라이언트일때는 msw/node 모듈을 읽지 않도록 강제한다.
//next.config.js
webpack: (config, { isServer }) => {
if (isServer) {
// next server build => ignore msw/browser
config.resolve.alias["msw/browser"] = false;
} else {
// browser => ignore msw/node
config.resolve.alias["msw/node"] = false;
}
...
return config;
},
};
위 방법으로 진행해서 에러는 사라졌지만 Mocking연결이 안됬다
⭕️ 방법2) 최상단이 아닌 실행시점에 모듈을 import한다 (+ 즉시실행)
//_app.tsx
if (typeof window === "undefined") {
(async () => {
const { setupServer } = await import("msw/node");
const server = setupServer(...handlers);
server.listen();
})();
} else {
(async () => {
const isServiceWorker = await navigator.serviceWorker.getRegistrations();
if (!isServiceWorker.length) {
const { setupWorker } = await import("msw/browser");
const worker = setupWorker(...handlers);
worker.start();
}
})();
}
⭕️ 방법3) 브라우저모듈은 (msw/browser)는 useEffect내부에서 import,
서버모듈은 (msw/node) instrumentation.ts파일에서 import
// mocks/MSWComponent.tsx
import { initMsw } from "mocks";
import { useEffect } from "react";
import { handlers } from "./handlers";
const MSWComponent = () => {
useEffect(() => {
const msw = async () => {
if (
typeof window !== "undefined" &&
process.env.NEXT_PUBLIC_REGION === "develop"
) {
navigator.serviceWorker.getRegistrations().then(async res => {
if (!!res.length) {
return;
} else {
const { setupWorker } = await import("msw/browser");
const worker = setupWorker(...handlers);
worker.start();
}
});
}
};
msw();
}, []);
return null;
};
export default MSWComponent;
navigator.serviceWorker.getRegistrations() 는 현재 등록된 serviceworker리스트를 보여주는 함수로 현재 등록된 serviceWorker가 없을때만 등록되도록 함
//pages/_app.tsx
const App=()=>{
return (
<div>
<MSWComponent/>
{children}
</div>
}
instrumentation은 next14기준 실험용기능이기 때문에 next-config.js에서 별도로 설정을 추가해줘야 한다.
//next-config.js
const nextConfig = {
experimental: {
instrumentationHook: true,
},
webpack(config) {
return config;
},
};
instrumentation은 서버 인스턴스가 실행될때 단 한번만 실행되는 곳으로 주로 로깅,모니터링관련 기능을 작업과정이 담긴다.
(단 한번만 실행됨으로 hot-reload시에는 실행되지 않음)
//instrumentation.ts
import { handlers } from "./mocks/handlers";
export async function register() {
if (
process.env.NEXT_RUNTIME === "nodejs" &&
process.env.NEXT_PUBLIC_REGION === "develop"
) {
const { setupServer } = await import("msw/node");
const server = setupServer(...handlers);
server.listen();
}
}
위 두 해결방안으로 하면 해당에러는 더이상 나타나지 않는다.
아래와 같은 화면을 콘솔창에서 확인한다면 Mocking할 준비가 되었다

다음 포스터에는 실제로 네트워크요청을 가로채서 미리 만들어둔 응답값을 리턴해보겠다.