CSR
(클라이언트 사이드 렌더링)은 브라우저에서 JavaScript를 통해 동적으로 페이지를 생성하는 방식
- 껍데기 뿐인 html 파일을 다운로드
- bundle.js 다운로드 및 실행
- 추가 데이터(users.json)를 다운로드하여 렌더링
SSR
(서버사이드 렌더링)은 웹 페이지의 초기 로딩 시 서버에서 HTML을 생성하여 클라이언트로 전달하는 방식
- 서버에서 index.html을 렌더링
- 클라이언트는 index.html을 다운로드하여 화면에 띄움
- bundle.js를 다운로드하여 앱을 살아있는 상태로 만듬
CSR | SSR | |
---|---|---|
초기 로딩 속도 | 자바스크립트 파일까지 모두 다운로드 받은 후에 화면이 띄워지기 때문에 초기 로딩 속도가 느리다. | 브라우저는 서버로부터 받은 렌더링 준비가 완료된 HTML 파일을 화면에 바로 띄우기 때문에 초기 로딩 속도가 빠르다. |
이후 구동 속도 | 초기 로딩 시 필요한 모든 파일이 연결되어 있기므로 구동 속도가 빠르다. | 화면을 띄운 후 자바스크립트 로직을 연결하기 때문에 사용 가능한 시점이 뒤늦다. |
조작 가능 시점 | 사용자가 페이지를 볼 수 있음과 동시에 입력 및 조작이 가능하다. | 사용자가 페이지를 볼 수는 있지만 조작은 불가능한 로딩 시간이 존재한다. |
SEO | 빈 HTML 파일이 전달되어 SEO에 취약하다. | 데이터가 포함된 렌더링 준비된 HTML 파일이 전달되어 SEO에 유리하다. |
서버 자원 사용 | 클라이언트가 연산, 라우팅 등을 직접 처리하여 서버의 부담이 적다. | 매번 서버에 로딩을 요청해야 하기 때문에 서버 부하가 크다. |
보안 | 쿠키 외에 사용자 정보를 저장할 공간이 마땅치 않아 보안에 취약할 수 있다. | 렌더링 시, 서버에서의 필터링이나 이스케이핑 등 선조치를 통해 보안 공격에 대응할 수 있다. |
npx create-react-app my-react-app
명령어로 React 앱을 만들어서 npm run start
을 실행한다. 그리고 개발자 도구에서 Network
탭을 열어, localhost:3000 에 진입했을 때 Webpack의 개발용 서버가 어떤 파일들을 응답해주는지 확인해보자
이 중에서 응답받은 파일 중 제일 처음에 있는 HTML 파일 부터 살펴보면
응답 받은 HTML 을 살펴보면 root 라는 id를 가지고 있는 div 태그 안에는 비어있는 것을 볼 수 있다. 네트워크 응답 화면에서 HTML 파일 아래에 bundle.js 라는 javascript 파일도 받았는데, 이 javascript 파일을 통해 element 들을 그리게 된다. 이것이 CSR의 동작하는 원리이다.
위에서 리액트는 CSR인 것을 볼 수 있는데 서버사이드 리액트는 말그대로 서버사이드에서 리액트가 렌더링이 되도록 하는 것을 말한다. 서버사이드 리액트 같은 경우 완성된 html을 html파일로 보내지 않고 string으로 보낸다.
SSR에서 백엔드 개발환경은 api서버와 렌더링 서버로 나뉜다. 2가지로 나누는 이유는 서버 부하를 막기위함이다.
나는 렌더링 서버를 express로 구현하였다.
npm init
npm install
로 패키지 다운로드{
"name": "react-server-side-rendering",
"version": "1.0.0",
"description": "react server side rendering project",
"main": "index.js",
"scripts": {},
"author": "",
"license": "ISC",
"dependencies": {
"axios": "0.16.2",
"babel-cli": "6.26.0",
"babel-core": "6.26.0",
"babel-loader": "7.1.2",
"babel-preset-env": "1.6.0",
"babel-preset-es2015": "6.24.1",
"babel-preset-es2017": "6.24.1",
"babel-preset-react": "6.24.1",
"babel-preset-stage-0": "6.24.1",
"compression": "1.7.0",
"concurrently": "3.5.0",
"express": "4.15.4",
"express-http-proxy": "1.0.6",
"lodash": "4.17.4",
"nodemon": "1.12.0",
"npm-run-all": "4.1.1",
"react": "16.0.0",
"react-dom": "16.0.0",
"react-helmet": "5.2.0",
"react-redux": "5.0.6",
"react-router-config": "1.0.0-beta.4",
"react-router-dom": "4.2.2",
"redux": "3.7.2",
"redux-thunk": "2.2.0",
"serialize-javascript": "1.4.0",
"webpack": "3.5.6",
"webpack-dev-server": "2.8.2",
"webpack-merge": "4.1.0",
"webpack-node-externals": "1.6.0"
}
}
index.js
const express = require("express");
const app = express();
const React = require("react");
const renderToString = require("react-dom/server").renderToString;
const Home = require("./components/Home").default;
app.get("/", (req, res) => {
const content = renderToString(<Home />); // renderToString은 string으로 바꾸는 메서드
res.send(content);
}); // 사용자가 루트 주소로 요청하면 콜백함수를 실행한다.
app.listen(3000, () => {
console.log("3000번 포트가 열렸습니다!");
}); // 서버가 포트를 연다.
Home.js
import React from "react";
const Home = () => {
return (
<div>
<h1>this is Eunsu!!</h1>
</div>
);
};
export default Home;
``
클라이언트사이드와 마찬가지로 서버사이드도 jsx를 해석하는 과정이 필요하다. jsx문법이 nodejs 환경이 이해하는 문법으로 바뀌어야하고 이를 webpack과 babel이 한다.
따라서 webpack.server.js 파일 생성해서 webpack 설정을 해줘야한다.
build 폴더 생성
webpack.server.js
const path = require("path");
module.exports = {
target: "node", // 웹팩이 빌드할 때 노드환경에서 돌아가도록 빌드해야하는 구나를 알게됨.
entry: path.resolve("./src/index.js"),
output: {
filename: "bundle.js",
path: path.resolve("./build"),
},
module: {
rules: [
{
test: /\.js?$/,
loader: "babel-loader",
exclude: /node_modules/,
options: {
presets: [
"react",
"stage-0", // 최신 js문법도 사용할 수 있도록 만들어주는 옵션
["env", { target: { browsers: ["last 2 versions"] } }], // babel을 어떤 환경에서 구동시킬 것인가. 최신 브라우저에서 2버전 전까지도 지원하겠다. 라는 뜻
],
},
},
],
},
};
"build-server": "webpack --config webpack.server.js"
추가npm run build-server
명령어 터미널에 입력node build/bundle.js
터미널에 치면, localhost:3000에 Home 컴포넌트
가 string 형태로 온 것을 볼 수 있다.npm run build
명령어와 node build/bundle.js
를 치는 것은 번거롭기 때문에 package.json파일에 아래와 같은 명령어 들을 추가한다."scripts": {
"build-server": "webpack --config webpack.server.js --watch",
"start-server": "nodemon --watch build --exec node build/bundle.js"
},
webpack --config webpack.server.js --watch
: 수정사항 생길 때마다 자동으로 빌드해라nodemon --watch build --exec node build/bundle.js
: build폴더를 감시하고 수정사항이 생기면, node build/bundle.js 명령어를 실행시켜라import React from "react";
const Home = () => {
return (
<div>
<h1>this is Eunsu!!</h1>
<button
onClick={() => {
console.log("clicked");
}}
>
버튼입니다
</button>
</div>
);
};
export default Home;
public 폴더 생성
webpack.client.js
const path = require("path");
module.exports = {
entry: path.resolve("./src/client.js"),
output: {
filename: "bundle.js",
path: path.resolve("./public"),
},
module: {
rules: [
{
test: /\.js?$/,
loader: "babel-loader",
exclude: /node_modules/,
options: {
presets: [
"react",
"stage-0", // 최신 js문법도 사용할 수 있도록 만들어주는 옵션
["env", { target: { browsers: ["last 2 versions"] } }], // babel을 어떤 환경에서 구동시킬 것인가. 최신 브라우저에서 2버전 전까지도 지원하겠다. 라는 뜻
],
},
},
],
},
};
"scripts": {
...
"build-client": "webpack --config webpack.client.js --watch"
},
...
import React from "react";
import ReactDOM from "react-dom";
import Home from "./components/Home.js";
ReactDOM.hydrate(<Home />, document.querySelector("#root"));
import express from "express";
const app = express();
import React from "react";
import { renderToString } from "react-dom/server";
import Home from "./components/Home.js";
app.use(express.static("public")); // express.static(): 지정해둔 디렉토리에 있는 정적파일을 클라이언트로 제공해라라는 메서드
app.get("/", (req, res) => {
const html = `
<html>
<head></head>
<body>
<div id="root"></div>
<script src="bundle.js"></script>
</body>
</html>
`;
res.send(html);
});
app.listen(3000, () => {
console.log("3000번 포트가 열렸습니다!");
});
이 글은 멋쟁이사자처럼 한재현 강사님 수업시간에 배운 내용 + 제가 추가적으로 공부한 내용을 바탕으로 하고 있습니다.