관리 메뉴

거니의 velog

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

React_백엔드 프로그래밍

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

Unlimited00 2023. 12. 16. 15:31

4. 데이터 로딩

* 데이터 로딩은 서버 사이드 렌더링을 구현할 때 해결하기가 매우 까다로운 문제 중 하나이다. 데이터 로딩을 한다는 것은 API 요청을 의미한다. 예를 들어 페이지에서 필요로 하는 데이터가 있다면 API를 요청해서 응답을 받아 와야 한다. 일반적인 브라우저 환경에서는 API를 요청하고 응답을 받아 와서 리액트 state 혹은 리덕스 스토어에 넣으면 자동으로 리렌더링하니까 큰 걱정은 없다. 하지만 서버의 경우 문자열 형태로 렌더링하는 것이므로 state나 리덕스 스토어의 상태가 바뀐다고 해서 자동으로 리렌더링되지 않는다. 그 대신 우리가 renderToString 함수를 한 번 더 호출해 주어야 한다. 게다가 서버에서는 componentDidMount 같은 라이프사이클 API도 사용할 수 없다.

* 서버 사이드 렌더링 시 데이터 로딩을 해결하는 방법 또한 다양하다. 그 중에 정말 깔끔하고 편한 방법을 살펴보자. 서버 사이드 렌더링을 할 때 데이터 로딩의 원리를 공부하고, 여기서 제시한 코드를 원하는 대로 변형하여 사용해도 좋다.

* 우리는 redux-thunk 혹은 redux-saga 미들웨어를 사용하여 API를 호출하는 환경에서 서버 사이드 렌더링을 하는 방법을 알아보겠다.


(1) redux-thunk 코드 준비하기

* 우선 redux-thunk 를 사용하여 API 호출 후 데이터를 가져오는 코드를 작성해 보겠다. 다음 명령어를 입력하여 필요한 라이브러리를 설치해 주자.

$ yarn add redux react-redux redux-thunk axios

* 액션 타입, 액션 생성 함수, 리듀서 코드를 한 파일에 넣어서 관리하는 Ducks 패턴을 사용하여 리덕스 모듈을 작성하겠다. 

* src 디렉터리에 modules 디렉터리를 만들고, 다음 파일을 작성하자.

import axios from 'axios';

const GET_USERS_PENDING = 'users/GET_USERS_PENDING';
const GET_USERS_SUCCESS = 'users/GET_USERS_SUCCESS';
const GET_USERS_FAILURE = 'users/GET_USERS_FAILURE';

const getUsersPending = () => ({ type: GET_USERS_PENDING });
const getUsersSuccess = payload => ({ type: GET_USERS_SUCCESS, payload });
const getUsersFailure = payload => ({
  type: GET_USERS_FAILURE,
  error: true,
  payload
});

export const getUsers = () => async dispatch => {
  try {
    dispatch(getUsersPending());
    const response = await axios.get(
      'https://jsonplaceholder.typicode.com/users'
    );
    dispatch(getUsersSuccess(response));
  } catch (e) {
    dispatch(getUsersFailure(e));
    throw e;
  }
};

const initialState = {
  users: null,
  loading: {
    users: false,
    user: false
  },
  error: {
    users: null,
    user: null
  }
};

function users(state = initialState, action) {
  switch (action.type) {
    case GET_USERS_PENDING:
      return {
        ...state,
        loading: { ...state.loading, users: true },
        error: { ...state.error, users: null }
      };
    case GET_USERS_SUCCESS:
      return {
        ...state,
        loading: { ...state.loading, users: false },
        users: action.payload.data
      };
    case GET_USERS_FAILURE:
      return {
        ...state,
        loading: { ...state.loading, users: false },
        error: { ...state.error, users: action.payload }
      };
    default:
      return state;
  }
}

export default users;

* 이 모듈에서는 JSONPlaceholder에서 제공하는 다음 API를 호출하여 테스트용 데이터를 조회한다.

jsonplaceholder.typicode.com/users

* 이 API는 사용자들에 대한 정보를 응답한다.

