SSR은 서버에서 html을 생성하고, CSR은 브라우저에서 html 을 생성한다.
그렇다면 예시를 보며 비교해보자
우리는 api 서버에서 영화 목록을 불러오고, 불러온 영화목록을 화면에 보여줘야하는 상황이다. 이때 뼈대 html 코드는 다음과 같다.
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="../assets/styles/reset.css" />
...생략
<script src="./"></script>
<title>영화 리뷰</title>
</head>
<body>
<div id="wrap">
<main>
<section>
<h2>지금 인기 있는 영화</h2>
<ul class="thumbnail-list">
<!--${MOVIE_ITEMS_PLACEHOLDER}-->
</ul>
</section>
</main>
...생략
다음과 같은 html이 있을때 데이터를 fetch해서 최종 html 을 어떻게 완성하는지
SSR, SSR+CSR 별로 살펴보자.
🙎 가정 : 초기 페이지에 접속하면 영화 목록을 확인하고 싶어요
이때, 초기 렌더링 속도를 줄이기 위해 SSR 를 활용하고 싶어요.
참고 : 다음 코드는 초기 ssr인 jsp 같이 api 서버도 같이 처리하는 서버가 아닌 js로 구현한 SSR이다.
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const router = Router();
const renderMoviePage = async (req, res) => {
try {
const templatePath = path.join(__dirname, "../../views", "index.html");
const movies = await fetchMovies();
// 1️⃣ 영화 목록 fetch
const featuredMovie = movies[0];
const moviesHTML = renderMovieItems(movies);
let template = fs.readFileSync(templatePath, "utf-8");
const renderedHTML = template
.replace("<!--${MOVIE_ITEMS_PLACEHOLDER}-->", moviesHTML);
// 2️⃣ html 생성
res.send(renderedHTML);
} catch (error) {
console.error(error.message);
res.status(500).send("Internal Server Error");
}
};
export default router;
여기서는 ssr 서버에서 어떻게 렌더링하는지 알 수 있다.
<!--${MOVIE_ITEMS_PLACEHOLDER}-->
부분을 위에서 생성한 html으로 바꿔준다. 🙎 가정 : 초기 페이지에 접속하면 영화 목록을 확인하고, 영화를 클릭하면 영화의 상세 정보를 모달로 확인하고 싶어요.
이때, 초기 렌더링 속도를 줄이기 위해 SSR 를 활용하나, 그 이후부터 CSR로 동작했으면 좋겠어요.
📦src
┣ 📂apis
┃ ┗ 📜fetchMovies.js
┣ 📂client
┃ ┣ 📂components
┃ ┣ 📂pages
┃ ┣ 📜App.jsx
┃ ┗ 📜main.js
┣ 📂server
┃ ┣ 📂routes
┃ ┃ ┗ 📜index.js
┗ ┗ 📜main.js
client 와 server 폴더를 분리해주었다.
client 에서는 리액트에서 한 것 처럼 구현해주면 된다.
이 부분은 첫 렌더링 후 동적인 웹사이트를 구성하기 위해 사용된다.
const initialData = window.__INITIAL_DATA__;
hydrateRoot(
document.getElementById("root"),
<BrowserRouter>
<App movies={initialData.movies} movieDetail={initialData.movieDetail} />
</BrowserRouter>
);
기존에 사용하던 리액트와 다르게 initialData
가 있고, createRoot 대신 hydrateRoot
가 있는 것을 확인할 수 있다.
function App({ movies, movieDetail }) {
return (
<Routes>
<Route path="/" element={<Home movies={movies} />} />
<Route
path="/detail/:id"
element={<MovieDetail movies={movies} movieDetail={movieDetail} />}
/>
</Routes>
);
}
app 에서 다음과 같이 라우팅해주었다.
import { StaticRouter } from "react-router-dom/server";
const router = Router();
function getInitialDataScript(movies, movieDetail) {
return `
<script>
window.__INITIAL_DATA__ = {
movies: ${JSON.stringify(movies)},
movieDetail: ${JSON.stringify(movieDetail)}
};
</script>
`;
}
async function renderApp(location, res, movies, movieDetail) {
try {
const renderedApp = renderToString(
<StaticRouter location={location}>
//1️⃣ StaticRouter 를 활용하면 서버에서도 클라이언트의 라우팅 구조를 그대로 사용할 수 있다
<App movies={movies} movieDetail={movieDetail} />
</StaticRouter>
);
//...생략
}
router.get("/", async (req, res) => {
const movies = (await fetchNowPlayingMovieItems()) || [];
await renderApp("/", res, movies, null);
});
// 2️⃣ 영화 목록을 fetch 후 라우팅에 알맞게 html을 보내준다.
router.get("/detail/:id", async (req, res) => {
//...생략
});
// 3️⃣ 영화 상세 목록도 마찬가지로
export default router;
서버 코드이다. client 에서 사용한 코드를 정적인 html 코드로 바꾼 후
각 라우트 마다 넣어주었다. StaticRouter
를 활용하면 경로에 따른 App rendering 이 가능하다.
이 코드는 어떻게 동작할까?
첫 접속 시 서버에서 렌더링된 html 파일이 전달되고(SSR), 그 이후 인터렉션에서는 CSR 이 작동할것이다.
첫 접속 시 네트워크창
클릭 시 네트워크창
첫 접속하면 서버에서 HTML을 전달하고, 영화의 디테일을 보려고 클릭 시 click 이벤트와 navigate가 실행되어 URL이 변경되고 영화의 디테일을 보여줄 것이다. 그리고, 그 상태에서 새로고침 시 첫 접속이기 때문에 디테일 페이지가 서버에서 생성되어 완성된 HTML을 전달할 것이다.
정리하면 다음과 같다.
<script defer src="/static/bundle.js"></script>
포함코드로 SSR 을 알아보고, CSR 과 SSR 을 함께 사용하는 방법, 그리고 이 둘의 렌더링 방식이 어떻게 다른지 알아보았다.
마지막으로 이 두 방식을 비교해보자.
단, react 에서 CSR,SSR을 할 경우에 대한 예시이다.
사용자의 인터렉션이 많은 웹
예: SNS, 웹 메일, 관리자 대시보드
CSR은 클라이언트에서 자바스크립트를 실행하기 때문에 즉각적인 UI 업데이트
가능하다
서버 비용을 줄여야 할 경우
정적 파일 제공만 하면 되므로 서버 부하가 감소
한다.
추가로, 정적 파일만으로 구성되어 있어 CDN 배포가 용이하다.
예: 뉴스, 쇼핑몰 상품 페이지
💭 (+) 왜 CSR에서 fetch 하는 것 보다 SSR에서 fetch 하는게 비교적 빠른 경우가 많을까?
- 물리적 거리
서버 ↔ API 서버: 보통 같은 데이터센터 내에 위치
브라우저 ↔ API 서버: 사용자의 위치에 따라 물리적 거리가 멀 수 있다.- 네트워크 환경
서버: 안정적인 고속 네트워크 환경
브라우저: 사용자의 네트워크 상태에 따라 불안정할 수 있다 (3G, 4G, WiFi 등)
예: 콘텐츠 중심 웹사이트, 마케팅 페이지
요즘은 SSR과 CSR을 함께 사용하는 하이브리드 방식이 많이 활용되고 있다.
Next.js와 같은 프레임워크들은 이러한 렌더링 방식들을 쉽게 구현할 수 있도록 도와주고 있다.
렌더링 방식에는 SSR, CSR만 있는 것이 아니라 SSG(Static Site Generation), ISR(Incremental Static Regeneration) 등 다양한 렌더링 전략들이 있다.
각 페이지나 컴포넌트의 특성을 잘 파악하고, 거기에 맞는 최적의 렌더링 방식을 선택해서 조합하자~~
레퍼런스