관리 메뉴

거니의 velog

(3) 서버 사이드 렌더링 3 본문

React_백엔드 프로그래밍

(3) 서버 사이드 렌더링 3

Unlimited00 2023. 12. 15. 10:34

(4) 서버 코드 작성하기

* 서버 사이드 렌더링을 처리할 서버를 작성해 보자. Express라는 Node.js 웹 프레임워크를 사용하여 웹 서버를 만들어본다. 이 과정은 꼭 Express가 아니어도 상관없으며, Koa, Hapi또는 connect 라이브러리를 사용하면 구현할 수 있다. 우리는 서버 사이드 렌더링을 할 때 Express를 사용한 이유는 해당 프레임워크가 사용률이 가장 높고, 추후 정적 파일들을 호스팅할 때도 쉽게 구현할 수 있기 때문이다.

* 먼저 yarn을 사용하여 Express를 설치해 주자.

$ yarn add express

* react-router-dom 버전을 5로 다운그레이드 한다.

$ yarn remove react-router-dom
$ yarn add react-router-dom@5

* 다음으로 index.server.js 코드를 다음과 같이 작성하자.

import React from "react";
import ReactDOMServer from "react-dom/server";
import express from "express";
import { StaticRouter } from "react-router-dom";
import App from "./App";

const app = express();

// 서버 사이드 렌더링을 처리할 핸들러 함수이다.
const serverRender = (req, res, next) => {
  // 이 함수는 404가 떠야 하는 상황에서 404를 띄우지 않고 서버 사이드 렌더링을 해 준다.

  const context = {};
  const jsx = (
    <StaticRouter location={req.url} context={context}>
      <App />
    </StaticRouter>
  );
  const root = ReactDOMServer.renderToString(jsx); // 렌더링을 하고
  res.send(root); // 클라이언트에게 결과물을 제공한다.
};

// 서버 사이드 렌더링 핸들러
app.use(serverRender);

// 5000 포트로 서버를 가동한다.
app.listen(5000, () => {
  console.log("Running on http://localhost:5000");
});

* 이 과정에서 리액트 라우터 안에 들어 있는 StaticRouter라는 컴포넌트가 사용되었다. 이 라우터 컴포넌트는 주로 서버 사이드 렌더링 용도로 사용되는 라우터이다. props로 넣어 주는 location 값에 따라 라우팅해 준다. 지금은 req.url 이라는 값을 넣어 주었는데, 여기서 req 객체는 요청에 대한 정보를 지니고 있다.

* StaticRouter에 context라는 props 도 넣어 주었다. 이 값을 사용하여 나중에 렌더링한 컴포넌트에 따라 HTTP 상태 코드를 설정해 줄 수 있다.

* 지금 당장 JS 파일과 CSS 파일을 웹 페이지에 불러오는 것은 생략하고, 리액트 서버 사이드 렌더링을 통해 만들어진 결과만 보여주도록 처리했다. 서버를 다시 빌드하고 실행해 보자.

$ yarn build:server
$ yarn start:server

* 이제 브라우저로 하단의 경로에 들어가서 다음과 같은 화면이 보이는지 확인하자.

- http://localhost:5000/

서버 사이드 렌더링 첫 번째 결과

* /red 페이지와 /blue 페이지에 들어갔을 때도 알맞은 컴포넌트가 잘 보인다.

* 지금은 CSS를 불러오지 않기 때문에 스타일이 적용되어 있지 않아도 괜찮다. 브라우저에서 자바스크립트도 실행되지 않기 때문에, 현재 브라우저에서 나타난 정보는 모두 서버 사이드에서 렌더링된 것으로 간주할 수 있다.

* 만약 자바스크립트를 로딩하면 현재 브라우저에 보이는 데이터가 서버에서 렌더링된 것인지, 클라이언트에서 렌더링된 것인지 분간하기 어려울 것이다. 서버 사이드 렌더링이 정말 제대로 이루어졌는지 확인하기 위해 개발자 도구의 Network 탭을 열고 새로고침 해 보자.

Network 탭에서 Response 보기

* 새로 고침을 하고 나서 맨 위에 있는 항목(위 그림에서는 Red)을 누른 후 우측의 Response를 눌러 보자. 컴포넌트 렌더링 결과가 문자열로 잘 전달되고 있는가?

* 하단의 { } 버튼을 누르면, 코드가 자동으로 들여쓰기가 되어 더욱 읽기 쉬워진다.


(5) 정적 파일 제공하기

* 이번에는 Express에 내장되어 있는 static 미들웨어를 사용하여 서버를 통해 build에 있는 JS, CSS 정적 파일들에 접근할 수 있도록 해 주자.

[index.server.js]

import React from "react";
import ReactDOMServer from "react-dom/server";
import express from "express";
import { StaticRouter } from "react-router-dom";
import App from "./App";
import path from "path";

const app = express();

