빌드시 기본 탑재된 SplitChunks 기능의 문제점:
효율적인 캐싱 효과만 있음
a 페이지 방문시 b, c 컴포넌트 정보까지 전부 저장되는 약점
해결방법: 코드 비동기 로딩(처음에는 불러오지 않지만, 필요한 시점에 불러와서 사용하기)
(notfy.js)
export default function notify(){
alert('안녕하세요')
}
import logo from './logo.svg';
import './App.css';
import notify from './notify';
function App() {
const onClick = () => {
notify();
};
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p onClick={onClick}>Hello React!</p>
</header>
</div>
);
}
export default App;
import logo from './logo.svg';
import './App.css';
function App() {
const onClick = () => {
import('./notify').then(result => result.default());
};
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p onClick={onClick}>Hello React!</p>
</header>
</div>
);
}
export default App;
React.lazy: 컴포넌트를 렌더링하는 시점에서 비동기적으로 로딩할 수 있게 해주는 유틸 함수( 맨 위에 사용 가능)
사용할 컴포넌트
(SplitMe.js )
const SplitMe = () => {
return <div>SplitMe</div>;
};
export default SplitMe;
import { Suspense } from 'react';
const SplitMe = React.lazy(() => import('./SplitMe'));
import { Suspense } from 'react';
(...)
<Suspense fallback={<div>loading...</div>}>
<SplitMe />
</Suspense>
3.app.js 전체 코드
1) 자바스크립트 비동기 로딩으로 가져옴 + React.lazy 사용
2) fallback 함수 설정하여 아직 못가져왔을 때 보여줄 jsx 설정
3) visible 이 true 시 컴포넌트 렌더링될 수 있도록 작성
import { useState, Suspense } from 'react';
import logo from './logo.svg';
import './App.css';
const SplitMe = React.lazy(() => import('./SplitMe'));
function App() {
const [visible, setVisible] = useState(false);
const onClick = () => {
setVisible(true);
};
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p onClick={onClick}>Hello React!</p>
<Suspense fallback={<div>loading...</div>}>
{visible && <SplitMe />}
</Suspense>
</header>
</div>
);
}
export default App;
yarn add @loadable/component
import loadable from '@loadable/component';
const SplitMe = loadable(() => import('./SplitMe'));
const SplitMe = loadable(() => import('./SplitMe'), {
fallback: <div>loading...</div>
});
const onMouseOver = () => {
SplitMe.preload();
};
import React, { useState } from 'react';
import logo from './logo.svg';
import './App.css';
import loadable from '@loadable/component';
const SplitMe = loadable(() => import('./SplitMe'), {
fallback: <div>loading...</div>
});
function App() {
const [visible, setVisible] = useState(false);
const onClick = () => {
setVisible(true);
};
const onMouseOver = () => {
SplitMe.preload();
};
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p onClick={onClick} onMouseOver={onMouseOver}>
Hello React!
</p>
{visible && <SplitMe />}
</header>
</div>
);
}
export default App;
정의: ui를 서버에서 렌더링하는 것
목적: 검색 엔진이 웹의 페이지를 수집할 수 있도록 하는 것
단점:
1) 서버 리소스가 사용됨 ->캐싱과 로드밸런싱으로 성능 최적화 필요
2) 프로젝트의 구조가 복잡해질 수 있음
3) 데이터 미리 불러오기 / 코드 스플리팅 호환 등 개발이 어려워질 수 있음
문제: 서버사이드 렌더링과 코드 스플리팅 충돌
해결방법: Loadable Components 사용. 라우트 경로마다 코드 스플리팅된 파일 중 필요한 모든 파일을 브라우저에서 렌더링하기 전에 미리 불러와야 함 (필요한 파일의 경로를 추출하여 렌더링 결과에 스크립트/스타일링 태그를 삽입)
컴포넌트/페이지 컴포넌트 만들기
1을 app.js에 구동하기
import { Route } from 'react-router-dom';
import Menu from './components/Menu';
import RedPage from './pages/RedPage';
import BluePage from './pages/BluePage';
const App = () => {
return (
<div>
<Menu />
<hr />
<Route path="/red" component={RedPage} />
<Route path="/blue" component={BluePage} />
</div>
);
};
export default App;
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import { BrowserRouter } from 'react-router-dom';
ReactDOM.render(
<BrowserRouter>
<App />
</BrowserRouter>,
document.getElementById('root')
);
----------------(서버사이드 렌더링 본격 구현하기) -----
4. 깃허브 추가 및 웹팩 설정 꺼내기
git add .
git commit -m'Commit before eject'
yarn eject
import ReactDOMServer from 'react-dom/server';
const html = ReactDOMServer.renderToString(
<div>Hello Server Side Rendering!</div>
);
console.log(html);
ssrIndexJs: resolveApp('src/index.server.js'), // 서버 사이드 렌더링 엔트리 파일의 경로.
ssrBuild: resolveApp('dist') // 웹팩 처리 후 결과물 저장 경로
publicUrlOrPath,
(전체파일)
(...)
module.exports = {
dotenv: resolveApp('.env'),
appPath: resolveApp('.'),
appBuild: resolveApp('build'),
appPublic: resolveApp('public'),
appHtml: resolveApp('public/index.html'),
appIndexJs: resolveModule(resolveApp, 'src/index'),
appPackageJson: resolveApp('package.json'),
appSrc: resolveApp('src'),
appTsConfig: resolveApp('tsconfig.json'),
yarnLockFile: resolveApp('yarn.lock'),
testsSetup: resolveModule(resolveApp, 'src/setupTests'),
proxySetup: resolveApp('src/setupProxy.js'),
appNodeModules: resolveApp('node_modules'),
swSrc: resolveModule(resolveApp, 'src/service-worker'),
ssrIndexJs: resolveApp('src/index.server.js'), // 서버 사이드 렌더링 엔트리
ssrBuild: resolveApp('dist') // 웹팩 처리 후 저장 경로
publicUrlOrPath,
};
module.exports.moduleFileExtensions = moduleFileExtensions;
const paths = require('./paths');
module.exports = {
mode: 'production', // 프로덕션 모드로 설정하여 최적화 옵션들을 활성화
entry: paths.ssrIndexJs, // 엔트리 경로
target: 'node', // node 환경에서 실행될 것이라는 점을 명시
output: {
path: paths.ssrBuild, // 빌드 경로
filename: 'server.js', // 파일 이름
chunkFilename: 'js/[name].chunk.js', // 청크 파일 이름
publicPath: paths.publicUrlOrPath, // 정적 파일이 제공될 경로
}
};
const paths = require('./paths');
const getCSSModuleLocalIdent = require('react-dev-utils/getCSSModuleLocalIdent');
const cssRegex = /\.css$/;
const cssModuleRegex = /\.module\.css$/;
const sassRegex = /\.(scss|sass)$/;
const sassModuleRegex = /\.module\.(scss|sass)$/;
const env = getClientenvironment(paths.publicUrlOrPath.slice(0, -1));
module.exports = {
mode: 'production', // 프로덕션 모드로 설정하여 최적화 옵션들을 활성화
entry: paths.ssrIndexJs, // 엔트리 경로
target: 'node', // node 환경에서 실행될 것이라는 점을 명시
output: {
path: paths.ssrBuild, // 빌드 경로
filename: 'server.js', // 파일 이름
chunkFilename: 'js/[name].chunk.js', // 청크 파일 이름
publicPath: paths.publicUrlOrPath, // 정적 파일이 제공될 경로
},
module: {
rules: [
{
oneOf: [
// 자바스크립트를 위한 처리
// 기존 webpack.config.js 를 참고하여 작성
{
test: /\.(js|mjs|jsx|ts|tsx)$/,
include: paths.appSrc,
loader: require.resolve('babel-loader'),
options: {
customize: require.resolve(
'babel-preset-react-app/webpack-overrides'
),
presets: [
[
require.resolve('babel-preset-react-app'),
{
runtime: 'automatic',
},
],
],
plugins: [
[
require.resolve('babel-plugin-named-asset-import'),
{
loaderMap: {
svg: {
ReactComponent:
'@svgr/webpack?-svgo,+titleProp,+ref![path]',
},
},
},
],
],
cacheDirectory: true,
cacheCompression: false,
compact: false,
},
},
// CSS 를 위한 처리
{
test: cssRegex,
exclude: cssModuleRegex,
// exportOnlyLocals: true 옵션을 설정해야 실제 css 파일을 생성하지 않습니다.
loader: require.resolve('css-loader'),
options: {
importLoaders: 1,
modules: {
exportOnlyLocals: true,
},
},
},
// CSS Module 을 위한 처리
{
test: cssModuleRegex,
loader: require.resolve('css-loader'),
options: {
importLoaders: 1,
modules: {
exportOnlyLocals: true,
getLocalIdent: getCSSModuleLocalIdent,
},
},
},
// Sass 를 위한 처리
{
test: sassRegex,
exclude: sassModuleRegex,
use: [
{
loader: require.resolve('css-loader'),
options: {
importLoaders: 3,
modules: {
exportOnlyLocals: true,
},
},
},
require.resolve('sass-loader'),
],
},
// Sass + CSS Module 을 위한 처리
{
test: sassRegex,
exclude: sassModuleRegex,
use: [
{
loader: require.resolve('css-loader'),
options: {
importLoaders: 3,
modules: {
exportOnlyLocals: true,
getLocalIdent: getCSSModuleLocalIdent,
},
},
},
require.resolve('sass-loader'),
],
},
// url-loader 를 위한 설정
{
test: [/\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/],
loader: require.resolve('url-loader'),
options: {
emitFile: false, // 파일을 따로 저장하지 않는 옵션
limit: 10000, // 원래는 9.76KB가 넘어가면 파일로 저장하는데
// emitFile 값이 false 일땐 경로만 준비하고 파일은 저장하지 않습니다.
name: 'static/media/[name].[hash:8].[ext]',
},
},
// 위에서 설정된 확장자를 제외한 파일들은
// file-loader 를 사용합니다.
{
loader: require.resolve('file-loader'),
exclude: [/\.(js|mjs|jsx|ts|tsx)$/, /\.html$/, /\.json$/],
options: {
emitFile: false, // 파일을 따로 저장하지 않는 옵션
name: 'static/media/[name].[hash:8].[ext]',
},
},
],
},
],
},
resolve: {
modules: ['node_modules']
}
};
yarn add webpack-node-externals
10 . 모듈 적용하기
const nodeExternals = require('webpack-node-externals');
(...)
module.exports = {
(...)
resolve: {
modules: ['node_modules']
},
externals: [nodeExternals()]
};
const nodeExternals = require('webpack-node-externals');
const paths = require('./paths');
const getCSSModuleLocalIdent = require('react-dev-utils/getCSSModuleLocalIdent');
const webpack = require('webpack');
const getClientEnvironment = require('./env');
const cssRegex = /\.css$/;
const cssModuleRegex = /\.module\.css$/;
const sassRegex = /\.(scss|sass)$/;
const sassModuleRegex = /\.module\.(scss|sass)$/;
const env = getClientEnvironment(paths.publicUrlOrPath.slice(0, -1));
module.exports = {
(...)
resolve: {
modules: ['node_modules'],
},
externals: [
nodeExternals({
allowlist: [/@babel/],
}),
],
}
process.env.BABEL_ENV = 'production';
process.env.NODE_ENV = 'production';
process.on('unhandledRejection', err => {
throw err;
});
require('../config/env');
const fs = require('fs-extra');
const webpack = require('webpack');
const config = require('../config/webpack.config.server');
const paths = require('../config/paths');
function build() {
console.log('Creating server build...');
fs.emptyDirSync(paths.ssrBuild);
let compiler = webpack(config);
return new Promise((resolve, reject) => {
compiler.run((err, stats) => {
if (err) {
console.log(err);
return;
}
console.log(stats.toString());
});
});
}
build();
node scripts/build.server.js
node dist/server.js
"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"
},
yarn add express
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');
});
const serve = express.static(path.resolve('./build'), {
index: false // "/" 경로에서 index.html을 보여 주지 않도록 설정
});
app.use(serve); // 순서가 중요합니다. serverRender 전에 위치해야 합니다.
app.use(serverRender);
{
"files": {
"main.css": "/static/css/main.b5fbe955.chunk.css",
"main.js": "/static/js/main.4706fa80.chunk.js",
"main.js.map": "/static/js/main.4706fa80.chunk.js.map",
"runtime-main.js": "/static/js/runtime-main.c5541365.js",
"runtime-main.js.map": "/static/js/runtime-main.c5541365.js.map",
"static/js/2.7980f885.chunk.js": "/static/js/2.7980f885.chunk.js",
"static/js/2.7980f885.chunk.js.map": "/static/js/2.7980f885.chunk.js.map",
"index.html": "/index.html",
"precache-manifest.b4b36e28ec1848575e2868cbda9aa15b.js": "/precache-manifest.b4b36e28ec1848575e2868cbda9aa15b.js",
"service-worker.js": "/service-worker.js",
"static/css/main.b5fbe955.chunk.css.map": "/static/css/main.b5fbe955.chunk.css.map"
}
}
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="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>
<link href="${manifest.files['main.css']}" rel="stylesheet" />
</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');
});
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,
user: 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 } };
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;
[
{
"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"
}
},
(...)
]
import { combineReducers } from 'redux';
import users from './users';
const rootReducer = combineReducers({ users });
export default rootReducer;
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';
const store = createStore(rootReducer, applyMiddleware(thunk));
ReactDOM.render(
<Provider store={store}>
<BrowserRouter>
<App />
</BrowserRouter>
</Provider>,
document.getElementById('root')
);
import React, { useEffect } from 'react';
import Users from '../components/Users';
import { connect } from 'react-redux';
import { getUsers } from '../modules/users';
const UsersContainer = ({ users, getUsers }) => {
// 컴포넌트가 마운트되고 나서 호출
useEffect(() => {
if (users) return; // users가 이미 유효하다면 요청하지 않음
getUsers();
}, [getUsers, users]);
return <Users users={users} />;
};
export default connect(
state => ({
users: state.users.users
}),
{
getUsers
}
)(UsersContainer);
import UsersContainer from '../containers/UsersContainer';
const UsersPage = () => {
return <UsersContainer />;
};
export default UsersPage;
import { Route } from 'react-router-dom';
import Menu from './components/Menu';
import RedPage from './pages/RedPage';
import BluePage from './pages/BluePage';
import UsersPage from './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;
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;
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;
};
import Users from '../components/Users';
import { connect } from 'react-redux';
import { getUsers } from '../modules/users';
import { Preloader } from '../lib/PreloadContext';
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);
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';
(...)
// 서버 사이드 렌더링을 처리할 핸들러 함수입니다.
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)); // 결과물을 응답합니다.
};
(...)
(...)
import PreloadContext from './lib/PreloadContext';
(...)
// 서버 사이드 렌더링을 처리할 핸들러 함수입니다.
const serverRender = async (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)); // 결과물을 응답합니다.
};
(...)
(...)
function createPage(root, stateScript) {
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>
<link href="${manifest['main.css']}" rel="stylesheet" />
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root">
${root}
</div>
${stateScript}
<script src="${manifest['runtime~main.js']}"></script>
${chunks}
<script src="${manifest['main.js']}"></script>
</body>
</html>
`;
}
const app = express();
// 서버 사이드 렌더링을 처리할 핸들러 함수입니다.
const serverRender = async (req, res, next) => {
(...)
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)); // 결과물을 응답합니다.
};
(...)
(index.js)
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';
const store = createStore(
rootReducer,
window.__PRELOADED_STATE__, // 이 값을 초기 상태로 사용함
applyMiddleware(thunk)
);
ReactDOM.render(
<Provider store={store}>
<BrowserRouter>
<App />
</BrowserRouter>
</Provider>,
document.getElementById('root')
);