3. 서버 사이드 렌더링 구현하기
서바 시이드 렌더링을 구현하려면 웹팩 설정을 커스타마이징 해주어야 합니다. CRA(Create-React-App)로 만든 프로젝트에서는 웹팩 관련 설정을 숨겨 두여서 yarn eject 명령어를 실행하여 보이도록 해주어야 합니다.
git add
git commit -m 'Commit before eject'
yarn eject
3.1 서버 사이드 렌더링용 엔트리 만들기
엔트리(entry)는 웹팩에서 프로젝트를 불러올 때 가장 먼저 불러오는 파일입니다. 예시로 현재 작성 중인 리액트 프로젝트에서는 index.js 파일을 엔트리 파일로 사용하고 있습니다. 이 파일부터 시작하여 내부에 필요한 다른 컴포넌트와 모듈을 불러오고 있습니다.
서버 사이드 렌더링을 할 때는 서버를 위한 엔트리 파일을 따로 생성 해주어야 합니다. src 디렉토리에 index.server.js 파일을 생성 해주세요.
- index.server.js
import React from "react";
import ReactDOMServer from "react-dom/server";
const html = ReactDOMServer.renderToString(
<div>Chul Lee. Server Side Rendering!</div>
);
console.log(html);
서버에서 리액트 컴포넌트를 렌더링할 때는 ReactDOMServer의 renderToString 함수를 사용합니다. 이 함수에 JSX를 넣어서 호출하면 렌더링 결과를 문자열로 반환합니다.
3.2 서버 사이드 렌더링 전용 웹펙 설정하기
작성한 엔트리 파일을 웹팩으로 불러와서 빌드하기 위해서는 서버 환경 설정을 만들어 주어야 합니다.
먼저 처음에는 config 설정 파일이 숨겨져 있기 때문에 보이도록 꺼내주어야 합니다.
아래의 명령어를 입력하여 숨겨져있는 파일을 꺼내주세요.
npm run eject
다음으로 config 경로의 paths.js 파일을 열어서 스크롤을 맨밑으로 내린 후 module.exports 부분에 다음과 같이 두줄을 추가해 주세요.
- 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는 웹팩으로 처리한 결과물을 저장할 경로입니다.
다음으로 웹팩 환경 설정 파일을 작성합니다. config 디렉토리에 webpack.config.server.js 파일을 생성해 주세요.
- config/webpack.config.server.js
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", // 정크 파일 이름
publicPatnh: paths.publicUrlOrPath, // 정적 파일이 제공될 경로
},
};
웹팩 기본 설정을 작성하였습니다. 빌드할 때 어떤 파일에서 시작해 파일들을 볼러오는지, 어딍 결과물을 저장할지 위의 웹팩 설정을 통해서 정해 주었습니다.
다음으로 로더를 설정해야 합니다. 웹팩의 로더는 파일을 불러올 떄 확장자에 맞게 필요한 처리를 해줍니다. 예시로 자바스크립트는 babel을 사용하여 트랜스파일링(특정 언어로 작성된 코드를 다른 언어로 변화하는 작업)을 해주고, CSS는 모든 CSS 코드를 결합해 주고, 이미지 파일은 파일의 경로에 따로 저장합니다.
서버 사이드 렌더링을 할 떄 CSS 혹은 이미지 파일은 크게 중요하지는 않습니다. 다만 자바스크립트 내부에서 해당 파일 경로가 필요하거나 CSS Module처럼 local className을 참조하는 경우도 존재합니다. 그런 경우 해당 파일을 로더에서 별도로 설정하여 처리합니다. 그리고 따로 결과물을 포함되지 않도록 아래와 같이 구현합니다.
- config/webpack.config.server.js
const paths = require("./paths");
const getCSSModuleLocalIdent = require("react-dev-utils/getCSSModuleLocalIdent");
// CSS Module의 고유 className을 만들 때 필요한 옵션
const nodeExternals = require("webpack-node-externals");
// 환경 변수 주입
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 publicUrl = paths.servedPath.slice(0, -1);
const env = getClientEnvironment(publicUrl);
module.exports = {
mode: "production", // 프로덕션 모드로 설정하여 최적화 옵션들을 활성화
entry: paths.ssrIndexJs, // 엔트리 경로
target: "node", // node 환경에서 실행
output: {
path: paths.ssrBuild, // 빌드 경로
filename: "server.js", // 파일 이름
chunkFilename: "js/[name].chunk.js", // 청크 파일 이름
publicPath: paths.servedPath, // 정적 파일이 제공될 경로
},
// 로더 설정
// 웹팩의 로더는 파일을 불러올 때 확장자에 맞게 필요한 처리를 해줌.
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"
),
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,
// onlyLocals: true 옵션을 설정해야 실제 CSS 파일을 생성하지 않는다.
loader: require.resolve("css-loader"),
options: {
onlyLocals: true,
},
},
// CSS Module 처리
{
test: cssModuleRegex,
loader: require.resolve("css-loader"),
options: {
modules: true,
onlyLocals: true,
getLocalIdent: getCSSModuleLocalIdent,
},
},
// SASS 처리
{
test: sassRegex,
exclude: sassModuleRegex,
use: [
{
loader: require.resolve("css-loader"),
options: {
onlyLocals: true,
},
},
require.resolve("sass-loader"),
],
},
// Sass + CSS Module을 위한 처리
{
test: sassRegex,
exclude: sassModuleRegex,
use: [
{
loader: require.resolve("css-loader"),
options: {
module: true,
onlyLocals: 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.67KB가 넘어가면 파일로 저장하는데,
// emitFlie 값이 false일 때는 경로만 준비하고 파일은 저장 안함.
name: "static/media/[name].[hash:8].[ext]",
},
},
// 위에서 설정된 확장자를 제외한 파일은
// file-loader를 사용
{
loader: require.resolve("file-loader"),
exclude: [/\.(js|mjs|jsx|ts|tsx)$/, /\.html$/, /\.json$/],
options: {
emitFlie: false, // 파일을 따로 저장하지 않는 옵션
name: "static/media/[name].[hash:8].[ext]",
},
},
],
},
],
},
};
이렇게 했을때 react, react-dom/server 같은 라이브러리를 import 구문으로 볼러오면 node_modules에서 찾아서 사용합니다.
라이브러리를 불러오면 빌드할 때 결과를 파일 안에 해당 라이브러리 코드에서 번들링됩니다.
브라우저에서 사용할 떄는 결과물 파일에 리액트 라이브러리와 애플리케이션 코드가 공존해야합니다. 서버에서는 결과물 파일안에 라이브러리가 없어도 node_modules를 통해서 불러와서 사용할 수 있기 때문입니다.
그렇기에 서버에서 번들링할 떄는 node_modules에서 불러오는 것을 제외하고 번들링해야 합니다. 이런 부분을 위해 webpack-node-externals 라이브러리를 사용합니다.
이 라이브러리를 설치 해주세요.
yarn add webpack-node-externals
다음으로 라이브러리르 webpack.config.server.js 상단에서 불러와서 설정에 적용합니다.
마지막으로 환경변수를 추가해 줍니다.
- config/webpack.config.server.js
const modeExternals = requir("webpack-node-externals");
(...)
module.exports = {
(...)
resolve: {
modules: ["node_modules"],
},
externals: [
nodeExternals({
allowlist: [/@babel/],
}),
],
};
환경 변수를 주입하면 프로젝트 내에서 process.env.NODE_ENV 값을 참조하여 현재 개발환경인지 아닌지를 구분 할 수 있습니다.
3.3 빌드 스크립트 작성하기
이번에는 방금 만든 환경 설정을 사용하여 웹팩으로 프로젝트를 빌드하는 스크립트를 작성 하겠습니다.
scripts 경로를 얼어보면 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
빌드가 정상적으로 완료 되었으면 dist/server.js 파일이 빌드 되어있을 것 입니다.
이어서 아래의 명령어를 실행하여 작성한 결과가 나타나는지 확인해 보세요.
node dist/server.js
이제 매번 빌드하고 실행할 떄마다 파일 경로를 입력하는 것은 비효율적이니, package.json에 스크립트를 생성하여 편하게 명렁어로 입력하도록 수정 하겠습니다.
- 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"
},
yarn build:server
yarn start:server
이제 작성한 스크립트를 실행하여 동일하게 동작되는지를 확인하시면 됩니다.
이제 서버 사이드 렌더링을 구현할 준비가 되었습니다.
'프론트엔드 > React' 카테고리의 다른 글
[프론트엔드] 서버 사이드 랜더링[5] (0) | 2022.07.06 |
---|---|
[프론트엔드] 서버 사이드 랜더링[4] (0) | 2022.06.30 |
[프론트엔드] 서버 사이드 랜더링[2] (0) | 2022.06.09 |
[프론트엔드] 서버 사이드 랜더링[1] (0) | 2022.06.08 |
[프론트엔드] 코드 스플리팅 [3] (0) | 2022.06.07 |