Next.js 서버 컴포넌트에서 MSW로 모킹한 데이터 React-query로 가져오기

sangsang·2024년 9월 1일
1

개요

Aigoo 프로젝트에서 모의 데이터를 사용하기 위해 MSW를 도입했습니다. React 환경에서는 MSW를 쉽게 사용할 수 있었지만, Next.js의 서버 컴포넌트에서 MSW를 사용할 때 예상치 못한 문제가 발생했습니다. 서버 컴포넌트에서 MSW가 undefined 값을 반환하는 문제를 해결하기 위해 Next.js의 서버 렌더링 과정을 파악하고 해결 방안을 찾아 보았습니다.

기술 스택

Next.js

  • app route 버전 사용
  • 서버 컴포넌트를 통한 서버 사이드 렌더링 적용

MSW(Mock Service Worker)

  • 네트워크 요청을 가로채서 모의(mock) 데이터를 반환
  • 클라이언트 및 node.js 모두에서 동작 가능
  • 특정 API 엔드포인트에 대한 모의 응답 설정 가능

React Query(tanstack query)

  • 5 버전 사용
  • prefetch, caching, refetch 등의 기능 제공

서버 컴포넌트에서 undefined 반환

서버 컴포넌트 환경에서 MSW를 사용해 모의 데이터를 가져오려고 했을 때 문제가 발생했습니다. MSW는 정상적으로 API 요청을 intercept했으나, 서버 컴포넌트에서 데이터 페칭 결과가 undefined로 반환되었습니다.

원인 분석

아래 원인들을 분석해본 결과, 한마디로 서버 컴포넌트 랜더링 과정에서 MSW가 요청을 intercept하는 방식이 맞지 않는다는 것이었다. 서버에서 네트워크 요청을 가로채는 서버 사이드 모킹 솔루션을 적용해야 했습니다.

  1. Next.js 서버 컴포넌트는 서버에서 실행되고, 클라이언트 컴포넌트는 브라우저에서 실행됩니다.

  2. MSW는 브라우저 환경에서 네트워크 요청을 가로채도록 설계되었습니다.

  3. 서버 컴포넌트에서 useQuery를 사용할 때 데이터 페칭은 페이지가 클라이언트로 전달되기 전에 서버에서 발생합니다. 이 시점에서는 MSW가 브라우저 환경에 존재하지 않기 때문에 활성화되지 않습니다.

  4. MSW는 자바스크립트가 브라우저에서 실행될 때 네트워크 요청 가로채기 메커니즘을 설정합니다. 이는 서버가 이미 컴포넌트를 렌더링하고 데이터를 페칭한 이후에 발생합니다.

  5. React Query의 useQuery 훅은 클라이언트 사이드 데이터 페칭을 위해 설계되었습니다. 이를 서버 컴포넌트에서 사용하면 기대한 대로 작동하지 않을 수 있습니다.

서버 컴포넌트 랜더링 과정

이 문제를 해결하기 위해 서버 컴포넌트의 렌더링 과정에 대한 이해가 필요했습니다. 서버 사이드 렌더링, 디하이드레이션, 하이드레이션 과정을 이해하며 어떻게 문제를 해결해야 할지 인사이트를 얻었습니다.

  1. 서버 사이드 렌더링

    • 서버가 페이지 요청을 받습니다.
    • 서버에서 React 컴포넌트를 렌더링하여 HTML을 생성합니다.
    • 이 HTML과 필요한 데이터가 클라이언트로 전송되어 빠른 초기 페이지 로드를 가능하게 합니다.
  2. Dehydration (서버에서 렌더링 후 발생)

    • 서버가 컴포넌트를 렌더링하고 필요한 데이터를 가져온 후, Dehydration을 수행합니다.
    • 디하이드레이션은 데이터를 직렬화하는 과정입니다.
    • 직렬화된 데이터는 HTML 응답에 포함되며, 일반적으로 페이지 내의 JSON 객체로 삽입됩니다.
  3. 초기 클라이언트 로드 (브라우저에서 발생)

    • 브라우저는 서버에서 미리 렌더링된 HTML을 표시합니다.
  4. Hydration (JavaScript 번들이 로드된 후 클라이언트에서 발생)

    • JavaScript 번들이 로드된 후, 하이드레이션 과정이 시작됩니다.
    • 하이드레이션이 완료되면 애플리케이션은 사용자와 상호작용이 가능합니다.

    하이드레이션 과정
    1) 서버에서 렌더링된 정적 DOM에 JavaScript 이벤트 리스너를 연결한다.
    2) 서버에서 제공된 HTML과 클라이언트 동기화합니다.
    3) 서버에서 직렬화된 데이터를 클라이언트로 가져와 컴포넌트 상태에 반영한다.

해결 방법

React Query의useQuery 대신 prefetchQuery를 사용하여 미리 서버에서 데이터를 가져온 후, dehydratehydrate를 통해 데이터를 직렬화/역직렬화해 처리하는 방식을 사용했습니다.

