2 분 소요

✅ 코드 흐름으로 SSR 이해

코드를 한줄씩 따라가며 SSR이 어떻게 동작하는지 이해하자.

1. 브라우저에서 http://localhost:3000 으로 접근하여 index.html 요청한다.
2. SSR서버는 router.get(”/”) 으로 요청을 받은 후 API 서버로 데이터 요청한다.
3. 스크립트 태그를 이용하여 응답받은 데이터를 window 전역 객체에 초기화한다.
4. renderToString 함수를 통해 React 트리를 HTML 문자열로 변환한다.
5. SSR서버에 존재하는 index.html 파일을 찾아 string으로 읽어온다.
6. 앞에서 가져온 App 컴포넌트와 API 응답 데이터를 주입하는 initData를 index.html 파일에 주입한다.
7. 변환된 HTML 파일을 브라우저에 응답한다. (res.send)
8. 브라우저는 응답받아 HTML을 파싱하며 bundle.js를 불러오는 스크립트 태그를 만나면 client/index.js 를 다운받아 실행한다.
9. index.js 에서 만드는 React DOM과 SSR 서버에서 만든 HTML을 일치시키기 위해 window 전역 객체에서 데이터를 가져와 주입한다.
10. 서버에서 만든 정적 HTML에 컴포넌트 로직을 결합시켜 상호작용이 가능하도록 하기 위해 hydration 한다.
1. 브라우저에서 http://localhost:3000 으로 접근하여 index.html 요청한다.

index.html 첫 로드

2. SSR서버는 router.get(”/”) 으로 요청을 받은 후 API 서버로 데이터 요청한다.
router.get("/", async (_, res) => {
  const movies = await fetchMovies();
  const movieItems = parseMovieItems(movies);
  const bestMovieItem = movieItems[0];
  ...
}
3. 스크립트 태그를 이용하여 응답받은 데이터를 window 전역 객체에 초기화한다.
router.get("/", async (_, res) => {
	...
  const initData = /*html*/ `
    <script>
      window.__INITIAL_DATA__ = {
        movies: ${JSON.stringify(movieItems)}
      }
    </script>
  `;
	...
});
4. `renderToString` 함수를 통해 React 트리를 HTML 문자열로 변환한다.
router.get("/", async (_, res) => {
	...
  const renderedApp = renderToString(
    <App popularMovies={movieItems} bestMovieItem={bestMovieItem} />
  );
  ...
});
5. SSR서버에 존재하는 index.html 파일을 찾아 string으로 읽어온다.
const router = Router();

router.get("/", async (_, res) => {
	...
  const templatePath = path.resolve(__dirname, "index.html");
  const template = fs.readFileSync(templatePath, "utf-8");
  ...
});
6. 앞에서 가져온 App 컴포넌트와 API 응답 데이터를 주입하는 initData를 index.html 파일에 주입한다.
7. 변환된 HTML 파일을 브라우저에 응답한다. (res.send)
router.get("/", async (_, res) => {
  ...
  res.send(
    template
      .replace('<div id="root"></div>', `<div id="root">${renderedApp}</div>`)
      .replace("<!--${INIT_DATA_AREA}-->", initData)
  );
});
8. 브라우저는 응답받아 HTML을 파싱하며 bundle.js를 불러오는 스크립트 태그를 만나면 client/index.js 를 다운받아 실행한다.
9. index.js 에서 만드는 React DOM과 SSR 서버에서 만든 HTML을 일치시키기 위해 window 전역 객체에서 데이터를 가져와 주입한다.
10. 서버에서 만든 정적 HTML에 컴포넌트 로직을 결합시켜 상호작용이 가능하도록 하기 위해 hydration 한다.
import React from "react";
import { hydrateRoot } from "react-dom/client";
import App from "./App";

const data = window.__INITIAL_DATA__;

hydrateRoot(
  document.getElementById("root"),
  <App popularMovies={data.movies} bestMovieItem={data.movies[0]} />
);

✅ 결과 코드

🚨 index.js
router.get("/", async (_, res) => {
  const movies = await fetchMovies();
  const movieItems = parseMovieItems(movies);
  const bestMovieItem = movieItems[0];

const initData = /_html_/ `

  <script>
    window.__INITIAL_DATA__ = {
      movies: ${JSON.stringify(movieItems)}
    }
  </script>

`;

const renderedApp = renderToString(
<App popularMovies={movieItems} bestMovieItem={bestMovieItem} />
);

const templatePath = path.resolve(\_\_dirname, "index.html");
const template = fs.readFileSync(templatePath, "utf-8");

res.send(
template
.replace('<div id="root"></div>', `<div id="root">${renderedApp}</div>`)
.replace("<!--${INIT_DATA_AREA}-->", initData)
);
});

🚨 index.html
<!DOCTYPE html>
<html lang="ko">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <link rel="stylesheet" href="./static/styles/index.css" />
    <title>영화 리뷰</title>
  </head>
  <body>
    <div id="root"></div>
  </body>
  <!--${INIT_DATA_AREA}-->
</html>
🚨 client/index.js
import React from "react";
import { hydrateRoot } from "react-dom/client";
import App from "./App";

const data = window.__INITIAL_DATA__;

hydrateRoot(
  document.getElementById("root"),
  <App popularMovies={data.movies} bestMovieItem={data.movies[0]} />
);

webpack으로 번들링하면서 script 태그가 추가된 것을 확인할 수 있다.

bundle 불러오는 스크립트

📘 reference

React - renderToString

댓글남기기