서버사이드 렌더링(3)

리린·2021년 8월 2일
0

Loadable Components

  • 서버사이드 렌더링 + 코드 스플리팅
  • 기본 제공: 서버 유틸 함수, 웹팩 플러그인, babel 플러그인
  • 설치:
    (콘솔) yarn add @loadable/component @loadable/server @loadable/webpack-plugin @loadable/babel-plugin

babel 플러그인 적용

  • 목적: 깜빡임 현상 해결
  1. package.json 에서 다음과 같이 babel 설정 코드 추가
(..)
"babel": {
    "presets": [
      "react-app"
    ],
    "plugins":[
      "@loadable/babel-plugin"
    ]
  }
}
  1. webpack.congif.js (프런트엔드 웹팩설정파일) 열어서 LoadablePlugin을 불러오고 적용하기
const LoadablePlugin=require('@loadable/webpack-plugin');
(...)
plugins: [
	new LoadablePlugin(),
	new HTMLWebpackPlugin(
    (...)
 ].filter(Boolean),
 (...)
 
 
  1. 저장 후 yarn build 명령어 실행하기
  • loadable-stats.json 파일 생성된다.
    각 컴포넌트의 코드가 어떤 청크 파일에 들어가 있는지에 대한 정보를 가지고 있다.
    이를 통해 ssr을 할 때 어떤 컴포넌트가 렌더링되었는지에 따라 어떤 파일들을 사전에 불러와야 할지 설정 가능하다.
    즉 asset-manifest.json을 통해 확인할 필요 없다. (너무좋아 )
  1. ChunkExtractor, ChunkExtractorManager 사용하기 + index.server.js 파일에서, asset-manifest.json과 연관된 부분 삭제하기
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";
import {
  ChunckExtractor,
  ChunkExtractor,
  ChunkExtractorManager,
} from "@loadable/server";

const statsFile = path.resolve("./build/loadable-stats.json");

/*

삭제 


// 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, tags) {
  return `<!DOCTYPE html>
    <html lang="en">
    <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" content="#000000" />
      <title>React App</title>
      ${tags.styles}
      ${tags.links}
    </head>
    <body>
      <noscript>You need to enable JavaScript to run this app.</noscript>
      <div id="root">
        ${root}
      </div>
      ${tags.scripts}
    </body>
    </html>
      `;
}

const app = express();

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

  const context = {};
  const store = createStore(rootReducer, applyMiddleware(thunk));
  const preloadContext = {
    done: false,
    promises: [],
  };

  const extractor = new ChunkExtractor({ statsFile });

  const jsx = (
    <ChunkExtractorManager extractor={extractor}>
      <PreloadContext.Provider value={preloadContext}>
        <Provider store={store}>
          <StaticRouter location={req.url} context={context}>
            <App />
          </StaticRouter>
        </Provider>
      </PreloadContext.Provider>
    </ChunkExtractorManager>
  );

  ReactDOMServer.renderToStaticMarkup(jsx); // renderToStaticMarkup으로 한번 렌더링합니다.

  try {
    await Promise.all(preloadContext.promises); // 모든 프로미스를 기다립니다.
  } catch (e) {
    return res.staus(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>`; // 리덕스 초기 상태를 스크립트로 주입합니다.

  const tags = {
    scripts: stateScript + extractor.getScriptTags(),
    links: extractor.getLinkTags(),
    styles: extractor.getStyleTags(),
  };
  res.send(createPage(root, tags)); // 클라이언트에게 결과물을 응답합니다.
};

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");
});
  1. 비동기 작업(스크립트 로딩 뒤 렌더링 ) : loadableReady 함수
  • index.js
import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
import App from "./App";
import { BrowserRouter } from "react-router-dom";
import { createStore, applyMiddleware } from "redux";
import { Provider } from "react-redux";
import thunk from "redux-thunk";
import rootReducer 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);
}
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
profile
개발자지망생

0개의 댓글