[
    {
        "id": 1,
        "name": "Leanne Graham",
        "username": "Bret",
        "email": "Sincere@april.biz",
        "address": {
            "street": "Kulas Light",
            "suite": "Apt. 556",
            "city": "Gwenborough",
            "zipcode": "92998-3874",
            "geo": {
                "lat": "-37.3159",
                "lng": "81.1496"
            }
        },
        "phone": "1-770-736-8031 x56442",
        "website": "hildegard.org",
        "company": {
            "name": "Romaguera-Crona",
            "catchPhrase": "Multi-layered client-server neural-net",
            "bs": "harness real-time e-markets"
        }
    },
    (...)
]

* 현재 작성한 모듈은 getUsers라는 thunk 함수를 만들고, 이와 관련된 액션 GET_USERS_PENDING, GET_USERS_SUCCESS, GET_USERS_FAILURE를 사용하여 상태 관리를 해 주고 있다.

* 모듈의 상태에는 loading과 error 라는 객체가 들어 있다. 로딩 상태와 에러 상태를 이렇게 객체로 만든 이유는 추후 redux-saga를 사용한 서버 사이드 렌더링 방법을 연습할 때 단 하나의 사용자 정보를 가져오는 다른 API를 호출할 것이기 때문이다.

* 즉, 이 모듈에서 관리하는 API는 한 개 이상이므로 loadingUsers, loadingUser와 같이 각 값에 하나하나 이름을 지어 주는 대신에 loadinf이라는 객체에 넣어 준 것이다.

* 모듈을 다 작성한 뒤에는 루트 리듀서를 만들고, Provider 컴포넌트를 사용하여 프로젝트에 리덕스를 적용하자.

[modules/index.js]

import { combineReducers } from 'redux';
import users from './users';

const rootReducer = combineReducers({ users });
export default rootReducer;

[src/index.js]

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
import { BrowserRouter } from 'react-router-dom';
import { createStore, applyMiddleware } from 'redux';
import { Provider } from 'react-redux';
import thunk from 'redux-thunk';
import rootReducer, { rootSaga } from './modules';
import { loadableReady } from '@loadable/component';

const store = createStore(rootReducer, applyMiddleware(thunk));

// 같은 내용을 쉽게 재사용 할 수 있도록 렌더링 할 내용을 하나의 컴포넌트로 묶음
const Root = () => {
  return (
    <Provider store={store}>
      <BrowserRouter>
        <App />
      </BrowserRouter>
    </Provider>
  );
};

const root = document.getElementById('root');

// 프로덕션 환경 에서는 loadableReady 와 hydrate 를 사용하고
// 개발 환경에서는 기존 하던 방식으로 처리
if (process.env.NODE_ENV === 'production') {
  loadableReady(() => {
    ReactDOM.hydrate(<Root />, root);
  });
} else {
  ReactDOM.render(<Root />, root);
}

serviceWorker.unregister();

(2) Users, UsersContainer 컴포넌트 준비하기

* 이제 사용자에 대한 정보를 보여 줄 컴포넌트를 준비하겠다.

* components 디렉터리에 Users 컴포넌트를 다음과 같이 만들어 보자.

import React from "react";
import { Link } from "react-router-dom";

const Users = ({ users }) => {
  if (!users) return null; // users가 유효하지 않다면 아무것도 보여주지 않음
  return (
    <div>
      <ul>
        {users.map((user) => (
          <li key={user.id}>
            <Link to={`/users/${user.id}`}>{user.username}</Link>
          </li>
        ))}
      </ul>
    </div>
  );
};

export default Users;

* 다음으로  src 디렉터리에 containers 디렉터리를 만들고, 그 안에 UsersContainer 컴포넌트를 작성하자.

import React from "react";
import Users from "../components/Users";
import { connect } from "react-redux";
import { getUsers } from "../modules/users";
import { Preloader } from "../lib/PreloaderContext";

const { useEffect } = React;