// 서버 사이드 렌더링을 처리할 핸들러 함수이다.
const serverRender = (req, res, next) => {
  // 이 함수는 404가 떠야 하는 상황에서 404를 띄우지 않고 서버 사이드 렌더링을 해 준다.

  const context = {};
  const jsx = (
    <StaticRouter location={req.url} context={context}>
      <App />
    </StaticRouter>
  );
  const root = ReactDOMServer.renderToString(jsx); // 렌더링을 하고
  res.send(root); // 클라이언트에게 결과물을 제공한다.
};

const serve = express.static(path.resolve("./build"), {
  index: false, // "/" 경로에서 index.html을 보여 주지 않도록 설정
});

app.use(serve); // 실행 순서가 중요하다. serverRender 전에 위치해야 한다.
// 서버 사이드 렌더링 핸들러
app.use(serverRender);

// 5000 포트로 서버를 가동한다.
app.listen(5000, () => {
  console.log("Running on http://localhost:5000");
});

* 그 다음에는 JS와 CSS 파일을 불러오도록 html에 코드를 삽입해 주어야 한다. 불러와야 하는 파일 이름은 매번 빌드할 때마다 바뀌기 때문에 빌드하고 나서 만들어지는 asset-manifest.json 파일을 참고하여 불러오도록 작성한다.

* 한번 yarn build 명령어를 실행한 다음, build 디렉터리의 asset-manifest.json 을 열어 보자.

* 위 코드에서 파란색 원으로 표시된 파일을 html 내부에 삽입해 주어야 한다.

* 서버 코드를 다음과 같이 수정해 보자.

[index.server.js]

import React from "react";
import ReactDOMServer from "react-dom/server";
import express from "express";
import { StaticRouter } from "react-router-dom";
import App from "./App";
import path from "path";
import fs from "fs";

// asset-manifest.json에서 파일 경로들을 조회한다.
const manifest = JSON.parse(
  fs.readFileSync(path.resolve("./build/asset-manifest.json"), "utf8")
);

const chunks = Object.keys(manifest.files)
  .filter((key) => /chunk\.js$/.exec(key)) // chunk.js로 끝나는 키를 찾아서
  .map((key) => `<script src="${manifest.files[key]}"></script>`) // 스크립트 태그로 변환하고
  .join(""); // 합침

function createPage(root) {
  return `
    <!DOCTYPE html>
    <html lang="ko">
    <head>
      <meta charset="utf-8" />
      <link rel="shortcut icon" href="/favicon.ico" />
      <meta
        name="viewport"
        content="width=device-width, initial-scale=1, shrink-to-fit=no"
      />
      <meta name="theme-color" context="#000" />
      <title>React App</title>
      <link href="${manifest.files["main.css"]}" />
    </head>
    <body>
      <noscript>You need to enable Javascript to run this app.</noscript>
      <div id="root">
        ${root}
      </div>
      <script src="${manifest.files["runtime~main.js"]}"></script>
      ${chunks}
      <script src="${manifest.files["main.js"]}"></script>
    </body>
    </html>
  `;
}
const app = express();

// 서버 사이드 렌더링을 처리할 핸들러 함수이다.
const serverRender = (req, res, next) => {
  // 이 함수는 404가 떠야 하는 상황에서 404를 띄우지 않고 서버 사이드 렌더링을 해 준다.

  const context = {};
  const jsx = (
    <StaticRouter location={req.url} context={context}>
      <App />
    </StaticRouter>
  );
  const root = ReactDOMServer.renderToString(jsx); // 렌더링을 하고
  res.send(createPage(root)); // 결과물 응답
};

const serve = express.static(path.resolve("./build"), {
  index: false, // "/" 경로에서 index.html을 보여 주지 않도록 설정
});

app.use(serve); // 실행 순서가 중요하다. serverRender 전에 위치해야 한다.
// 서버 사이드 렌더링 핸들러
app.use(serverRender);

// 5000 포트로 서버를 가동한다.
app.listen(5000, () => {
  console.log("Running on http://localhost:5000");
});

* 이제 서버를 빌드하고 다시 시작해 보자.

$ yarn build:server
$ yarn start:server

- http://localhost:5000/red 페이지에 들어가 CSS도 함께 적용되는지 확인하고, 개발자 도구의 Network 탭에서 서버 사이드 렌더링이 되었는지 검증해 보자. 여기서 링크를 눌러 이동할 때는 클라이언트 렌더링이 되어야 한다. 즉, 다른 링크를 클릭하여 다른 페이지로 이동할 때 네트워크 요청이 추가로 발생하지 않아야 한다.

* 서버 사이드 렌더링을 구현하면 이렇게 첫 번째 렌더링은 서버를 통해 하지만, 그 이후에는 브라우저에서 처리한다.

기본적인 서버 사이드 렌더링 완성

* 결과물이 잘 나타난다. 기본적인 서버 사이드 렌더링은 모두 완성이다!