ReactJS를 사용한 CSR VS SSR

Feb 02 2023
아마도 가장 중요하고 근본적인 결정 중 하나는 새 웹 앱 설계를 시작할 때 웹 애플리케이션을 렌더링하는 방법일 것입니다. 그래서 오늘 저는 두 가지 렌더링 방법인 클라이언트 측 렌더링과 서버 측 렌더링에 대해 이야기하고자 합니다.

아마도 가장 중요하고 근본적인 결정 중 하나는 새 웹 앱 설계를 시작할 때 웹 애플리케이션을 렌더링하는 방법일 것입니다. 그래서 오늘 저는 두 가지 렌더링 방법인 클라이언트 측 렌더링 과 서버 측 렌더링에 대해 이야기하고자 합니다. 물론 이 두 가지 패턴만 있는 것은 아니며 프로그레시브 렌더링 및 기타 패턴에 대해서는 가까운 시일 내에 논의할 것입니다.

내가 사용할 용어

표현

  • SSR: Server-Side Rendering — 클라이언트 측 또는 범용 앱을 서버의 HTML로 렌더링합니다.
  • CSR: Client-Side Rendering — 일반적으로 DOM을 사용하여 브라우저에서 앱을 렌더링합니다.
  • Rehydration: 서버에서 렌더링된 HTML의 DOM 트리 및 데이터를 재사용하도록 클라이언트에서 JavaScript 보기를 "부팅"합니다.
  • FP: 첫 번째 페인트 — 모든 픽셀이 사용자에게 처음으로 표시되는 시간입니다.
  • FCP: First Contentful Paint — 요청한 콘텐츠(기사 본문 등)가 표시되는 시간입니다.
  • TTI: Time To Interactive — 페이지가 상호작용하는 시간(연결된 이벤트 등).
  • TTFB: Time to First Byte — 링크 입력과 들어오는 콘텐츠의 첫 번째 비트 사이의 시간

CSR — 구조.

서버에 빈 div 태그와 번들 js가 있는 html이 있다고 가정합니다. CSR 렌더링은 다음과 같습니다.

html은 단일 루트 div 태그로 구성되며 DOM 표시 및 업데이트와 같은 다른 모든 작업은 JS에서 처리합니다.

번들 및 성능

성능을 생각해보면 페이지의 복잡도가 높아질수록 JS의 복잡도와 크기도 커지고 서버에서 가져오는 시간도 늘어난다는 것을 먼저 보게 된다. 그러면 페이지의 FCP 및 TTI가 지연됩니다.

장점과 단점

장점

  • CSR은 페이지를 새로 고치지 않고 탐색할 수 있도록 지원하는 SPA를 갖는 방법을 제공합니다. 이것은 오랫동안 큰 문제였으며 이제 웹 사이트가 동적 애플리케이션처럼 작동하기 시작할 수 있습니다.

그러나 몇 가지 함정이 있습니다.

  • 성능. 페이지의 복잡성이 증가함에 따라 JS가 FCP를 볼 때까지 더 기다려야 합니다 .
  • 서. 사용자 방문자와 마찬가지로 봇은 페이지 보기를 요청합니다. 이것은 크롤링으로 잘 알려진 프로세스입니다. 첫 번째 단계에서 Googlebot은 페이지의 소스 코드를 크롤링하고 표시되는 모든(자바스크립트가 아닌) 콘텐츠의 색인을 생성합니다. 그러나 DOM을 빌드하기 위해 JS가 실행될 때까지는 아무것도 보이지 않습니다. 이 렌더링 단계에서 그는 다시 와서(때때로 그 시간은 몇 달이 걸립니다) 이제 크롤링에 사용할 수 있는 페이지의 실제 콘텐츠를 가져오고 결국에는 인덱스에 추가합니다.
  • 게으른 로딩. 지연 로딩을 사용하면 중요하지 않은 리소스 로딩을 피할 수 있습니다. 이 접근 방식은 초기 페이지 로드 시간을 줄입니다.
  • 적극적인 코드 분할. 런타임 중에 동적으로 로드할 수 있는 여러 번들을 만드는 데 도움이 됩니다. 또한 JS 리소스를 지연 로드할 수 있습니다. 코드 분할은 Turbopack 또는 Webpack 과 같은 번들러에서 지원됩니다 .
  • 서비스 작업자와 캐싱 . 응용 프로그램 셸을 오프라인으로 캐시하고 성능 시간을 늘리는 데 사용할 수 있습니다.

