Server Side Rendering이란 클라이언트가 매번 서버에게 데이터를 요청하여 서버에서 처리하는 방식
서버에서 렌더링 준비를 마친 html 파일과 js 코드를 브라우저에게 넘겨 준 뒤에
브라우저에서 html을 렌더링하고 js 파일을 다운 받아 html 코드와 연결하는 렌더링 방식
장점 : 검색엔진 최적화, 초기 구동 빠름, js 다운이 안되는 브라우저에서도 작동
단점 : TTV (Time To View), TTI (Time To Interact)의 시간이 달라서 처음에 작동이 안될 수 있음, 초기로딩이후 페이지 전환시 느림
❗️ Next.js를 사용해보기전에 기본적으로 코딩을 해보려고 한다
프로젝트 생성
mkdir test-ssr
cd test-ssr
npm init -y
npm install react react-dom
패키지 설치
npm install @babel/core @babel/preset-env @babel/preset-react
npm install webpack webpack-cli babel-loader clean-webpack-plugin html-webpack-plugin
src/About.js
import React from "react";
export default function About() {
return (
<div>
<h3>{"This is about page"}</h3>
</div>
);
}
src/Home.js
import React from "react";
export default function Home() {
return (
<div>
<h3>{"This is home page"}</h3>
</div>
);
}
src/App.js
import React, { useEffect, useState } from "react";
import About from "./About";
import Home from "./Home";
export default function App({ pages }) {
const [page, setPage] = useState(pages);
useEffect(() => {
window.onpopstate = (event) => {
setPage(event.state);
};
}, []);
const onChagePage = (e) => {
const newPage = e.target.dataset.page;
window.history.pushState(newPage, "", `/${newPage}`);
setPage(newPage);
};
const PageComponent = page === "home" ? Home : About;
return (
<div className={"container"}>
<button data-page={"home"} onClick={onChagePage}>
{"Home"}
</button>
<button data-page={"about"} onClick={onChagePage}>
{"about"}
</button>
<PageComponent />
</div>
);
}
src/index.js
import React from "react";
import { render } from "react-dom";
import App from "./App";
// template/index.html의 root안에 App 컴포넌트 추가
render(<App pages={"home"} />, document.getElementById("root"));
웹팩 설정
webpack.config.js
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
module.exports = {
entry: "./src/index.js",
output: {
filename: "[name].[chunkhash].js",
path: path.resolve(__dirname, "dist"),
},
module: {
rules: [
{
test: /\.js$/,
use: {
loader: "babel-loader", // 모든 자바스크립트 파일을 바벨 로더로 처리
},
},
},
],
},
plugins: [
new HtmlWebpackPlugin({
template: "./template/index.html", // index.html파일을 기반으로 html파일 생성
}),
],
mode: "production",
};
바벨 설정
babel.config.js
// 바벨 로더 실행시에 적용됨
const presets = ["@babel/preset-react", "@babel/preset-env"];
const plugins = [];
module.exports = { presets, plugins };
template/index.html
<!DOCTYPE html>
<html>
<head>
<title>test-ssr</title>
</head>
<body>
<div id="root"></div>
</body>
</html>
위의 클라이언트 코드를 요약해 보자면 Home
과 About
컴포넌트를 가지고 있는 App
컴포넌트를 index.js에서 template/index.html의 root 안에 넣어준다.
이 코드들을 webpack을 이용해서 dist폴더 아래에 index.html과 main.[hash값].js로 바벨을 이용해서 코드변환 한뒤에 압축해서 만들어 준다.
📌 file:///Users/[username]/test-ssr/dist/index.html경로를 브라우저에 넣어보면 App.js가 나온다 대신 url이 file로 시작해서 에러가 난다.
npm install express @babel/cli @babel/plugin-transform-modules-commonjs
웹서버 코드
src/server.js
import express from "express";
import fs from "fs";
import path from "path";
import { renderToString } from "react-dom/server"; // react-dom/server 안에 서버 사용되는 것들 모여있음
import React from "react";
import App from "./App";
const app = express(); // app변수(express 객체)를 이용해서 미들웨어와 url 경로 설정
const html = fs.readFileSync(
// 웹팩 빌드후에 생성되는 dist의 index.html을 가져와서 이걸로 새로운 html만듬
path.resolve(__dirname, "../dist/index.html"),
"utf8"
);
app.use("/dist", express.static("dist")); // 웹팩으로 빌드한 자바스크립트 코드를 서비스함
app.get("/favicon.ico", (req, res) => res.sendStatus(204)); // 아래에서 favicon 안쓰도록
app.get("*", (req, res) => {
const renderString = renderToString(<App pages="home" />); // app컴포넌트 렌더링
const result = html.replace(
// 렌더링된 결과를 index.html root 안에 반영
'<div id="root></div>',
`<div id="root>${renderString}</div>`
);
res.send(result); // 클라이언트에 전송
});
app.listen(3000); // 3000포트로 들어오는 요청을 기다림
바벨 설정
.babelrc.common.js
.babelrc.client.js
.babelrc.server.js
// 공통으로 사용되는 설정은 여기
const presets = ["@babel/preset-react"];
const plugins = [];
module.exports = { presets, plugins };
const config = require("./.babelrc.common.js"); // common을 가져와서 사용
config.presets.push("@babel/preset-env"); // 클라이언트에서 필요한 preset 추가
module.exports = config;
const config = require("./.babelrc.common.js");
config.plugins.push("@babel/plugin-transform-modules-commonjs"); // 서버에서 필요한 플러그인 추가
module.exports = config;
웹팩 설정
클라이언트에서 한거에서 publicPath, module.rules.use 만 바꾸면 된다
webpack.config.js
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
module.exports = {
entry: "./src/index.js",
output: {
filename: "[name].[chunkhash].js",
path: path.resolve(__dirname, "dist"),
// SSR 할때 필요함, server.js 에서
// /dist 로 시작하는 경우에만 dist 폴더 내의 파일을 서비스 하도록 설정해서
publicPath: "/dist/",
},
module: {
rules: [
{
test: /\.js$/,
use: {
loader: "babel-loader", // 모든 자바스크립트 파일을 바벨 로더로 처리
options: {
// 웹팩을 클라이언트 코드에 대해서만 실행하기 때문에 클라이언트 설정으로 실행되도록 했음
configFile: path.resolve(__dirname, ".babelrc.client.js"),
},
},
},
],
},
plugins: [
new HtmlWebpackPlugin({
template: "./template/index.html", // index.html파일을 기반으로 html파일 생성
}),
],
mode: "production",
};
package.json
"scripts": {
// 서버측 코드 빌드 src밑에 있는 모든 파일을 babelrc.server 설정으로 컴파일
"build-server":"babel src --out-dir dist-server --config-file ./.babelrc.server.js",
// 서버와 클라이언트 둘다 빌드, 클라이언트는 웹팩 실행
"build":"npm run build-server && webpack",
// express 서버 띄움, build후 실행 해야함
"start" :"node dist-server/server.js",
...
},
src/index.js
import React from "react";
import { hydrate } from "react-dom";
import App from "./App";
// SSR 결과로 온 돔에 이벤트 처리함수 붙혀줌
hydrate(<App pages={"home"} />, document.getElementById("root"));
npm run build
npm start
localhost:3000 들어가서 보면 잘 나오는것을 확인 할 수 있다.
서버측 코드(server.js)를 요약해 보자면
renderToString
으로 가져온뒤에 위에서 가져온 html의 root 안에 넣어준 뒤에 클라이언트에게 전송한다