관리 메뉴

거니의 velog

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

React_백엔드 프로그래밍

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

Unlimited00 2023. 12. 14. 18:29

1. 서버 사이드 렌더링의 이해

* 서버 사이드 렌더링은 UI를 서버에서 렌더링하는 것을 의미한다. 앞에서 만든 리액트 프로젝트는 기본적으로 클라이언트 사이드 렌더링을 하고 있다. 클라이언트 사이드 렌더링은 UI 렌더링을 브라우저에서 모두 처리하는 것이다. 즉, 자바스크립트를 실행해야 우리가 만든 화면이 사용자에게 보인다.

* 한번 CRA로 프로젝트를 생성하고 개발 서버를 실행해 보자. 그리고 크롬 개발자 도구의 Network 탭을 열고 새로고침을 해 보자.

$ yarn create react-app ssr-recipe
$ cd ssr-recipe
$ yarn start

비어 있는 root 엘리먼트

* package.json을 다음의 코드를 참조하여  다시 빌드하자.

{
  "name": "server-side-rendering",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "@babel/core": "7.4.3",
    "@loadable/babel-plugin": "^5.10.0",
    "@loadable/component": "^5.10.1",
    "@loadable/server": "^5.9.0",
    "@loadable/webpack-plugin": "^5.7.1",
    "@svgr/webpack": "4.1.0",
    "@typescript-eslint/eslint-plugin": "1.6.0",
    "@typescript-eslint/parser": "1.6.0",
    "axios": "^0.19.0",
    "babel-eslint": "10.0.1",
    "babel-jest": "^24.8.0",
    "babel-loader": "8.0.5",
    "babel-plugin-named-asset-import": "^0.3.2",
    "babel-preset-react-app": "^9.0.0",
    "camelcase": "^5.2.0",
    "case-sensitive-paths-webpack-plugin": "2.2.0",
    "css-loader": "2.1.1",
    "dotenv": "6.2.0",
    "dotenv-expand": "4.2.0",
    "eslint": "^5.16.0",
    "eslint-config-react-app": "^4.0.1",
    "eslint-loader": "2.1.2",
    "eslint-plugin-flowtype": "2.50.1",
    "eslint-plugin-import": "2.16.0",
    "eslint-plugin-jsx-a11y": "6.2.1",
    "eslint-plugin-react": "7.12.4",
    "eslint-plugin-react-hooks": "^1.5.0",
    "file-loader": "3.0.1",
    "fs-extra": "7.0.1",
    "html-webpack-plugin": "4.0.0-beta.5",
    "identity-obj-proxy": "3.0.0",
    "is-wsl": "^1.1.0",
    "jest": "24.7.1",
    "jest-environment-jsdom-fourteen": "0.1.0",
    "jest-resolve": "24.7.1",
    "jest-watch-typeahead": "0.3.0",
    "mini-css-extract-plugin": "0.5.0",
    "optimize-css-assets-webpack-plugin": "5.0.1",
    "pnp-webpack-plugin": "1.2.1",
    "postcss-flexbugs-fixes": "4.1.0",
    "postcss-loader": "3.0.0",
    "postcss-normalize": "7.0.1",
    "postcss-preset-env": "6.6.0",
    "postcss-safe-parser": "4.0.1",
    "react": "^16.8.6",
    "react-app-polyfill": "^1.0.1",
    "react-dev-utils": "^9.0.1",
    "react-dom": "^16.8.6",
    "react-redux": "^7.1.0",
    "react-router-dom": "^5.0.1",
    "redux": "^4.0.1",
    "redux-saga": "^1.0.3",
    "redux-thunk": "^2.3.0",
    "resolve": "1.10.0",
    "sass-loader": "7.1.0",
    "semver": "6.0.0",
    "style-loader": "0.23.1",
    "terser-webpack-plugin": "1.2.3",
    "ts-pnp": "1.1.2",
    "url-loader": "1.1.2",
    "webpack": "4.29.6",
    "webpack-dev-server": "3.2.1",
    "webpack-manifest-plugin": "2.0.4",
    "webpack-node-externals": "^1.7.2",
    "workbox-webpack-plugin": "4.2.0"
  },
  "scripts": {
    "start": "node scripts/start.js",
    "build": "node scripts/build.js",
    "test": "node scripts/test.js",
    "start:server": "node dist/server.js",
    "build:server": "node scripts/build.server.js"
  },
  "eslintConfig": {
    "extends": "react-app"
  },
  "browserslist": {
    "production": [
      ">0.2%",
      "not dead",
      "not op_mini all"
    ],
    "development": [
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version"
    ]
  },
  "jest": {
    "collectCoverageFrom": [
      "src/**/*.{js,jsx,ts,tsx}",
      "!src/**/*.d.ts"
    ],
    "setupFiles": [
      "react-app-polyfill/jsdom"
    ],
    "setupFilesAfterEnv": [],
    "testMatch": [
      "<rootDir>/src/**/__tests__/**/*.{js,jsx,ts,tsx}",
      "<rootDir>/src/**/*.{spec,test}.{js,jsx,ts,tsx}"
    ],
    "testEnvironment": "jest-environment-jsdom-fourteen",
    "transform": {
      "^.+\\.(js|jsx|ts|tsx)$": "<rootDir>/node_modules/babel-jest",
      "^.+\\.css$": "<rootDir>/config/jest/cssTransform.js",
      "^(?!.*\\.(js|jsx|ts|tsx|css|json)$)": "<rootDir>/config/jest/fileTransform.js"
    },
    "transformIgnorePatterns": [
      "[/\\\\]node_modules[/\\\\].+\\.(js|jsx|ts|tsx)$",
      "^.+\\.module\\.(css|sass|scss)$"
    ],
    "modulePaths": [],
    "moduleNameMapper": {
      "^react-native$": "react-native-web",
      "^.+\\.module\\.(css|sass|scss)$": "identity-obj-proxy"
    },
    "moduleFileExtensions": [
      "web.js",
      "js",
      "web.ts",
      "ts",
      "web.tsx",
      "tsx",
      "json",
      "web.jsx",
      "jsx",
      "node"
    ],
    "watchPlugins": [
      "jest-watch-typeahead/filename",
      "jest-watch-typeahead/testname"
    ]
  },
  "babel": {
    "presets": [
      "react-app"
    ],
    "plugins": [
      "@loadable/babel-plugin"
    ]
  }
}
* 기존 코드 삭제후 붙여 넣기하고, node_modules 폴더를 삭제후, 
  해당 디렉토리 루트에서 다음의 명령어를 입력한다.

