회사에서 일을 하다보면 기획이나, 명세서를 100% 따라갈 수 없다는 걸 느낄 때가 있을 것이다.
어떤 기능을 추가 혹은 수정해서 언제까지 반영을 부탁한다는 말을 들었을 때 백엔드 팀원과의 눈치싸움이 시작된다.
기능을 추가하기 위해서는 프론트단 뿐만 아니라 백엔드단에서도 바뀌는 로직들과 추가되는 로직들이 있을 터라 만약 프론트와 백엔드가 병렬적으로 일처리가 불가능하다면 데드라인내에 못맞추는 불상사가 일어날 수 있다. 하지만 이러한 추가, 수정되는 기능들이 계속해서 생겨나자 나는 추가되는 ui를 먼저 처리하고, 벙찌는 시간들이 많아지는걸 느꼈다. 그래서 nextjs 프로젝트에 msw를 도입하고자 하였고, 그 과정에서 생긴 에러들을 정리할 예정이다. 물론 다른 프로젝트를 진행하다가 언제든 해당 에러들을 마주칠 수 있기에 메모의 느낌도 있긴하다.
트러블슈팅에 대한 기록을 주로 할 것이기 때문에, msw가 뭔지 왜쓰는지는 작성하지 않을 것이다. 다른 블로그들에 엄청나게 잘 정리해놓은 포스트들이 차고 넘친다. 그것들 보면 될 것 같다.
기초적인 msw라이브러리 설치, 보일러플레이트 작성은 아래의 순서대로 진행했다.
- npm i msw@latest --save-dev
- handlers 정의
- npx msw init public/ --save
- 브라우저용, 서버용 워커 분리해서 정의
- 노드 환경에 맞게 msw init 함수 정의
- MSWComponent 컴포넌트 생성
- provider에 MSWComponent 추가
트러블 슈팅
에러 1. Uncaught Error: Module not found: Can't resolve '_http_common'
에러 분석
이 에러는 "_http_common"모듈을 찾을 수 없기 때문에 발생한다. MSW가 @mswjs/interceptorsNode.js 환경에서 라이브러리를 사용하여 네트워크 요청을 가로챌 때 _http_common 모듈이 사용이 된다. 그러나 webpack은 서버환경에서의 모듈을 명시해주지 않는다면 자동으로 처리해주지 않는다고 한다. 따라서 next.config.ts 파일에 서버환경에서 추가될 모듈을 명시하여 런타임에서 처리하게끔 해주어야한다.
해결 방법
해당 이슈의 해결방법은 mswjs의 issues에서 찾아볼 수 있었다.
https://github.com/mswjs/msw/issues/2291
Bug: Module not found: Can't resolve '_http_common' in Next.js · Issue #2291 · mswjs/msw
Prerequisites I confirm my issue is not in the opened issues I confirm the Frequently Asked Questions didn't contain the answer to my issue Environment check I'm using the latest msw version I'm us...
github.com
webpack: (config, { isServer }) => {
if (isServer) {
config.externals = [...(config.externals || []), '_http_common'];
config.target = 'node';
}
return config;
}
isServer를 통해 브라우저환경이 아닌 서버환경에서만 적용되게 하고, externals에 '_http_common' 모듈을 명시해주어 런타임에서 실행할 수 있게끔 작성해준다. 또한 next 서버가 서버환경에서 node 모듈을 실행할 수 있게끔 타겟팅을 해주어 해결할 수 있었다.
에러 2. [MSW] Found a redundant "worker.start()" call.
콘솔에 위와 같은 경고가 발생
에러 분석
출력된 경고를 읽어보니 이미 실행된 worker가 여러번 초기화가 되어 발생한 경고였다.
해결 방법
// MSWComponent.tsx
// As-Is
'use client';
import { useEffect, useState } from 'react';
export const MSWComponent = ({ children }: { children: React.ReactNode }) => {
const [mswReady, setMswReady] = useState(false);
useEffect(() => {
const init = async () => {
const { initMsw } = await import('../mocks/index');
await initMsw();
setMswReady(true);
};
if (!mswReady) {
init();
}
}, [mswReady]);
if (!mswReady) {
return null;
}
return <>{children}</>;
};
// MSWComponent.tsx
// To-Be
'use client';
import { useEffect, useRef, useState } from 'react';
export const MSWComponent = ({ children }: { children: React.ReactNode }) => {
const [mswReady, setMswReady] = useState(false);
const hasStartedRef = useRef(false);
useEffect(() => {
const init = async () => {
if (hasStartedRef.current) return;
hasStartedRef.current = true;
const { initMsw } = await import('../mocks/index');
await initMsw();
setMswReady(true);
};
if (!mswReady) {
init();
}
}, [mswReady]);
return <>{children}</>;
};
위에서 useEffect의 콜백 함수로 MSW를 초기화 시켜주는 init함수에 useRef를 통해 MSW가 한번 초기화됐으면 바로 return되게 수정해주어 해결하였다.
에러 3. [MSW] Warning: intercepted a request without a matching request handler
에러 분석
MSW의 정의인 클라이언트가 HTTP 요청을 전송하면 Service Worker가 요청을 가로챈(intercept) 후 Mocking된 응답 값을 반환함으로써 서버와의 통신을 모방하는 방식인데 인터셉터 된 요청과 handler에 정의된 주소 중에 맞는게 없다는 경고라는 것이다. 어쩌면 당연하다 cdn이나 이미지 다운로드는 인터셉트를 할 생각을 안했기 때문에 handler에 정의해두지 않았다. 에러가 아니라 경고이긴 하지만 요청시마다 콘솔에 찍히는게 보기가 좋지 않았다.
해결 방법
아래의 handler는 msw 공식홈페이지의 기본 예제이다.
// handlers.ts
// As-Is
import { http, HttpResponse } from 'msw';
export const handlers = [
http.get('https://example.com/user', () => {
return HttpResponse.json({
id: 'c7b3d8e0-5e0b-4b0f-8b3a-3b9f4b3d3b3d',
firstName: 'John',
lastName: 'Maverick',
});
}),
];
// handlers.ts
// To-Be
import { delay, http, HttpResponse } from 'msw';
export const handlers = [
http.all('*', async () => {
await delay(100);
}),
http.get('https://example.com/user', () => {
return HttpResponse.json({
id: 'c7b3d8e0-5e0b-4b0f-8b3a-3b9f4b3d3b3d',
firstName: 'John',
lastName: 'Maverick',
});
}),
];
위의 코드처럼 모든 http요청에 대해 지연시간을 추가해주었다.
이렇게 했을 때 MSW는 해당 페이지에서 요청하는 모든 http요청들을 기다려야 하는 상황이 생기므로, 경고를 발생시키지 않는다. http.all('*')를 통해 모든 요청에 대해 적용되는 기본적인 처리를 설정해주었다.
MSW는 요청을 가로채고 나서 핸들러를 찾지 못했을 때 경고를 발생시키는데, 이 경우 모든 요청에 대해 처리가 지연되므로 경고가 발생하지 않는다. 또, MSW에서는 등록된 핸들러의 우선순위가 중요하다.
먼저 등록된 핸들러가 먼저 처리되며, http.all('*')와 같이 모든 요청을 처리하는 핸들러가 최우선으로 실행되고, 특정 URL 패턴에 대한 핸들러가 있으면 후에 우선적으로 처리된다.
즉, 지연시간을 추가한 경우 MSW는 모든 요청에 대해 기다리는 시간을 가지게 되어 경고가 발생하지 않는다.
하지만 해당 방법이 베스트인지는 잘 모르겠다. 뭔가 임시로 delay줘서 경고를 막는식으로 보여서 다른 방법도 찾아봐야 할 것으로 보인다.
에러 4. 모킹api 404 에러
에러 분석
컴포넌트가 이미 렌더링 된 후 MSW init이 실행되는 문제이다.
콘솔에 찍히는 것과 같이 렌더링이 된 후 빈배열로 콘솔이 찍히고, 그 후에 mocking이 enabled가 된 것을 볼 수 있다.
즉, 컴포넌트가 렌더링되면서 api요청이 발생되는데 그때 MSW가 초기화되어 있지 않아서 요청을 보내도 msw가 가로챌 수가 없어서 404에러가 발생한 것이다.
해결 방법
// MSWComponent.tsx
// As-Is
'use client';
import { useEffect, useRef, useState } from 'react';
export const MSWComponent = ({ children }: { children: React.ReactNode }) => {
const [mswReady, setMswReady] = useState(false);
const hasStartedRef = useRef(false);
useEffect(() => {
const init = async () => {
if (hasStartedRef.current) return;
hasStartedRef.current = true;
const { initMsw } = await import('../mocks/index');
await initMsw();
setMswReady(true);
};
if (!mswReady) {
init();
}
}, [mswReady]);
return <>{children}</>;
};
// MSWComponent.tsx
// To-Be
'use client';
import { env } from '@/lib/env';
import { useEffect, useRef, useState } from 'react';
const isMockingMode = env.API_MOCKING === 'enabled';
export const MSWComponent = ({ children }: { children: React.ReactNode }) => {
const [mswReady, setMswReady] = useState(() => !isMockingMode);
const hasStartedRef = useRef(false);
useEffect(() => {
const init = async () => {
if (hasStartedRef.current) return;
hasStartedRef.current = true;
if (isMockingMode) {
const initMsw = await import('../mocks/index').then((res) => res.initMsw);
await initMsw();
setMswReady(true);
}
};
if (!mswReady) {
init();
}
}, [mswReady]);
if (!mswReady) {
return null;
}
return <>{children}</>;
};
현재 발생하고 있는 문제점인 MSW가 enabled가 되기전에 요청을 보내는 것을 MSW가 초기화 된 후에 컴포넌트를 렌더링시킨다면 해결 될 것이다.
msw는 layout에서 children을 provider로 감싸고 있기 때문에 컴포넌트를 초기화하는 로직을 먼저 실행시키고, 초기화가 됐을 때 children을 렌더링하게끔 해주려고 한다.
이전의 코드는 mswReady 상태가 false일때만 init함수를 호출하여 초기화를 시켜주는 로직만 존재했었다.
하지만 수정된 코드에서는 env.local를 통해 개발/운영 모드를 분기처리하고, 개발모드에서는 mswReady의 초기값을 false로 두고, null을 리턴하여 감싸고 있는 children을 렌더링하지 않고 있는다. 그리고 useEffect의 콜백을 통해 init함수를 호출하여mswReady상태를 true로 바꿔줌으로써 해당 상태변경으로 인한 리렌더링은 null이 아닌 children을 렌더링하게 된다.
운영모드에서는 isMockingMode가 false 이기때문에 mswReady의 초기상태값은 true가 된다. 따라서 init함수를 호출하지 않고 바로 children을 렌더링시켜주게 된다. 이렇게 개발모드에서만 msw init이 호출되게 만들어주었다.
이렇게 msw를 도입하면서 겪은 이슈들을 분석해보고 해결한 일대기를 작성해보았다.
나의 해결방법이 진정한 해결방법이 아닐수도 있다. 먼저, 에러와 경고없이 정상적으로 msw가 실행되고, 요청 인터셉터가 되었으니 사용해보면서 여러가지 케이스들이 생길 때 다시한번 에러가 생긴다면 해결방법을 재점검해보는 식으로 사용할 예정이다.