const UsersContainer = ({ users, getUsers }) => {
  // 컴포넌트 마운트될 때 호출
  useEffect(() => {
    if (users) return; // users가 이미 유효하다면 요청하지 않음
    getUsers();
  }, [getUsers, users]);
  return (
    <>
      <Users users={users} />
      <Preloader resolve={getUsers} />
    </>
  );
};

export default connect(
  (state) => ({
    users: state.users.users,
  }),
  {
    getUsers,
  }
)(UsersContainer);

* 서버 사이드 렌더링을 할 때는 이미 있는 정보를 재요청하지 않게 처리하는 작업이 중요하다. 이 작업을 하지 않으면 서버 사이드 렌더링 후 브라우저에서 페이지를 확인할 때 이미 데이터를 가지고 있음에도 불구하고 불필요한 API를 호출하게 된다. 그러면 트래픽도 낭비되고 사용자 경험도 저하될 것이다.

* 컨테이터 컴포넌트를 모두 작성했으면 이 컴포넌트를 보여 줄 페이지 컴포넌트를 만들고, 라우트 설정을 해 주자.

import React from 'react';
import UsersContainer from '../containers/UsersContainer';

const UsersPage = () => {
  return (
    <>
      <UsersContainer />
    </>
  );
};

export default UsersPage;

[App.js]

import React from "react";
import { Route } from "react-router-dom";
import Menu from "./components/Menu";
import loadable from "@loadable/component";
const RedPage = loadable(() => import("./pages/RedPage"));
const BluePage = loadable(() => import("./pages/BluePage"));
const UsersPage = loadable(() => import("./pages/UsersPage"));

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

export default App;

* 브라우저에서 더욱 쉽게 /users 경로로 이동할 수 있도록 Menu 컴포넌트도 수정하겠다.

[components/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>
      <li>
        <Link to="/users">Users</Link>
      </li>
    </ul>
  );
};

export default Menu;

* 아직 데이터 로딩에 대한 서버 사이드 렌더링 구현이 끝나지 않았지만, 리액트 개발 서버에서 방금 구현한 데이터 로딩 기능이 잘 작동하는지 확인해 보자.

* yarn start 명령어를 사용하여 개발 서버를 구동하고, 브라우저에서 5000 포트가 아닌 3000 포트 주소로 접속하자. 그리고 Users 링크를 눌러 /user 경로로 들어가 보자. 다음 화면이 나타나면 된다.

Users 페이지


[3] PreloadContent 만들기

* 현재 getUsers 함수는 UsersContainer의 useEffect 부분에서 호출된다. 이를 클래스형으로 작성했더라면 componentDidMount에서 호출했을 것이다. 서버 사이드 렌더링을 할 때는 useEffect나 componentDidMount에서 설정한 작업이 호출되지 않는다. 렌더링하기 전에 API를 요청한 뒤 스토어에 데이터를 담아야 하는데, 서버 환겨엥서 이러한 작업을 하려면 클래스형 컴포넌트가 지니고 있는 constructor 메서드를 사용하거나 render 함수 자체에서 처리해야 한다. 그리고 요청이 끝날 때까지 대기했다가 다시 렌더링해 주어야 한다.

* 우리는 이 작업을 PreloadContext를 만들고, 이를 사용하는 Preloader 컴포넌트를 만들어 처리해 볼 것이다.

* src 디렉터리에 lib 디렉터리를 생성하고, 그 안에 PreloadContext.js 파일을 다음과 같이 작성하자.

import { createContext, useContext } from 'react';

// 클라이언트 환경: null
// 서버 환경:{ done: false, promises: [] }
const PreloadContext = createContext(null);
export default PreloadContext;

// resolve는 함수 타입입니다.
export const Preloader = ({ resolve }) => {
  const preloadContext = useContext(PreloadContext);
  if (!preloadContext) return null; // context 값이 유효하지 않다면 아무것도 하지 않음
  if (preloadContext.done) return null; // 이미 작업이 끝났다면 아무것도 하지 않음

  // promises 배열에 프로미스 등록
  // 설령 resolve 함수가 프로미스를 반환하지 않더라도, 프로미스 취급을 하기 위하여
  // Promise.resolve 함수 사용
  preloadContext.promises.push(Promise.resolve(resolve()));
  return null;
};

