Loadable Components
- 서버사이드 렌더링 + 코드 스플리팅
- 기본 제공: 서버 유틸 함수, 웹팩 플러그인, babel 플러그인
- 설치:
(콘솔) yarn add @loadable/component @loadable/server @loadable/webpack-plugin @loadable/babel-plugin
babel 플러그인 적용
- package.json 에서 다음과 같이 babel 설정 코드 추가
(..)
"babel": {
"presets": [
"react-app"
],
"plugins":[
"@loadable/babel-plugin"
]
}
}
- webpack.congif.js (프런트엔드 웹팩설정파일) 열어서 LoadablePlugin을 불러오고 적용하기
const LoadablePlugin=require('@loadable/webpack-plugin');
(...)
plugins: [
new LoadablePlugin(),
new HTMLWebpackPlugin(
(...)
].filter(Boolean),
(...)
- 저장 후 yarn build 명령어 실행하기
- loadable-stats.json 파일 생성된다.
각 컴포넌트의 코드가 어떤 청크 파일에 들어가 있는지에 대한 정보를 가지고 있다.
이를 통해 ssr을 할 때 어떤 컴포넌트가 렌더링되었는지에 따라 어떤 파일들을 사전에 불러와야 할지 설정 가능하다.
즉 asset-manifest.json을 통해 확인할 필요 없다. (너무좋아 )
- 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");
});
- 비동기 작업(스크립트 로딩 뒤 렌더링 ) : loadableReady 함수
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