SSR 구현해보기

jaehan·2023년 2월 8일
1

React

목록 보기
29/33
post-thumbnail

SSR이란?

Server Side Rendering이란 클라이언트가 매번 서버에게 데이터를 요청하여 서버에서 처리하는 방식
서버에서 렌더링 준비를 마친 html 파일과 js 코드를 브라우저에게 넘겨 준 뒤에
브라우저에서 html을 렌더링하고 js 파일을 다운 받아 html 코드와 연결하는 렌더링 방식

장점 : 검색엔진 최적화, 초기 구동 빠름, js 다운이 안되는 브라우저에서도 작동
단점 : TTV (Time To View), TTI (Time To Interact)의 시간이 달라서 처음에 작동이 안될 수 있음, 초기로딩이후 페이지 전환시 느림

❗️ Next.js를 사용해보기전에 기본적으로 코딩을 해보려고 한다

클라이언트 코드 작성

프로젝트 생성

mkdir test-ssr
cd test-ssr
npm init -y
npm install react react-dom

패키지 설치

npm install @babel/core @babel/preset-env @babel/preset-react
npm install webpack webpack-cli babel-loader clean-webpack-plugin html-webpack-plugin

src/About.js

import React from "react";

export default function About() {
  return (
    <div>
      <h3>{"This is about page"}</h3>
    </div>
  );
}

src/Home.js

import React from "react";

export default function Home() {
  return (
    <div>
      <h3>{"This is home page"}</h3>
    </div>
  );
}

src/App.js

import React, { useEffect, useState } from "react";
import About from "./About";
import Home from "./Home";

export default function App({ pages }) {
  const [page, setPage] = useState(pages);
  useEffect(() => {
    window.onpopstate = (event) => {
      setPage(event.state);
    };
  }, []);
  const onChagePage = (e) => {
    const newPage = e.target.dataset.page;
    window.history.pushState(newPage, "", `/${newPage}`);
    setPage(newPage);
  };
  const PageComponent = page === "home" ? Home : About;
  return (
    <div className={"container"}>
      <button data-page={"home"} onClick={onChagePage}>
        {"Home"}
      </button>
      <button data-page={"about"} onClick={onChagePage}>
        {"about"}
      </button>
      <PageComponent />
    </div>
  );
}

src/index.js

import React from "react";
import { render } from "react-dom";
import App from "./App";

// template/index.html의 root안에 App 컴포넌트 추가
render(<App pages={"home"} />, document.getElementById("root"));

웹팩 설정
webpack.config.js

const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");

module.exports = {
  entry: "./src/index.js",
  output: {
    filename: "[name].[chunkhash].js",
    path: path.resolve(__dirname, "dist"),
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        use: {
          loader: "babel-loader", // 모든 자바스크립트 파일을 바벨 로더로 처리
          },
        },
      },
    ],
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: "./template/index.html", // index.html파일을 기반으로 html파일 생성
    }),
  ],
  mode: "production",
};

바벨 설정
babel.config.js

// 바벨 로더 실행시에 적용됨
const presets = ["@babel/preset-react", "@babel/preset-env"];
const plugins = [];
module.exports = { presets, plugins };

template/index.html

<!DOCTYPE html>
<html>
  <head>
    <title>test-ssr</title>
  </head>
  <body>
    <div id="root"></div>
  </body>
</html>

위의 클라이언트 코드를 요약해 보자면 HomeAbout 컴포넌트를 가지고 있는 App컴포넌트를 index.js에서 template/index.html의 root 안에 넣어준다.
이 코드들을 webpack을 이용해서 dist폴더 아래에 index.html과 main.[hash값].js로 바벨을 이용해서 코드변환 한뒤에 압축해서 만들어 준다.

📌 file:///Users/[username]/test-ssr/dist/index.html경로를 브라우저에 넣어보면 App.js가 나온다 대신 url이 file로 시작해서 에러가 난다.

서버코드 작성

npm install express @babel/cli @babel/plugin-transform-modules-commonjs

  • 웹 서버를 띄우기 위해 express 설치
  • @babel/cli 서버에서 사용될 자바스크립트 파일을 컴파일 할 때 사용
  • @babel/plugin-transform-modules-commonjs : ESM으로 작성된 모듈 시스템을 commonJS로 변경하기 위해

웹서버 코드
src/server.js

import express from "express";
import fs from "fs";
import path from "path";
import { renderToString } from "react-dom/server"; // react-dom/server 안에 서버 사용되는 것들 모여있음
import React from "react";
import App from "./App";

