loader(React Router)를 MSW와 함께 사용 시 주의점
React Router loader | React Router 공식 문서의 loader 링크
loader란 react router에서 path에 등록한 element를 랜더링하기 이전에 호출되는 함수다. react router 6.4버전 이후의 data router을 사용했을 때 loader를 사용할 수 있다.
...
createBrowserRouter([
{
element: <Teams />,
path: "teams",
loader: async () => {
return fakeDb.from("teams").select("*");
},
children: [
{
element: <Team />,
path: ":teamId",
loader: async ({ params }) => {
return fetch(`/api/teams/${params.teamId}.json`);
},
},
],
},
]);
...
MSW(Mock Service Worker) | MSW 공식 문서 링크
MSW는 API를 모킹할 수 있는 라이브러리다. MSW의 워커를 실행하면 호출하는 API들을 중간에서 가로채 처리를 할 수 있다. 보통 프로젝트의 진입점에서 현재 프로젝트가 실행되는 환경(개발 혹은 배포)에 분기를 두고 개발 모드 때 워커를 실행한다.
// 리액트의 진입점(엔트리 포인트) 파일 예시
...
if (process.env.NODE_ENV === 'development') {
const { worker } = await import('@/mocks/browser.ts');
await worker.start();
}
ReactDOM.createRoot(document.getElementById('root')!).render(<App />)
백에서 API 만드는 것을 기다리지 않고 프론트에서 API를 간단하게 모킹할 수 있기 때문에 백엔드 개발 일정과 독립적으로 개발할 수 있는 장점이 있다.
그런데 loader와 MSW를 함께 사용했을 때 문제가 생길 수 있다.
MSW가 초기화 되기 이전에 loader를 부르는 현상
MSW의 워커는 보통 앱의 엔트리 포인트에서 실행되도록 설계한다. 이 때 엔트리 포인트에서 router를 외부 파일에서 import를 할 경우 문제가 발생할 수 있다.
// 앱의 엔트리 포인트 파일 main.tsx
import React from "react";
import ReactDOM from "react-dom/client";
import { RouterProvider } from "react-router-dom";
import { router } from "./router.tsx";
if (process.env.NODE_ENV === "development") {
const { worker } = await import("../../mocks");
await worker.start();
}
ReactDOM.createRoot(document.getElementById("root")!).render(
<RouterProvider router={router} />
);
// router.tsx
import { createBrowserRouter } from "react-router-dom";
import { Root } from "./pages/Root.tsx";
import { fetchData } from "./apis/fetchData.ts";
export const router = createBrowserRouter([
{
path: "/",
element: <Root />,
loader: async () => {
return fetchData(); // 데이터를 받아올 비동기 함수
}
},
]);
ReactDOM이 랜더링하기 이전에 MSW 워커 실행을 await 했기 때문에 문제가 없어 보인다. 하지만 실제로 코드를 실행 시 MSW 워커 실행 이전에 loader함수가 호출되어서 API 모킹에 실패한다.
원인 | loader 실행 시점
loader가 호출되는 시점은 createBrowserRouter를 호출할 때이다. 위의 코드에서 router는 MSW 워커 실행 이전에 import되고 있기 때문에 loader 호출이 워커 실행 이전에 된다.
해결 1. MSW 워커 실행 이후 router 생성
import 없이 엔트리 포인트에서 라우터를 생성하는 방법이다. 라우터가 복잡해지고 다른 provider가 많아졌을 때 엔트리 포인트가 복잡해질 수 있다는 단점이 있다. 차선책으로는 라우터 객체 배열만 따로 import하는 방법도 있지만 추상화 수준을 맞추기 어려울 것이다.
// 방법 1. 엔트리 포인트에서 워커 실행 이후 라우터 생성
import React from "react";
import ReactDOM from "react-dom/client";
import { RouterProvider } from "react-router-dom";
if (process.env.NODE_ENV === "development") {
const { worker } = await import("../../mocks");
await worker.start();
}
const router = createBrowserRouter([
{
path: "/",
element: <Root />,
loader: async () => {
return fetchData(); // 데이터를 받아올 비동기 함수
}
},
]);
ReactDOM.createRoot(document.getElementById("root")!).render(
<RouterProvider router={router} />
);
해결 2. RouterProvider를 JSX로 분리해 함수 내부에서 router 생성
두번째 방법은 RouterProvider를 JSX로 분리하는 방법이다. MSW 워커 실행 이전에 import할 땐 함수가 아직 실행되지 않았으므로 loader를 미리 호출하지 않는다.
// 방법 2. RouterProvider를 JSX로 분리
import React from "react";
import ReactDOM from "react-dom/client";
import { RouterProvider } from "./providers";
if (process.env.NODE_ENV === "development") {
const { worker } = await import("../../mocks");
await worker.start();
}
ReactDOM.createRoot(document.getElementById("root")!).render(<RouterProvider />);
// /providers/RouterProvider.tsx
import { RouterProvider as Provider, createBrowserRouter } from 'react-router-dom';
export default function RouterProvider() {
const rootRouter = createBrowserRouter([
{
path: "/",
element: <Root />,
loader: async () => {
return fetchData(); // 데이터를 받아올 비동기 함수
}
},
]);
return <Provider router={rootRouter} />;
}
참고자료
- 동일한 문제가 MSW github repo의 issue에 등록되어 있다.
MSW does not mock APIS in react-router-6 loader in the first load · Issue #1653 · 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