React 18: 서스펜스가 있는 스트리밍 SSR 및 서버에서 데이터 가져오기(방법)

문제



React 18에서 Suspense로 스트리밍 SSR을 사용하여 서버에서 데이터 가져오기를 시도하면 문제에 직면하게 되는데, 바로 수화 불일치입니다. 여기에서 이를 해결하는 방법을 설명합니다(here에서 추출한 솔루션).

해결책



다음은 서버 앱의 코드입니다.

import express from "express";
import { renderToPipeableStream } from "react-dom/server";
import React from "react";
import AppServer from "../src/components/AppServer";
import path from "path";
import { DataProvider, data } from "../src/providers/data";
import { createServerData } from "../src/api/resource";
import { Writable } from "node:stream";

const app = express();
const port = 3000;
app.get("/", (req, res) => {
  const stream = new Writable({
    write(chunk, _encoding, cb) {
      res.write(chunk, cb);
    },
    final() {
      res.write(
        `<script>
        window.globalCache={comments:[${data.comments.map((c) => `'${c}'`)}]}
        </script>`
      );
      res.end("</body></html>");
    },
  });
  const { pipe } = renderToPipeableStream(
    <DataProvider data={createServerData()}>
      <AppServer />
    </DataProvider>,
    {
      bootstrapScripts: ["/main.js"],
      onShellReady() {
        res.write("<html><body>");
        pipe(stream);
      },
    }
  );
});

app.use(express.static(path.join(__dirname, "/../dist")));

app.listen(port, () => {
  console.log(`app running on port ${port}`);
});


여기서 핵심은 다음과 같습니다.

 const stream = new Writable({
    write(chunk, _encoding, cb) {
      res.write(chunk, cb);
    },
    final() {
      res.write(
        `<script>
        window.globalCache={comments:[${data.comments.map((c) => `'${c}'`)}]}
        </script>`
      );
      res.end("</body></html>");
    },
  });


우리는 브라우저의 globalCache 변수를 서버의 데이터로 채우기 위해 스트리밍 끝에 스크립트를 작성하고 있습니다.
data의 출처는 다음과 같습니다.

import React, { createContext, useContext } from "react";

export let data;

const DataContext = createContext(null);

export function DataProvider({ children, data }) {
  return <DataContext.Provider value={data}>{children}</DataContext.Provider>;
}

export function useData() {
  const ctx = useContext(DataContext);
  if (ctx) {
    data = ctx.read();
  } else {
    data = window.globalCache;
  }
  return data;
}


서버에서는 data를 컨텍스트에서 읽고 브라우저에서는 globalCache 변수에서 읽습니다. 그것이 수화 불일치 문제를 피하는 방법입니다.
createServerData 함수를 살펴보겠습니다.

export function createServerData() {
    let done = false;
    let promise = null;
    let value
    return {
      read: ()=> {
        if (done) {
          return value
        }
        if (promise) {
          throw promise;
        }
        promise = new Promise((resolve) => {
          setTimeout(() => {
            done = true;
            promise = null;
            value={comments:['a','b','c']}
            resolve()
          }, 6000);
        });
        throw promise;
      }
    };
  }

6000ms 내에 해결되는 약속입니다.

이제 useData 구성 요소에서 Comments 후크를 사용하는 위치를 살펴보겠습니다.

import React from "react";
import { useData } from "../providers/data";

export default function Comments() {
  const { comments } = useData();

  return (
    <ul>
      {comments && comments.map((comment, i) => <li key={i}>{comment}</li>)}
    </ul>
  );
}


서버에서는 data에서 Context를 읽고 브라우저에서는 전역 변수 data에서 globalCache를 읽습니다. 이는 브라우저에서 컨텍스트가 정의되지 않기 때문입니다. 즉, 브라우저의 경우 App 구성 요소를 DataProvider로 래핑하지 않기 때문입니다.

import React from "react";
import { hydrateRoot } from "react-dom/client";
import App from "./components/App";

hydrateRoot(document.getElementById("root"), <App />);

App 구성 요소는 다음과 같습니다.

import React, { Suspense, lazy } from "react";

const Comments = lazy(() => import("./Comments"));

const App = () => (
  <>
    <Suspense fallback={<div>loading...</div>}>
      <Comments />
    </Suspense>
  </>
);

export default App;


그리고 위에서 (서버에서) 사용된 AppServer 구성 요소는 다음과 같습니다.

import React from "react";
import App from "./App";

const AppServer = () => (
      <div id="root"> 
        <App />
       </div> 
);

export default AppServer;


이것으로 Suspense로 스트리밍 SSR을 수행하는 방법과 React 18의 서버에서 데이터 가져오기를 수행하여 수화 불일치 문제를 피하는 방법에 대한 이 예제의 모든 코드를 보았습니다.

좋은 웹페이지 즐겨찾기