SSR 이 무엇인지 알아보겠습니다 . 사용자가 요청하는 동안 렌더링할 페이지 콘텐츠에 대한 전체 HTML을 생성하는 가장 오래된 방법입니다. 이 구현을 위해 우리는 서버 측에서 html을 빌드하고 브라우저가 FCP를 쉽게 빌드할 수 있도록 브라우저로 보내야 합니다 .

브라우저는 서버에 요청을 하고 빈 루트뿐만 아니라 그 안에 있는 콘텐츠로 생성된 html을 가져옵니다. 따라서 브라우저는 가능한 한 빨리 콘텐츠를 표시합니다. 이제 브라우저가 번들 다운로드를 시작하고 사용자는 여전히 화면에 콘텐츠를 볼 수 있지만 아직 대화형은 아닙니다. 번들이 다운로드된 후에만 이벤트 리스너를 html에 연결하고 대화형이 됩니다. 이 과정을 수화 또는 재수화 라고 합니다 .

SSR — 장단점

장점

더 적은 Javascript는 더 빠른 FCP 및 TTI를 위해 브라우저를 활성화합니다.

서버 사이드 렌더링

클라이언트 FCP에 많은 Javascript를 보내는 것을 피하고 있고 사용자는 TTI를 기다리고 있기 때문입니다. 페이지에 많은 UI 요소가 있을 때 SSR은 CSR보다 JS가 훨씬 적기 때문에 스크립트를 가져오는 시간이 더 적습니다. 때로는 FP=FCP=TTI가 있습니다.

SEO

검색 엔진 봇은 첫 번째 단계에서 콘텐츠를 쉽게 가져오고 있으므로 이제 페이지를 인덱싱할 수 있습니다. 이제 우리는 페이지에서 꽤 좋은 SEO를 얻고 있습니다.

단점

때때로 우리는 느린 TTFB를 가질 수 있습니다 . 이는 사용자에게 느린 네트워크가 있거나 서버 코드가 최적화되지 않은 시나리오입니다.

모든 코드를 클라이언트 측에서 사용할 수 없기 때문에 서버에 대한 빈번한 호출로 인해 전체 페이지가 다시 로드되고 때때로 상호 작용 사이의 시간이 길어집니다. 따라서 SPA는 SSR과 함께 사용할 수 없습니다 .

Express 로 간단한 React SSR 앱을 만들어 봅시다 .

npx create-react-app ssr-react

컴포넌트를 작성해 봅시다: 증가 및 감소 방법이 있는 카운터.

import React, { useState } from "react";

const App = ({ initialValue }) => {
  const [count, setCount] = useState(initialValue);
  return (
    <>
      <div className="App">
        <button onClick={() => setCount(count + 1)}>Increase</button>
        <div style={{ margin: "20px" }}>{count}</div>
        <button onClick={() => setCount(count - 1)}>Decrease</button>
      </div>
    </>
  );
};


export default App;

npm i express --save

import express from "express";
const app = express();
app.listen(3000, function () {
  console.log("server running on 3000");
});

import express from "express";
import fs from "fs";
import path from "path";
import App from "../src/App";
import React from "react";
import ReactDOMServer from "react-dom/server";
const app = express();

app.get("/page", async (req, res) => {
  fs.readFile(path.resolve("./build/index.html"), "utf-8", (err, data) => {
    if (err) {
      return res.status(500).send("Error happened");
    }
    const html = `<div id="root">${ReactDOMServer.renderToString(
      <App />
    )}</div>`;
    return res.send(data.replace('<div id="root"></div>', html));
  });
});

app.use(express.static(path.resolve(__dirname, "..", "build")));

app.listen(3000, function () {
  console.log("server running on 3000");
});

따라서 우리는 서버 측에서 JSX를 사용하고 있으며 이를 지원해야 하므로 babel을 추가해야 합니다.

npm i @babel/preset-env @babel/preset-react @babel/register ignore-styles --save-dev

require("ignore-styles");

require("@babel/register")({
  ignore: [/(node_module)/],
  presets: ["@babel/preset-env", "@babel/preset-react"],
});

require("./server");


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

hydrateRoot(document.getElementById("root"), <App initialValue={10} />);

"ssr": "node server/index.js",

그게 다야.

Github 소스 코드는 여기에 있습니다 .