$ yarn

* 맨 위에 있는 localhost를 선택하고 Response를 보면 root 엘리먼트가 비어 있는 것을 확인할 수 있다. 즉, 이 페이지는 처음에 빈 페이지라는 뜻이다. 그 이후에 자바스크립트가 실행되고 리액트 컴포넌트가 렌더링되면서 우리에게 보이는 것이다.

* 서버 사이드 렌더링을 구현하면 사용자가 웹 서비스에 방문했을 때 서버 쪽에서 초기 렌더링을 대신해 준다. 그리고 사용자가 html을 전달받았을 때 그 내부에서 렌더링된 결과물이 보인다.


(1) 서버 사이드 렌더링의 장점

* 서버 사이드 렌더링에는 어떤 장점이 있을까? 일단 구글, 네이버, 다음 등의 검색 엔진이 우리가 만든 웹 애플리케이션의 페이지를 원활하게 수집할 수 있다. 리액트로 만든 SPA는 검색엔진 크롤러 봇처럼 자바스크립트가 실행되지 않는 환경에서는 페이지가 제대로 나타나지 않는다. 따라서 서버에서 클라이언트 대신 렌더링을 해 주면 검색 엔진이 페이지의 내용을 제대로 수집해 갈 수 있다. 구글 검색 엔진은 다른 검색 엔진과 달리 검색 엔진에서 자바스크립트를 실행하는 기능이 답재되어 있으므로 제대로 크롤링해 갈 때도 있지만, 모든 페이지에 대해 자바스크립트를 실행해 주지는 않는다. 따라서 웹 서비스의 검색 엔진 최적화(SEO(검색 엔진 최적화) : 웹사이트가 검색 결과에 더 잘 보이도록 최적화하는 과정)를 위해서라면 서버 사이드 렌더링을 구현해 주는 것이 좋다.

* 또한, 서버 사이드 렌더링을 통해 초기 렌더링 성능을 개선할 수 있다. 예를 들어 서버 사이드 렌더링이 구현되지 않은 웹 페이지에 사용자가 방문하면, 자바스크립트가 로딩되고 실행될 때까지 사용자는 비어 있는 페이지를 보며 대기해야 한다. 여기에 API까지 호출해야 한다면 사용자의 대기 시간이 더더욱 길어진다. 반면 서버 사이드 렌더링을 구현한 웹 페이지라면 자바스크립트 파일 다운로드가 완료되지 않은 시점에서도 html 상에 사용자가 볼 수 있는 콘텐츠가 있기 때문에 대기 시간이 최소화되고, 이로 인해 사용자 경험이 향상된다.


(2) 서버 사이드 렌더링의 단점

* 그럼 단점도 생각해 보자. 서버 사이드 렌더링은 결국 원래 브라우저가 해야 할 일을 서버가 대신 처리하는 것이므로 서버 리소스가 사용된다는 단점이 있다. 갑자기 수많은 사용자가 동시에 웹 페이지에 접속하면 서버에 과부하가 발생할 수 있다. 따라서 사용자가 많은 서비스라면 캐싱과 로드 밸런싱을 통해 성능을 최적화해 주어야 한다.