* PreloadContext는 서버 사이드 렌더링을 하는 과정에서 처리해야 할 작업들을 실행하고, 만약 기다려야 하는 프로미스(Promise)가 있다면 프로미스를 수집한다. 모든 프로미스를 수집한 뒤, 수집된 프로미스들이 끝날 때까지 기다렸다가 그다음에 다시 렌더링하면 데이터가 채워진 상태로 컴포넌트들이 나타나게 된다.

* PreLoader 컴포넌트는 resolve라는 함수를 props로 받아 오며, 컴포넌트가 렌더링 될 때 서버 환경에서만 resolve 함수를 호출해 준다.

* 이를 UsersContainer에서 한번 사용해 보자.

[containers/UsersContainer.js]

import React from "react";
import Users from "../components/Users";
import { connect } from "react-redux";
import { getUsers } from "../modules/users";
import { Preloader } from "../lib/PreloaderContext";

const { useEffect } = React;

const UsersContainer = ({ users, getUsers }) => {
  // 컴포넌트 마운트될 때 호출
  useEffect(() => {
    if (users) return; // users가 이미 유효하다면 요청하지 않음
    getUsers();
  }, [getUsers, users]);
  return (
    <>
      <Users users={users} />
      <Preloader resolve={getUsers} />
    </>
  );
};

export default connect(
  (state) => ({
    users: state.users.users,
  }),
  {
    getUsers,
  }
)(UsersContainer);

* 이렇게 코드 두 줄이면 끝난다. 참 간단하다.


[4] 서버에서 리덕스 설정 및 PreloadContent 사용하기

* 이제 서버에서 리덕스를 설정해 주자. 서버에서 리덕스를 설정하는 것은 브라우저에서 할 때와 비교하여 큰 차이가 없다.

[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";
import { createStore, applyMiddleware } from 'redux';
import { Provider } from 'react-redux';
import thunk from 'redux-thunk';
import rootReducer from './modules';

// 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 store = createStore(rootReducer, applyMiddleware(thunk));
  const jsx = (
    <Provider store={store}>
      <StaticRouter location={req.url} context={context}>
        <App />
      </StaticRouter>
    </Provider>
  );
  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");
});

* 브라우저에서 할 때와 거의 똑같다. 여기서 주의할 점은 서버가 실행될 때 스토어를 한 번만 만드는 것이 아니라, 요청이 들어올 때마다 새로운 스토어를 만든다는 것이다.

* 이제 PreloadContext를 사용하여 프로미스들을 수집하고 기다렸다가 다시 렌더링하는 작업을 수행해 보자.

[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";
import { createStore, applyMiddleware } from 'redux';
import { Provider } from 'react-redux';
import thunk from 'redux-thunk';
import rootReducer from './modules';
import PreloadContext from './lib/PreloadContext';

// 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 store = createStore(rootReducer, applyMiddleware(thunk));
  
  const preloadContext = {
    done: false,
    promises: []
  };
  
  const jsx = (
    <PreloadContext.Provider value={preloadContext}>
      <Provider store={store}>
        <StaticRouter location={req.url} context={context}>
          <App />
        </StaticRouter>
      </Provider>
    </PreloadContext.Provider>
  );
  
  ReactDOMServer.renderToStaticMarkup(jsx); // renderToStaticMarkup으로 한번 렌더링한다.
  try {
      await Promise.all(preloadContext.promises); // 모든 프로미스들을 기다린다.
  }catch(e) {
      return res.status(500);
  }
  preloadContext.done = true;
  
  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");
});

* 첫 번째 렌더링을 할 때는 renderToString 대신 renderToStaticMarkup이라는 함수를 사용했다. renderToStaticMarkup은 리액트를 사용하여 정적인 페이지를 만들 때 사용한다. 이 함수로 만든 리액트 렌더링 결과물은 클라이언트 쪽에서 HTML DOM 인터랙션을 지원하기 힘들다.

