SSR(Server-Side Rendering)은 서버에서 HTML 페이지를 렌더링한 후 클라이언트에 전달하는 방식입니다. 이로 인해 초기 로딩 속도가 빨라지고, 검색 엔진 최적화(SEO)에 유리하며, 서버 측에서 데이터를 처리하기 때문에 보안성이 높습니다. 그러나 페이지 이동 시 클라이언트에서 전체 페이지를 다시 로드해야 하므로 느릴 수 있으며, 서버에서 모든 렌더링을 처리하기 때문에 서버 부하가 증가할 수 있습니다.
1 ) 초기 로드 속도 향상
2 ) 사용자 경험 개선
3 ) 보안 강화
4 ) 초기 데이터 패칭
1 ) 서버 부하 증가
2 ) 복잡한 설정 및 유지보수
3 ) 네트워크 지연
4 ) 제한된 브라우저 기능
window
객체나 document
객체를 사용하는 기능)을 서버에서 사용할 수 없으므로 이러한 기능을 관리하는 데 어려움이 있을 수 있습니다.현재 SSR를 기능을 제공해주는 프레임워크 Next.js, Remix 등으로 편리하고, 쉽게 SSR 기능을 사용할 수 있습니다. 이런 프레임워크 없이 React만으로 SSR를 구현해보고, 어떻게 SSR 처리가 흘러가는지 알아보겠습니다.
현재 프론트엔드에서 사용하는 SSR은 자세히 본다면 Universal Rendering이라고 불러야합니다.
SSR과 CSR을 혼합한 방식이기 때문입니다.
Universal Rendering 에서는 최초 렌더링(SSR)에 필요한 node/
파일과 이후 CSR에 필요한 web/
파일 2가지 결과물이 필요합니다. 즉, 기존 코드에서 node/
파일을 새로 만들어줘야 한다는 뜻입니다.
SSR 구현을 위해 필요사항
node/
: SSR에 필요한 마크업을 만드는 코드가 들어있습니다.web/
: 최초 렌더링 이후 hydreate와 CSR에 필요한 파일이 들어있습니다.Rendering Server
: node/
를 이용해 HTML을 만들고 렌더링하여 클라이언트에 보내주게 됩니다.SSR 구현을 위해 생각해야하는 부분
1 ) Webpack 설정
SSR에서는 node/
, web/
2종류의 트랜스파일링을 해야 합니다.
2 ) 렌더링용 Express Server 구축
SSR 처리를 위한 렌더링용 Express Server 구축이 필요합니다.
필요한 패키지 설치
아래 명령어로 필요한 패키지를 설치합니다.
npm install react react-dom express
npm install -D typescript
npm install -D @types/express @types/node @types/react @types/react-dom
npm install -D webpack webpack-cli ts-loader html-webpack-plugin
webpack 설정하기
서버 및 클라이언트 config를 구분해서 생성합니다.
webpack.client.js
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
/** @type {import('webpack').Configuration} */
module.exports = {
mode: "development",
entry: path.resolve(__dirname, "src/client/main.tsx"),
resolve: {
extensions: [".ts", ".js", ".tsx", ".jsx"],
},
output: {
path: path.resolve(__dirname, "dist/client"),
},
module: {
rules: [
{
test: /\.(ts|tsx|js|jsx)$/,
use: "ts-loader",
exclude: /node_modules/,
},
],
},
plugins: [
new HtmlWebpackPlugin({
template: "src/client/index.html",
}),
],
};
webpack.server.js
const path = require("path");
/** @type {import('webpack').Configuration} */
module.exports = {
target: "node",
mode: "development",
entry: path.resolve(__dirname, "src/server/index.tsx"),
resolve: {
extensions: [".ts", ".js", ".tsx", ".jsx"],
},
output: {
filename: "[name].js",
path: path.resolve(__dirname, "dist/server"),
},
module: {
rules: [
{
test: /\.(ts|tsx|js|jsx)$/,
use: "ts-loader",
exclude: /node_modules/,
},
],
},
};
webpack.config.js
const path = require("path");
const clientConfig = require("./webpack.client.js");
const serverConfig = require("./webpack.server.js");
/** @type {import('webpack').Configuration[]} */
module.exports = [
{
...clientConfig,
},
{
...serverConfig,
},
];
client index.tsx 생성
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>React SSR</title>
</head>
<body>
<div id="root"></div>
</body>
</html>
client App.tsx 생성
import { Suspense, useState } from "react";
type Todo = {
content: string;
id: string;
isDone: boolean;
};
const App = () => {
const [todos, setTodos] = useState<Todo[]>([]);
const toggleDone = (id: string) => {
const newTodos = [...todos].map((todo) => {
if (todo.id === id) {
todo.isDone = !todo.isDone;
return todo;
}
return todo;
});
setTodos(newTodos);
};
const removeTodo = (id: string) => {
const newTodos = [...todos].filter((todo) => todo.id !== id);
setTodos(newTodos);
};
const onSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
const value = formData.get("todo-input");
if (value) {
const newTodo = {
content: value as string,
id: new Date().getTime().toString(),
isDone: false,
};
setTodos((prev) => [...prev, newTodo]);
e.currentTarget.reset();
}
};
return (
<>
<h1>React SSR TodoList</h1>
<h2>Add to Task.</h2>
<form onSubmit={onSubmit}>
<input name="todo-input" />
<button type="submit">Add</button>
</form>
<ul
style={{
margin: "10px 0",
padding: 0,
display: "flex",
flexDirection: "column",
gap: "10px",
}}
>
{todos.map((todo) => (
<li
style={{
margin: 0,
padding: 0,
listStyle: "none",
display: "flex",
gap: "5px",
alignItems: "center",
}}
key={todo.id}
>
<input
type="checkbox"
defaultChecked={todo.isDone}
onClick={() => toggleDone(todo.id)}
/>
<p
style={{
margin: 0,
padding: 0,
textDecoration: todo.isDone ? "line-through" : undefined,
color: todo.isDone ? "rgba(155,155,155)" : undefined,
}}
>
{todo.content}
</p>
<button onClick={() => removeTodo(todo.id)}>remove</button>
</li>
))}
</ul>
</>
);
};
export default App;
client inext.tsx 생성
import ReactDOM from 'react-dom/client';
import App from './App';
ReactDOM.hydrateRoot(document.getElementById('root') as HTMLElement, <App />);
createRoot 대신 hydreateRoot
를 사용합니다.
hydrateRoot
는 SSR로 생성된 HTML를 브라우저 DOM node 내부에 리액트 컴포넌트들을 보여질 수 있도록 합니다. 즉, 서버에서 렌더링된 HTML을 읽고 이를 React 컴포넌트와 연결합니다. 이 과정(hydreate)에서 React는 기존의 HTML 요소에 이벤트 리스너를 부착하고, 상태를 관리할 수 있게 만듭니다.
Rendering Server 구축
import express from "express";
import fs from "fs";
import path from "path";
import ReactDOMServer from "react-dom/server";
import App from "../client/App";
const app = express();
// client build index.html 가져오기
const html = fs.readFileSync(
path.resolve(__dirname, "../client/index.html"),
"utf-8"
);
app.get("/", (req, res) => {
// <App /> 컴포넌트 렌더링
const renderString = ReactDOMServer.renderToString(<App />);
// root <div>에 내부에 <App /> 컴포넌트 삽입
res.send(
html.replace(
'<div id="root"></div>',
`<div id="root">${renderString}</div>`
)
);
});
// dist/client 폴더에 있는 파일들 제공
app.use("/", express.static("dist/client"));
app.listen(3000, () => {
console.log("Server is listening on port 3000");
});
package.json scripts 명령어 추가
package.json 파일 scripts에 아래 명령어를 추가해줍니다.
"scripts": {
"build": "webpack --config webpack.config.js",
"start": "node dist/server/main.js"
}
build 및 실행하기
npm run bild
npm run start
실행 후 개발자 도구를 통해 요소를 살펴보면 SSR이 이루어져 root div안이 채워져 있는 것을 볼 수 있습니다. 실제 SSR은 이것보다 훨씬 복잡하게 이루어져 있지만 위 내용을 통해 SSR처리가 어떻게 흘러가는지 알 수 있었습니다.
SSR(server-side-rendering)은 서버에서 HTML 파일을 생성하고 클라이언트로 전달하는 방식입니다. 서버는 클라이언트의 요청에 따라 필요한 데이터를 가져와 HTML 파일을 파싱하여 렌더링합니다. 이후 이 HTML 파일이 클라이언트로 전송되며, 클라이언트 측에서는 전달받은 HTML에 JavaScript를 통해 동적인 기능을 추가하는 hydrate
과정을 거칩니다.
https://minoo.medium.com/next-js-처럼-server-side-rendering-구현하기-7608e82a0ab1
https://solo5star.tistory.com/44