const app = express(); // app변수(express 객체)를 이용해서 미들웨어와 url 경로 설정
const html = fs.readFileSync(
  // 웹팩 빌드후에 생성되는 dist의 index.html을 가져와서 이걸로 새로운 html만듬
  path.resolve(__dirname, "../dist/index.html"),
  "utf8"
);
app.use("/dist", express.static("dist")); // 웹팩으로 빌드한 자바스크립트 코드를 서비스함
app.get("/favicon.ico", (req, res) => res.sendStatus(204)); // 아래에서 favicon 안쓰도록
app.get("*", (req, res) => {
  const renderString = renderToString(<App pages="home" />); // app컴포넌트 렌더링
  const result = html.replace(
    // 렌더링된 결과를 index.html root 안에 반영
    '<div id="root></div>',
    `<div id="root>${renderString}</div>`
  );
  res.send(result); // 클라이언트에 전송
});
app.listen(3000); // 3000포트로 들어오는 요청을 기다림

바벨 설정
.babelrc.common.js
.babelrc.client.js
.babelrc.server.js

// 공통으로 사용되는 설정은 여기
const presets = ["@babel/preset-react"];
const plugins = [];
module.exports = { presets, plugins };

const config = require("./.babelrc.common.js"); // common을 가져와서 사용
config.presets.push("@babel/preset-env"); // 클라이언트에서 필요한 preset 추가
module.exports = config;

const config = require("./.babelrc.common.js");
config.plugins.push("@babel/plugin-transform-modules-commonjs"); // 서버에서 필요한 플러그인 추가
module.exports = config;

웹팩 설정
클라이언트에서 한거에서 publicPath, module.rules.use 만 바꾸면 된다
webpack.config.js

const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");

module.exports = {
  entry: "./src/index.js",
  output: {
    filename: "[name].[chunkhash].js",
    path: path.resolve(__dirname, "dist"),
    // SSR 할때 필요함, server.js 에서
    // /dist 로 시작하는 경우에만 dist 폴더 내의 파일을 서비스 하도록 설정해서
    publicPath: "/dist/",
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        use: {
          loader: "babel-loader", // 모든 자바스크립트 파일을 바벨 로더로 처리
          options: {
            // 웹팩을 클라이언트 코드에 대해서만 실행하기 때문에 클라이언트 설정으로 실행되도록 했음
            configFile: path.resolve(__dirname, ".babelrc.client.js"),
          },
        },
      },
    ],
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: "./template/index.html", // index.html파일을 기반으로 html파일 생성
    }),
  ],
  mode: "production",
};

package.json

"scripts": {
    // 서버측 코드 빌드 src밑에 있는 모든 파일을 babelrc.server 설정으로 컴파일
    "build-server":"babel src --out-dir dist-server --config-file ./.babelrc.server.js",
    // 서버와 클라이언트 둘다 빌드, 클라이언트는 웹팩 실행
    "build":"npm run build-server && webpack",
    // express 서버 띄움, build후 실행 해야함
    "start" :"node dist-server/server.js",
      ...
  },

src/index.js

import React from "react";
import { hydrate } from "react-dom";
import App from "./App";

// SSR 결과로 온 돔에 이벤트 처리함수 붙혀줌
hydrate(<App pages={"home"} />, document.getElementById("root"));

실행

npm run build
npm start

localhost:3000 들어가서 보면 잘 나오는것을 확인 할 수 있다.

💡 마무리

서버측 코드(server.js)를 요약해 보자면

  • app변수(express 객체)를 만들어서 미들웨어와 url 경로를 설정해 준다
  • 웹팩 빌드후에 생긴 dist/index.html을 가져온다
  • app.use("/dist", express.static("dist"))의 의미는
    url/dist로 들어와야 dist 폴더 내의 파일에 접근 할 수 있다는 것이다
    만약 앞의 문자열이 "/static"이면 url/static으로 들어와야 dist 폴더내의 파일에 접근하는 것이다.
    📌이래서 webpack 설정에서 publicPath를 추가해준것이다.
  • 자동으로 파비콘 가져오는 것을 막는 코드이고
  • App 컴포넌트를 renderToString으로 가져온뒤에 위에서 가져온 html의 root 안에 넣어준 뒤에 클라이언트에게 전송한다
  • listen(3000)은 3000포트로 들어오는 요청을 받는 다는 의미이다

0개의 댓글