과정

  1. Next.js의 로더 함수에서 QueryClient를 생성합니다.
  2. prefetchQuery 메서드를 사용하여 데이터를 서버에서 미리 가져옵니다.
  3. 프리페치된 데이터를 dehydrate하여 클라이언트 측에 전달합니다.
  4. Next.js의 서버 컴포넌트는 미리 가져온 데이터를 클라이언트에서 hydrate하여 화면에 보여줍니다.
// hook/queries/useLoaderData.ts

import { getStudyMateirals } from "@/lib/api/api";
import { dehydrate, DehydratedState, QueryClient } from "@tanstack/react-query";

interface LoaderData {
  dehydratedState: DehydratedState;
}

const useLoaderData = async (): Promise<LoaderData> => {
  const queryClient = new QueryClient();

  await queryClient.prefetchQuery({
    queryKey: ["material"],
    queryFn: async () => {
      const data = await getStudyMateirals();
      return data.data.studyDatas;
    },
  });

  return {
    dehydratedState: dehydrate(queryClient),
  };
};

export default useLoaderData;

HydrationBoundary를 Root에 감싸주고, state에 queryClient를 dehydrate(직렬화)시켜 전달합니다.
이렇게 되면 HydrationBoundary의 children 컴포넌트들은 모두 prefetch된 post 데이터를 사용할 수 있습니다.

// app/notes.page.tsx

import Materials from "@/components/material/Materials";
import useLoaderData from "@/hooks/queries/useLoaderData";
import { HydrationBoundary } from "@tanstack/react-query";

const Material = async () => {
  const { dehydratedState } = await useLoaderData();

  return (
    <HydrationBoundary state={dehydratedState}>
      <Materials />
    </HydrationBoundary>
  );
};

export default Material;

결과

MSW Service worker로 부터 데이터를 응답 받을 것을 확인할 수 있었습니다.

시도한 방법들

이 문제를 해결하기 위해 서버 컴포넌트에서 API 요청을 모의하는 방법을 변경해야 합니다.

여러 글을 참고하며 적용해보았지만, 잘 해결되지 않았다. 추후에 다시 한번 시도해보기 위해서 간략하게 내용을 남겨 놓았습니다.

1. next의 instrumentation을 이용

src/instumentation.ts

export async function register() {
  if (process.env.NEXT_RUNTIME === "nodejs") {
    const { server } = await import("./mocks/server");
    server.listen();
  }
}

next.config.mjs

experimental: { instrumentationHook: true },
  webpack(config, { isServer }) {
    if (isServer) {
      if (Array.isArray(config.resolve.alias)) {
        config.resolve.alias.push({ name: "msw/browser", alias: false });
      } else {
        config.resolve.alias["msw/browser"] = false;
      }
    } else {
      if (Array.isArray(config.resolve.alias)) {
        config.resolve.alias.push({ name: "msw/node", alias: false });
      } else {
        config.resolve.alias["msw/node"] = false;
      }
    }

    return config;
  },

2. express 서버 구축해서 서버사이드에서 일어나는 데이터 패칭 로직을 가로채기

src/mocks/http.ts

import { createMiddleware } from '@mswjs/http-middleware';
import express from 'express';
import cors from 'cors';
import { handlers } from './handlers';

const app = express();
const port = 9090;

app.use(cors({ origin: 'http://localhost:3000', optionsSuccessStatus: 200, credentials: true }));
app.use(express.json());
app.use(createMiddleware(...handlers));
app.listen(port, () => console.log(`Mock server is running on port: ${port}`));

package.json

{
  "scripts": {
    "dev": "next dev",
    "mock": "npx tsx ./mocks/http.ts",
  }
}

axios settings:

axios.defaults.baseURL = 'http://localhost:9090';
start mock server yarn mock, start dev server yarn dev
https://github.com/mswjs/msw/issues/1644#issuecomment-1750722052

마무리

이번 문제를 해결하면서 Next.js 서버 컴포넌트와 MSW의 동작 방식을 이해하게 되었습니다. React query의 prefetchQuery를 사용해서 서버사이드에서 데이터를 Prefetch하고 클라이언트에서 Hydration을 통해 서버 사이드에서 데이터 처리하는 방식도 배울 수 있었습니다.

공식 문서들을 참고하고, 여러 관련 글을 읽어보면서 정확한 내용을 정리하려고 노력했지만, 미흡한 부분이 있을 수 있습니다. 혹시 글을 읽으시면서 잘못된 부분이 있거나, 보완이 필요한 부분이 있다면 댓글 남겨주시면 도움이 될 것 같습니다.

참고

profile
개발이 너무 좋다. 정말 잘 하고 싶다.

2개의 댓글

comment-user-thumbnail
2024년 11월 28일

좋은 글 잘 읽었습니다 도움이 되었습니다 감사합니다.

1개의 답글

관련 채용 정보