* 또한 서버 사이드 렌더링을 하면 프로젝트의 구조가 좀 더 복잡해질 수 있고, 데이터 미리 불러오기, 코드 스플리팅과의 호환 등 고려해야 할 사항이 더 많아져서 개발이 어려워질 수도 있다.


(3) 서버 사이드 렌더링과 코드 스플리팅 충돌

* 서버 사이드 렌더링과 코드 스플리팅을 함께 적용하면 작업이 꽤 까다롭다. 별도의 호환 작업 없이 두 기술을 함께 적용하면, 다음과 같은 흐름으로 작동하면서 페이지에 깜박임이 발생한다.

* 이러한 이슈를 해결하려면 라우트 경로마다 코드 스플리팅된 파일 중에서 필요한 모든 파일을 브라우저에서 렌더링하기 전에 미리 불러와야 한다.

* 우리는 이 문제점을 다음과 같은 방법으로 해결한다. Loadable Components 라이브러리에서 제공하는 기능을 써서 서버 사이드 렌더링 후 필요한 파일의 경로를 추출하여 렌더링 결과에 스크립트/스타일 태그를 삽입해 주는 방법이다.

* 이 장에서는 리액트 프로젝트에 서버 사이드 렌더링을 어떻게 구현하는지 알아보겠다. 실습은 다음 흐름으로 진행된다.


2. 프로젝트 준비하기

* 서버 사이드 렌더링을 진행하기 전에 리액트 라우터를 사용하여 라우팅하는 간단한 프로젝트를 만들어 보자. 조금 전에 만들었던 ssr-recipe 프로젝트 디렉터리에 react-router-dom을 설치하자.

$ yarn add react-router-dom

// v5 설치 용도
$ yarn add react-router-dom@5

(1) 컴포넌트 만들기

* 간단한 컴포넌트를 세 개 작성한다. components 디렉터리를 생성하여 그 안에 다음 파일들을 하나하나 순서대로 작성하면 된다.

[Red.js]

import React from 'react';
import './Red.css';

const Red = () => {
  return <div className="Red">Red</div>;
};

export default Red;

[Red.css]

.Red {
  background: red;
  font-size: 1.5rem;
  color: white;
  width: 128px;
  height: 128px;
  display: flex;
  align-items: center;
  justify-content: center;
}

[Blue.js]

import React from 'react';
import './Blue.css';

const Blue = () => {
  return <div className="Blue">Blue</div>;
};

export default Blue;

[Blue.css]

.Blue {
  background: blue;
  font-size: 1.5rem;
  color: white;
  width: 128px;
  height: 128px;
  display: flex;
  align-items: center;
  justify-content: center;
}

[Menu.js]

import React from 'react';
import { Link } from 'react-router-dom';
const Menu = () => {
  return (
    <ul>
      <li>
        <Link to="/red">Red</Link>
      </li>
      <li>
        <Link to="/blue">Blue</Link>
      </li>
    </ul>
  );
};

export default Menu;

* 단순하게 빨간색, 파란색 박스를 보여 주는 컴포넌트와 각 링크로 이동할 수 있게 해 주는 메뉴 컴포넌트를 만들었다. 이 컴포넌트들을 리액트 앱에서 사용해 보겠다.


(2) 페이지 컴포넌트 만들기

* 이번에는 각 라우트를 위한 페이지 컴포넌트들을 만들겠다. 이 컴포넌트들은 pages 디렉터리에 작성해 주자.

[RedPage.js]

import React from 'react';
import Red from '../components/Red';

const RedPage = () => {
  return <Red />;
};

export default RedPage;

[BluePage.js]

import React from 'react';
import Blue from '../components/Blue';

const BluePage = () => {
  return <Blue />;
};

export default BluePage;

* 페이지 컴포넌트도 다 만들었다. 이제 App 컴포넌트에서 라우트 설정을 해 보자.

[App.js]

import React from "react";
import { Route } from "react-router-dom/cjs/react-router-dom";
import Menu from "./components/Menu";
import RedPage from "./pages/RedPage";
import BluePage from "./pages/BluePage";

function App() {
  return (
    <div>
      <Menu />
      <hr />
      <Route path="/red" component={RedPage} />
      <Route path="/blue" component={BluePage} />
    </div>
  );
}

export default App;

* 다음으로 BrowserRouter를 사용하여 프로젝트에 리액트 라우터를 적용하자.

[index.js]

import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
import App from "./App";
import { BrowserRouter } from "react-router-dom/cjs/react-router-dom";

ReactDOM.render(
  <BrowserRouter>
    <App />
  </BrowserRouter>,
  document.getElementById("root")
);

* 브라우저로 페이지를 열어서 Menu 컴포넌트에 있는 링크를 눌러 보자. 빨간색, 파란색 컴포넌트가 잘 나타나는가?

페이지 컴포넌트 만들기

* 이제 서버 사이드 렌더링을 구현할 프로젝트가 준비되었다. 본격적으로 서버 사이드 렌더링을 구현해 보자.