* 지금 단계에서 renderToString 대신 renderToStaticMarkup 함수를 사용한 이유는 그저 Preloader로 넣어 주었던 함수를 호출하기 위해서이다. 또 이 함수의 처리 속도가 renderToString 보다 좀 더 빠르기 때문이다.


(5) 스크립트로 스토어 초기 상태 주입하기

* 지금까지 작성한 코드는 API를 통해 받아 온 데이터를 렌더링하지만, 렌더링하는 과정에서 만들어진 스토어의 상태를 브라우저에서 재사용하지 못하는 상황이다. 서버에서 만들어준 상태를 브라우저에서 재사용하려면, 현재 스토어 상태를 문자열로 변환한 뒤 스크립트로 주입해 주어야 한다.

[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";
import { createStore, applyMiddleware } from 'redux';
import { Provider } from 'react-redux';
import thunk from 'redux-thunk';
import rootReducer from './modules';
import PreloadContext from './lib/PreloadContext';

// 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, stateScript) {
  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>
      ${stateScript}
      <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 store = createStore(rootReducer, applyMiddleware(thunk));
  
  const preloadContext = {
    done: false,
    promises: []
  };
  
  const jsx = (
    <PreloadContext.Provider value={preloadContext}>
      <Provider store={store}>
        <StaticRouter location={req.url} context={context}>
          <App />
        </StaticRouter>
      </Provider>
    </PreloadContext.Provider>
  );
  
  ReactDOMServer.renderToStaticMarkup(jsx); // renderToStaticMarkup으로 한번 렌더링한다.
  try {
      await Promise.all(preloadContext.promises); // 모든 프로미스들을 기다린다.
  }catch(e) {
      return res.status(500);
  }
  preloadContext.done = true;
  
  const root = ReactDOMServer.renderToString(jsx); // 렌더링을 한다.
  // JSON 을 문자열로 변환하고 악성스크립트가 실행되는것을 방지하기 위해서 < 를 치환처리
  // https://redux.js.org/recipes/server-rendering#security-considerations
  const stateString = JSON.stringify(store.getState()).replace(/</g, '\\u003c');
  const stateScript = `<script>__PRELOADED_STATE__ = ${stateString}</script>`; // 리덕스 초기 상태를 스크립트로 주입합니다.
  res.send(createPage(root, stateScript)); // 결과물 응답
};

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");
});

* 브라우저에서 상태를 재사용할 때는 다음과 같이 스토어 생성 과정에서 window.__PRELOADED_STATE__를 초깃값으로 사용하면 된다.

[index.js]

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
import { BrowserRouter } from 'react-router-dom';
import { createStore, applyMiddleware } from 'redux';
import { Provider } from 'react-redux';
import thunk from 'redux-thunk';
import rootReducer, { rootSaga } from './modules';
import { loadableReady } from '@loadable/component';

const store = createStore(
  rootReducer,
  window.__PRELOADED_STATE__, // 이 값을 초기상태로 사용함
  applyMiddleware(thunk)
);

// 같은 내용을 쉽게 재사용 할 수 있도록 렌더링 할 내용을 하나의 컴포넌트로 묶음
const Root = () => {
  return (
    <Provider store={store}>
      <BrowserRouter>
        <App />
      </BrowserRouter>
    </Provider>
  );
};

const root = document.getElementById('root');

// 프로덕션 환경 에서는 loadableReady 와 hydrate 를 사용하고
// 개발 환경에서는 기존 하던 방식으로 처리
if (process.env.NODE_ENV === 'production') {
  loadableReady(() => {
    ReactDOM.hydrate(<Root />, root);
  });
} else {
  ReactDOM.render(<Root />, root);
}

serviceWorker.unregister();

* 여기까지 코드를 작성했다면 빌드 후 서버를 실행하여 아래 경로로 들어가 보자.

$ yarn build
$ yarn build:server
$ yarn start:server

- http://localhost:5000/users

API 연동 후 서버 사이드 렌더링

* 결과물이 잘 나타났는가? 개발자 도구를 열고 Network 탭의 Response 부분에서도 잘 나타났는지 확인해 보자.