SSR (Server Side Rendering)이란?
말 그대로 이다. 사용자에게 제공할 UI를 서버에서 미리 렌더링 하는 것 이다.
반대로, CSR (Client Side Rendering)은 브라우저에서 UI를 렌더링 하는 것 이다.
각 방식의 장단점
Server Side Rendering | Client Side Rendering | |
장점 | 1. 검색 엔진이 웹 애플리케이션의 페이지를 원할하게 수집할 수 있다. 2. 초기 헨더링 성능을 개선할 수 있다. |
1. SSR에 비해 속도가 빠르다. 2. 서버의 부하가 낮다. |
단점 | 1. 브라우저 대신 서버가 처리하므로 서버 리소스가 사용된다. 2. 프로젝트 구조가 복잡해지고, 개발이 어려워질 수도 있다. |
1. 검색 엔진이 페이지의 정보를 수집하기 어렵다. 2. 모든 파일을 받아와서 초기 로딩 속도가 느리다. |
서버 사이드 렌더링과 코드 스플리팅 충돌
SSR과 코드 스플리팅을 같이 적용하려하면 조금 어렵다..... 복잡하기도 하고.....
호환 작업 없이 적용하면 페이지에 깜빡거림이 발생한다.
이때는 Loadable Components 라이브러리에서 제공하는 기능을 사용하여
SSR후 필요한 경로를 추출하여 렌더링 결과에 스크립트/스타일 태그를 삽입해서 해결할 것이다.
프로젝트 준비 (기본적인 작업이므로 접은글로 정리함)
react-router-dom 라이브러리를 설치하자
npm add react-router-dom
1. 컴포넌트를 만들기
1) components/Red.js
import './Red.css';
const Red = () => {
return <div className="Red">Red</div>
};
export default Red;
2) components/Red.css
.Red{
background: red;
font-size: 1.5rem;
color: white;
width: 128px;
height: 128px;
display: flex;
align-items: center;
justify-content: center;
}
3) components/Blue.js
import './Blue.css';
const Blue = () => {
return <div className="Blue">Blue</div>
};
export default Blue;
4) components/Blue.css
.Blue{
background: blue;
font-size: 1.5rem;
color: white;
width: 128px;
height: 128px;
display: flex;
align-items: center;
justify-content: center;
}
5) components/Menu.js
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. 페이지 컴포넌트 만들기
각 라우트를 위한 페이지 컴포넌트를 만들자
1) pages/RedPage.js
import Red from '../components/Red';
const RedPage = () => {
return <Red />;
};
export default RedPage;
2) pages/BluePage.js
import Blue from '../components/Blue';
const BluePage = () => {
return <Blue />;
};
export default BluePage;
이제 App.js와 index.js 까지 설정해주자
1) App.js
import { Routes, Route } from 'react-router-dom';
import Menu from './components/Menu';
import RedPage from './pages/RedPage';
import BluePage from './pages/BluePage';
function App() {
return (
<div>
<Menu />
<hr />
<Routes>
<Route path="/red" element={<RedPage />} />
<Route path="/blue" element={<BluePage />} />
</Routes>
</div>
);
};
export default App;
2) index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import App from './App';
import reportWebVitals from './reportWebVitals';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<BrowserRouter>
<App />
</BrowserRouter>
);
// 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
reportWebVitals();
이제 SSR을 구현할 프로젝트가 준비되었다.
💢서버 사이드 렌더링 구현하기
웹펙 관련 설정을 보이게 하기 위해 npm run eject 를 사용하자
1) SSR용 엔트리 만들기
엔트리(entry)는 웹펙에서 프로젝트를 불러올 때 가장 먼저 불러오는 파일이다.
SSR을 할 때는 서버를 위한 엔트리 파일을 따로 생성해야한다.
index.server.js
import ReactDOMServer from 'react-dom/server';
//서버에서 리액트 컴포넌트를 렌더링할 때는 ReactDOMServer의 renderToString함수를 사용한다.
//이 함수에 JSX를 넣어 호출하면 렌더링 결과를 문자열로 반환한다.
const html = ReactDOMServer.renderToString(
<div>Hello Server Side Rendering!</div>
);
console.log(html);
주석으로도 적혀있지만, 서버에서 리액트 컴포넌트를 렌더링할 때는 ReactDOMServer.renderToString를 사용한다.
2) SSR용 웹팩 환경 설정 작성하기
1. 작성한 엔트리 파일을 웹팩으로 불러와서 빌드하려면 서버 전용 환경 설정을 만들어야 한다.
config/paths.js
(...)
// config after eject: we're in ./config/
module.exports = {
dotenv: resolveApp('.env'),
appPath: resolveApp('.'),
appBuild: resolveApp(buildPath),
appPublic: resolveApp('public'),
appHtml: resolveApp('public/index.html'),
appIndexJs: resolveModule(resolveApp, 'src/index'),
appPackageJson: resolveApp('package.json'),
appSrc: resolveApp('src'),
appTsConfig: resolveApp('tsconfig.json'),
appJsConfig: resolveApp('jsconfig.json'),
yarnLockFile: resolveApp('yarn.lock'),
testsSetup: resolveModule(resolveApp, 'src/setupTests'),
proxySetup: resolveApp('src/setupProxy.js'),
appNodeModules: resolveApp('node_modules'),
appWebpackCache: resolveApp('node_modules/.cache'),
appTsBuildInfoFile: resolveApp('node_modules/.cache/tsconfig.tsbuildinfo'),
swSrc: resolveModule(resolveApp, 'src/service-worker'),
ssrIndexJs: resolveApp('src/index.server.js'), // 서버 사이드 렌더링 엔트리
ssrBuild: resolveApp('dist'), // 웹팩 처리 후 저장 경로
publicUrlOrPath,
};
module.exports.moduleFileExtensions = moduleFileExtensions;
ssrIndexJs는 불러올 파일의 경로이고, ssrBuild는 웹팩으로 처리한 뒤 결과물을 저장할 경로이다.
2. 웹팩 환경 설정 파일을 작성한다. 그리고 로더를 설정한다.
웹팩의 로더는 파일을 불러올 때 확장자에 맞게 필요한 처리를 해준다.
SSR시 css혹은 이미지 파일은 중요하지 않을 수 있지만, js내부에서 파일에 대한 경로가 필요하거나 css module처럼
로컬 className을 참조할 수도 있다.
그래서 해당 파일 로더에서는 별도로 설정하여 처리하지만 따로 결과물에 포함되지 않도록 구현할 수 있다.
config/webpack.config.server.js 너무 길어서 접은글로 정리
// 서버를 번들링할 때 node_modules에서 불러오는 것을 제외하고 번들링 하기 위해 webpack-node-externals 라이브러리 사용
const nodeExternals = require('webpack-node-externals');
const paths = require('./paths');
const getCSSModuleLocalIdent = require('react-dev-utils/getCSSModuleLocalIdent');
// 환경변수를 주입
const webpack = require('webpack');
const getClientEncironment = require('./env');
const cssRegex = /\.css$/;
const cssModuleRegex = /\.module\.css$/;
const sassRegex = /\.(scss||sass)$/;
const sassModuleRegex = /\.module\.(scss||sass)$/;
const env = getClientEncironment(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-preset-react-app/webpack-overrides'
),
options: {
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]',
},
},
],
},
],
},
// 이제 코드에서 node_modules 내부의 라이브러리를 불러올 수 있게 설정함
resolve: {
modules:['node_modules']
},
externals: [
nodeExternals({
allowlist: [/@babel/],
}),
],
};
3) 빌드 스크립트 작성하기
방금 만든 환경 설정을 사용해서 웹팩으로 프로젝트를 빌드하는 스크립트를 작성해 보자.
script/build.js라는 스크립트는 클라이언트에서 사용할 빌드 파일을 만드는 작업을 한다.
이 스크립트와 비슷한 형식으로 서버에서 사용할 빌드 파일을 만드는 build.server.js 스크립트를 작성해 보자
scripts/build.server.js
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
위의 명령어를 입력하는 것이 번거로울 수 있으니 package.json에서 스크립트를 생성하여 간단하게 하자
(...)
"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"
},
(...)
4. 서버 코드 작성하기
SSR을 처리할 서버를 작성해 보자
Express라는 Node.js 뤱 프레임워크를 사용하여 웹 서버를 만들어보자
Express는 사용률이 가장 높고, 정적 파일들을 호스팅할 때도 쉽게 구현할 수 있기에 사용한다.
먼저 Express를 설치해주자
npm add express
다음으로 index.server.js 코드를 작성한다.
import ReactDOMServer from 'react-dom/server';
import express from 'express';
import { StaticRouter } from 'react-router-dom/server';
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');
});
이때 StaticRouter라는 컴포넌트가 사용됬다.
이는 주로 서버 사이드 렌더링 용도로 사용되는 라우터 이다.
props로 넣어 주는 location 값에 따라 라우팅 해준다.
req.url이라는 값을 넣었는데, req객체는 요청에 대한 정보를 지니고 있다.
StaticRouter에 context라는 props도 넣었는데 이 값을 사용하여 나중에 렌더링한
컴포넌트에 따라 HTTP상태 코드를 설정할 수 있다.
한번 서버를 다시 빌드하고 실행해 보자
npm run build:server
npm run start:server
아직 css를 불러오지 않아 스타일 적용은 안된 상태이다.
그러나 서버 사이드 렌더링은 된 상태이다.
개발자 도구의 Network 탭을 열어 새로고침하고 Response를 눌러 확인해 보자
5. 정적 파일 제공하기
이번에는 Express에 내장된 static 미들웨어를 사용해 서버를 통해 build에 있는 JS, CSS파일에 접근할 수 있게 하자
index.server.js
import ReactDOMServer from 'react-dom/server';
import express from 'express';
import { StaticRouter } from 'react-router-dom/server';
import App from './App';
import path from 'path';
import fs from 'fs';
// asset-mainfest.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>
`;
} // 위의 runtime-main.js 참조는 현제 asset-manifest.json에 없으므로 적용안됨
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)); // 클라이언트에게 결과물 응답
};
// static 미들웨어를 이용해 서버를 통해 build에 있는 js, css 정적 파일에 접근하게 해줌
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');
});
JS와 CSS 파일을 불러오도록 html에 코드를 삽입해 주었다.
불러와야 하는 파일 이름은 매번 빌드할 때마다 바뀌기 때문에 빌드하고 나서 만들어지는
asset-manifest.json 파일을 참고하여 불러오도록 작성했다.
다시 서버를 빌드하고 실행했을 때, 다른 링크를 클릭하여 다른 페이지로 이동할 때 추가 네트워크 요청이 없어야 한다.
지금까지 서버 사이드 렌더링을 구현했다.
이제 데이터 로딩 즉, API 요청을 다뤄보도록 하자.
'React' 카테고리의 다른 글
React) 20장 서버 사이드 렌더링 (3) SSR과 코드 스플리팅 (0) | 2022.06.02 |
---|---|
React) 20장 서버 사이드 렌더링 (2) 데이터 로딩 (0) | 2022.06.02 |
React) 19장 코드 스플리팅 ( React.lazy-Suspense, loadable_component ) (0) | 2022.05.23 |
React) 18장 리덕스 미들웨어를 이용한 비동기 작업관리 ( redux-saga ) (0) | 2022.05.21 |
React) 18장 리덕스 미들웨어를 이용한 비동기 작업관리 ( Thunk ) (0) | 2022.05.20 |