본문 바로가기

Web

[Web] CSR (Client-Side Rendering)

클라이언트 사이드 렌더링 (CSR)

1. 개요

이 문서는 CSR(Client-Side Rendering) 렌더링 방식을 학습한 내용을 바탕으로,
해당 개념과 내용을 정리하고 복습하기 위한 목적으로 작성되었다.

2. SSR 방식의 한계

전통적인 SSR(Server-Side Rendering) 은 서버에서 완성된 HTML 을 생성하여 브라우저로 전송하는 방식이었기 때문에 빠른 초기 로딩SEO에 유리했지만, 웹 애플리케이션이 복잡해지고 사용자 경험에 대한 요구가 높아지면서 다음과 같은 한계점들이 드러났다.

SSR 의 한계

  • 모든 사용자 행동마다 전체 페이지 새로고침: 링크 클릭, 폼 제출 등 페이지 전환이 있을 때마다 서버에서 새로운 HTML 을 만들어야 함
  • 서버 부하 증가: 매 요청마다 서버가 HTML 생성을 담당해 트래픽이 많아질수록 부담이 커짐
  • 복잡한 클라이언트 인터랙션 구현 어려움: 동적인 UI나 풍부한 사용자 경험을 제공하기에 한계가 있었음
  • 상태 관리가 서버 중심: 상태 유지를 위해 세션, 쿠키 등 서버 기술에 의존해야 했음

이처럼 웹 애플리케이션이 복잡해지고, 더 부드럽고 동적인 인터페이스가 요구되면서
클라이언트에서 화면을 그리는 CSR(Client Side Rendering) 방식이 필요해졌다.


관련 포스팅: SSR (Server-Side Rendering)

3. CSR이란 무엇인가?

CSR (Client-Side Rendering)

브라우저에서 JavaScript 를 실행해 동적으로 화면을 그리는 렌더링 방식
서버는 최소한의 HTML 과 JavaScript 파일만 제공하고, 브라우저가 JavaScript 를 실행하여 실제 콘텐츠를 렌더링

CSR에서는 서버와 클라이언트의 역할이 SSR과 다르다.

  • 서버: 정적 파일(HTML, CSS, JavaScript)만 제공
  • 클라이언트: JavaScript 를 실행하여 DOM 을 조립하고 콘텐츠를 렌더링

이 방식은 SPA(Single Page Application) 의 핵심 기술로,
현재 React, Vue, Angular 등 현대적인 프론트엔드 라이브러리 및 프레임워크에서 널리 쓰이고 있다.

4. CSR의 동작 방식

이 문서에서는 데이터베이스에서 가져온 상품 목록(products)을 예시로, CSR이 어떻게 단계적으로 이뤄지는지에 대해 서술한다.


프로젝트 구조:

project/
├── public/
│   ├── index.html
│   └── main.js
└── server.js

[1단계] Server → Browser: 빈 HTML 과 JavaScript 파일 제공

서버는 정적 파일(빈 HTML 과 JavaScript 파일)을 브라우저에 제공한다.
HTML 은 최소한의 구조만 포함하며, 실제 콘텐츠는 포함되지 않는다.


서버가 제공하는 HTML (index.html):

<!DOCTYPE html>
<html>
  <head>
    <title>상품 목록</title>
  </head>
  <body>
    <div id="root"></div>
    <script src="/main.js"></script>
  </body>
</html>

<div id="root"></div> 는 이후 실행되는 JavaScript 코드가 동적으로 생성한 DOM 을 삽입하는 기준 컨테이너 역할을 한다.


서버 로직 (server.js):

const express = require('express');
const app = express();
const path = require('path');

// 정적 파일 서빙
app.use(express.static(path.join(__dirname, 'public')));

// API 엔드포인트
app.get('/api/products', (req, res) => {
  const products = getProductsFromDatabase();
  // products = [
  //   { id: 1, name: "노트북", price: 1000000 },
  //   { id: 2, name: "마우스", price: 30000 }
  // ]
  res.json(products);
});

// 모든 라우트에 대해 index.html 반환
app.get('*', (req, res) => {
  res.sendFile(path.join(__dirname, 'public', 'index.html'));
});

// 서버 시작
app.listen(3000);

서버는 모든 요청에 대해 동일한 index.html 파일을 반환한다. 실제 라우팅은 클라이언트에서 처리된다.

[2단계] Browser: JavaScript 파일 다운로드 및 실행

브라우저가 HTML에 포함된 <script> 태그를 통해 JavaScript 파일을 다운로드하고 실행을 시작한다.


JavaScript 코드 (public/main.js):

아래는 다운로드된 JavaScript 파일의 내용이다. 이 코드가 실행되면서 이후 3단계와 4단계의 동작이 순차적으로 수행된다.
// 순수 JavaScript로 DOM 조립
document.addEventListener('DOMContentLoaded', function() {
  const root = document.getElementById('root');

  // API를 통해 데이터 가져오기
  fetch('/api/products')
    .then(res => res.json())
    .then(products => {
      // DOM을 직접 조립하여 콘텐츠 렌더링
      const container = document.createElement('div');

      const title = document.createElement('h1');
      title.textContent = '상품 목록';
      container.appendChild(title);

      const ul = document.createElement('ul');

      products.forEach(product => {
        const li = document.createElement('li');

        const link = document.createElement('a');
        link.href = `/products/${product.id}`;
        link.textContent = product.name;
        li.appendChild(link);

        const span = document.createElement('span');
        span.textContent = `${product.price}원`;
        li.appendChild(span);

        ul.appendChild(li);
      });

      container.appendChild(ul);
      root.appendChild(container);
    });
});

이 예시는 순수 JavaScript 로 DOM 을 직접 조립하는 방식이다.
createElement, appendChild 같은 메서드를 사용하여 HTML 요소를 생성하고 조립한다.

[3단계] Browser → Server: API 호출하여 데이터 가져오기

2단계에서 실행된 JavaScript 코드가 API를 호출하여 실제 데이터를 가져온다.

서버는 /api/products 엔드포인트 요청에 대해 HTML 이 아닌 JSON 데이터만 반환한다.

[4단계] Browser: DOM 조작하여 콘텐츠 렌더링

3단계에서 받은 데이터를 사용하여 JavaScript 가 DOM 을 조작하고 실제 콘텐츠를 화면에 표시한다.


렌더링 결과 (브라우저의 DOM):

<!DOCTYPE html>
<html>
  <body>
    <div id="root">
      <div>
        <h1>상품 목록</h1>
        <ul>
          <li>
            <a href="/products/1">노트북</a>
            <span>1000000원</span>
          </li>
          <li>
            <a href="/products/2">마우스</a>
            <span>30000원</span>
          </li>
        </ul>
      </div>
    </div>
  </body>
</html>

정리:

CSR 은 서버가 빈 HTML 과 JavaScript 만 제공하고,
브라우저가 JavaScript 를 실행하여 API 를 호출하고 DOM 을 조립해 실제 콘텐츠를 렌더링하는 방식이다.

sequenceDiagram
    participant User
    participant Browser
    participant Server

    User->>Browser: 페이지 요청 (/products)
    Browser->>Server: HTML 요청
    Server-->>Browser: HTML + JavaScript 파일
    Browser->>Browser: 초기 화면 렌더링 (콘텐츠 없음)
    Browser->>Browser: JavaScript 실행
    Browser->>Server: API 요청 (/api/products)
    Server-->>Browser: JSON 응답
    Browser->>Browser: DOM 생성 및 렌더링
    Browser->>User: 콘텐츠 표시

  1. 사용자 요청: 브라우저가 서버에 페이지 요청
  2. 빈 HTML 제공: 서버가 빈 HTML 구조와 JavaScript 파일만 제공
  3. JavaScript 실행: 브라우저가 JavaScript 파일을 다운로드하고 실행
  4. API 호출: JavaScript 가 서버에 데이터 요청
  5. 데이터 조회: 서버가 데이터베이스에서 데이터 조회
  6. JSON 응답: 서버가 JSON 형식으로 데이터 반환
  7. DOM 조립: JavaScript 가 받은 데이터로 DOM 을 조립하여 콘텐츠 렌더링
  8. 화면 표시: 브라우저가 렌더링된 콘텐츠를 화면에 표시

5. SPA (Single Page Application)

CSR 방식은 단순히 화면을 그리는 방식의 변화에 그치지 않고, 애플리케이션 구조 자체를 변화시켰다고 할 수 있다. 그 대표적인 결과가 SPA(Single Page Application)다.

5.1. SPA란?

SPA (Single Page Application)

단일 HTML 페이지로 구성된 웹 애플리케이션으로, 페이지 전환 시 전체 페이지를 새로고침하지 않고 필요한 부분만 업데이트하는 방식

SSR vs SPA

SSR:

sequenceDiagram
    participant User as User
    participant Browser as Browser
    participant Server as Server

    User->>Browser: 링크 클릭 (/products)
    Browser->>Server: GET /products 요청
    Server-->>Browser: 완성된 HTML 반환
    Browser->>Browser: 전체 페이지 새로고침
    Browser->>User: 새 페이지 표시

SPA (CSR):

sequenceDiagram
    participant User as User
    participant Browser as Browser
    participant Server

    User->>Browser: 링크 클릭 (/products)
    Browser->>Browser: JavaScript가 라우팅 처리
    Browser->>Server: GET /api/products 요청
    Server-->>Browser: JSON 데이터 반환
    Browser->>Browser: 필요한 부분만 DOM 업데이트
    Browser->>User: 부드러운 페이지 전환

SPA의 특징

  • 전체 페이지 새로고침 없음: 페이지 전환 시 필요한 부분만 업데이트
  • 부드러운 사용자 경험: 페이지 전환이 즉각적이고 자연스러움
  • 상태 유지: 페이지 전환 시에도 애플리케이션 상태가 유지됨
  • 오프라인 지원 가능: Service Worker와 함께 사용하면 오프라인에서도 동작 가능

5.2. SPA 라우팅 예시

SPA에서는 클라이언트 사이드 라우팅을 사용한다.
서버에 요청을 보내지 않고, JavaScript 가 URL 을 변경하고 필요한 부분만 업데이트한다.


Vanilla JS로 구현한 간단한 라우터:

function home() {
  document.getElementById('root').innerHTML = '<h1>홈</h1>';
}

function products() {
  fetch('/api/products')
    .then(res => res.json())
    .then(products => {
      const html = products.map(product =>
        `<li>${product.name} - ${product.price}원</li>`
      ).join('');
      document.getElementById('root').innerHTML = `
        <h1>상품 목록</h1>
        <ul>${html}</ul>
      `;
    });
}

function about() {
  document.getElementById('root').innerHTML = '<h1>소개</h1>';
}

// 라우트 정의
const routes = {
  '/': home,
  '/products': products,
  '/about': about
};

// 라우팅 로직
function navigate(path) {
  window.history.pushState({}, '', path);
  routes[path]();
}

// 링크 클릭 시 라우팅 처리
document.addEventListener('click', function(e) {
  // SPA에서 서버로 이동하지 않고 JS로 라우팅을 제어하기 위해 사용
  // data-route 속성이 있는 a 태그를 클릭하면 다음 코드가 실행됨
  if (e.target.tagName === 'A' && e.target.getAttribute('data-route')) {
    // <a>의 기본 동작(페이지 이동) 방지
    e.preventDefault(); 

    // data-route에 지정된 경로 가져오기
    const path = e.target.getAttribute('data-route'); 

    // 클라이언트 라우팅 처리
    navigate(path); 
  }
});

// 브라우저 뒤로가기/앞으로가기 처리
window.addEventListener('popstate', function() {
  routes[window.location.pathname]();
});

HTML:

<nav>
  <a href="#" data-route="/">홈</a>
  <a href="#" data-route="/products">상품 목록</a>
  <a href="#" data-route="/about">소개</a>
</nav>
<div id="root"></div>

이처럼 Vanilla JavaScript 로도 SPA 라우팅을 구현할 수 있지만, 코드가 복잡해지고 유지보수가 어려워질 수 있다.
따라서 실제 프로젝트에서는 React Router, Vue Router 와 같은 라우팅 라이브러리를 사용하는 경우가 많다.

6. CSR의 장점

SSR 의 한계를 극복하기 위해 CSR 이 태동하기 시작했는데, CSR 은 다음과 같은 장점을 제공한다.

6.1. 부드러운 사용자 경험

  • 전체 페이지 새로고침 없음: 페이지 전환 시 필요한 부분만 업데이트되어 부드러운 전환
  • 빠른 인터랙션: 클라이언트에서 즉시 반응하는 UI 구현 가능
  • 상태 유지: 페이지 전환 시에도 애플리케이션 상태가 유지됨

6.2. 서버 부하 감소

  • 정적 파일 제공: 서버는 HTML, CSS, JavaScript 같은 정적 파일만 제공
  • API 서버 분리: 데이터 제공은 별도의 API 서버가 담당하여 확장성 향상
  • CDN 활용: 정적 파일을 CDN에 배포하여 전 세계 어디서나 빠르게 제공 가능

6.3. 풍부한 인터랙션

  • 복잡한 UI 구현: React, Vue 같은 프레임워크로 복잡한 컴포넌트 구조 구현 가능
  • 실시간 업데이트: WebSocket 등을 활용한 실시간 데이터 업데이트

7. Vanilla JS의 한계와 프레임워크의 필요성

앞선 예제에서 보았듯이 CSR 자체는 순수 JavaScript 만으로도 구현할 수 있다.
하지만 화면 구성 요소가 많아지고 상태 변화가 빈번해질수록, DOM 조립과 업데이트 로직은 급격히 복잡해진다.


이러한 복잡성은 단순한 예제에서는 드러나지 않지만, 애플리케이션 규모가 커질수록 구조적인 한계로 이어진다.

7.1. Vanilla JS의 한계

순수 JavaScript로 CSR을 구현할 때의 핵심 문제는 명령형 DOM 조작 이다.
개발자가 직접 DOM을 생성하고 조립해야 하기 때문에 코드가 복잡하고 유지보수가 어렵다.


Vanilla JS의 문제점:

  • 복잡한 DOM 조립: createElement, appendChild 같은 메서드를 반복적으로 사용해야 함
  • 수동 업데이트: 데이터가 변경될 때마다 수동으로 렌더링 함수를 호출해야 함
  • 상태 관리 어려움: 데이터가 변경되면 어떤 DOM을 업데이트해야 할지 직접 추적해야 함
  • 코드 재사용성 부족: 비슷한 UI를 만들 때마다 비슷한 코드를 반복 작성
  • 유지보수 어려움: 코드가 길어질수록 구조 파악이 어려워짐

이러한 문제를 해결하기 위해 여러 가지 라이브러리와 프레임워크가 등장했다.
이 도구들은 수동 DOM 조작의 번거로움을 줄이고, 선언적인 방식으로 UI를 개발할 수 있게 도와준다.

7.2. 라이브러리 및 프레임워크의 필요성

라이브러리 및 프레임워크는 수동 DOM 조립 문제를 해결하기 위해 선언적 UI자동 업데이트 를 제공한다.


Vanilla JS (명령형, 수동 DOM 조립):

// 상품 목록을 렌더링하는 함수
function renderProducts(products) {
  const root = document.getElementById('root');
  root.innerHTML = '';

  const container = document.createElement('div');

  const title = document.createElement('h1');
  title.textContent = '상품 목록';
  container.appendChild(title);

  const ul = document.createElement('ul');

  products.forEach(product => {
    const li = document.createElement('li');

    const link = document.createElement('a');
    link.href = `/products/${product.id}`;
    link.textContent = product.name;
    li.appendChild(link);

    const span = document.createElement('span');
    span.textContent = `${product.price}원`;
    li.appendChild(span);

    ul.appendChild(li);
  });

  container.appendChild(ul);
  root.appendChild(container);
}

// 데이터가 변경될 때마다 수동으로 호출해야 함
fetch('/api/products')
  .then(res => res.json())
  .then(products => renderProducts(products));

React (선언적, 자동 업데이트):

import { useState, useEffect } from 'react';

function Products() {
  const [products, setProducts] = useState([]);

  useEffect(() => {
    fetch('/api/products')
      .then(res => res.json())
      .then(data => setProducts(data));
  }, []);

  return (
    <div>
      <h1>상품 목록</h1>
      <ul>
        {products.map(product => (
          <li key={product.id}>
            <a href={`/products/${product.id}`}>{product.name}</a>
            <span>{product.price}원</span>
          </li>
        ))}
      </ul>
    </div>
  );
}

프레임워크의 핵심 개선점:

  • 선언적 UI: 원하는 UI 상태를 선언하면 프레임워크가 자동으로 DOM 을 생성
  • 자동 업데이트: 데이터가 변경되면 자동으로 DOM 을 업데이트
  • 가상 DOM: 효율적인 DOM 업데이트를 위한 가상 DOM 사용
  • 컴포넌트 생명주기: 컴포넌트의 생성, 업데이트, 제거를 자동으로 관리

7.3. 라이브러리/프레임워크

현재 React, Vue, Angular 같은 현대적인 프론트엔드 라이브러리와 프레임워크가 널리 사용되고 있다.
각각의 문법은 자세히 다루지 않고, 간단하게 동작 방식과 특징만 살펴보려고 한다.

7.3.1. React

React는 Facebook 에서 개발한 UI 라이브러리로, 컴포넌트 기반 아키텍처를 도입했다.


React의 특징:

  • 컴포넌트 기반: 재사용 가능한 컴포넌트로 UI 구성
  • 가상 DOM: 효율적인 DOM 업데이트를 위한 가상 DOM 사용
  • 단방향 데이터 흐름: 데이터가 한 방향으로만 흐르는 예측 가능한 구조
  • 선언적 UI: 원하는 UI 상태를 선언하면 React가 자동으로 DOM을 업데이트

React 예시:

import { useState, useEffect } from 'react';

function Products() {
  const [products, setProducts] = useState([]);

  useEffect(() => {
    fetch('/api/products')
      .then(res => res.json())
      .then(data => setProducts(data));
  }, []);

  return (
    <div>
      <h1>상품 목록</h1>
      <ul>
        {products.map(product => (
          <li key={product.id}>
            {product.name} - {product.price}원
          </li>
        ))}
      </ul>
    </div>
  );
}

React Router 예시

프레임워크를 사용하면 라우팅도 훨씬 간단해진다.


React Router:

import { BrowserRouter, Routes, Route, Link } from 'react-router-dom';

function App() {
  return (
    <BrowserRouter>
      <nav>
        <Link to="/">홈</Link>
        <Link to="/products">상품 목록</Link>
        <Link to="/about">소개</Link>
      </nav>

      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/products" element={<Products />} />
        <Route path="/about" element={<About />} />
      </Routes>
    </BrowserRouter>
  );
}

Vanilla JS로 구현한 라우터와 비교하면 훨씬 간단하고 직관적이다.

7.3.2. Vue

Vue는 학습이 쉽고 기존 프로젝트에도 부분적으로 적용할 수 있는, 점진적 프레임워크다.


Vue의 특징:

  • 컴포넌트 기반: 재사용 가능한 컴포넌트로 UI 구성
  • 반응형 시스템: 데이터 변경 시 자동으로 DOM 업데이트
  • 템플릿 문법: HTML과 유사한 템플릿 문법으로 직관적인 개발
  • 점진적 도입: 기존 프로젝트에 점진적으로 도입 가능

Vue 예시:

<template>
  <div>
    <h1>상품 목록</h1>
    <ul>
      <li v-for="product in products" :key="product.id">
        {{ product.name }} - {{ product.price }}원
      </li>
    </ul>
  </div>
</template>

<script>
export default {
  data() {
    return {
      products: []
    };
  },
  mounted() {
    fetch('/api/products')
      .then(res => res.json())
      .then(data => this.products = data);
  }
};
</script>

Vue Router 예시

Vue Router:

<template>
  <div>
    <nav>
      <router-link to="/">홈</router-link>
      <router-link to="/products">상품 목록</router-link>
      <router-link to="/about">소개</router-link>
    </nav>

    <router-view />
  </div>
</template>

<script>
import { createRouter, createWebHistory } from 'vue-router';
import Home from './components/Home.vue';
import Products from './components/Products.vue';
import About from './components/About.vue';

const routes = [
  { path: '/', component: Home },
  { path: '/products', component: Products },
  { path: '/about', component: About }
];

const router = createRouter({
  history: createWebHistory(),
  routes
});

export default router;
</script>

프레임워크는 이러한 문제들을 효과적으로 해결해 주었지만, 모든 문제를 공짜로 해결해 주는 것은 아니었다.
개발 편의성을 얻는 대신, 새로운 비용과 고려사항도 함께 등장했다.

7.7. 프레임워크 사용의 트레이드 오프

프레임워크는 개발 생산성과 유지보수성을 크게 향상시켰지만, 동시에 새로운 비용을 발생시켰다.
이는 CSR의 본질적 한계라기보다, 현대적인 CSR 구현 방식에서 발생하는 트레이드오프이다.


다음으로는 특정 라이브러리나 프레임워크의 문제가 아닌, CSR이라는 렌더링 방식 자체가 가지는 한계점들을 살펴본다.

8. CSR의 한계

CSR은 많은 장점을 제공하지만, 다음과 같은 한계점들도 가지고 있다.

8.1. 느린 초기 로딩

  • JavaScript 번들 다운로드: 초기 로딩 시 큰 JavaScript 번들을 다운로드해야 함
  • JavaScript 실행 시간: 번들을 다운로드한 후 실행하는데 시간이 걸림
  • 빈 화면 문제: JavaScript 가 실행되기 전까지는 빈 화면이 표시됨

초기 로딩 과정:

sequenceDiagram
    participant User as User
    participant Browser as Browser
    participant Server

    User->>Browser: 페이지 요청
    Browser->>Server: HTML 요청
    Server-->>Browser: 빈 HTML 반환
    Browser->>Browser: 빈 화면 표시
    Browser->>Server: JavaScript 번들 요청
    Server-->>Browser: JavaScript 번들 (수백 KB ~ 수 MB)
    Browser->>Browser: JavaScript 파싱 및 실행
    Browser->>Server: API 호출
    Server-->>Browser: JSON 응답
    Browser->>Browser: DOM 렌더링
    Browser->>User: 콘텐츠 표시

8.2. SEO 문제

  • 검색 엔진 크롤링: 검색 엔진이 JavaScript 를 실행하지 못해 콘텐츠를 읽지 못함
  • 메타 태그 동적 생성: JavaScript 로 생성된 메타 태그를 검색 엔진이 인식하지 못함
  • 소셜 미디어 공유: Facebook, Twitter 같은 소셜 미디어가 콘텐츠를 제대로 표시하지 못함

서버가 제공하는 HTML:

<div id="root"></div>

실제 콘텐츠는 JavaScript 가 실행된 후에야 나타나므로,
검색 엔진이나 소셜 미디어 봇은 페이지에 콘텐츠가 없는 빈 div 만 확인하게 된다.



이로 인해 CSR 만 사용하는 경우 SEO 와 미리보기, 공유 측면에서 불리하다.

8.3. 접근성 문제

  • JavaScript 비활성화: JavaScript 가 비활성화된 환경에서는 동작하지 않음
  • 느린 네트워크: 네트워크가 느린 환경에서 초기 로딩이 매우 느림
  • 낮은 성능 기기: 성능이 낮은 기기에서 JavaScript 실행이 느림

9. 하이브리드 방식 (SSR + CSR)

CSR의 한계점들, 특히 SEO 문제와 느린 초기 로딩을 해결하기 위해 SSR과 CSR을 결합한 하이브리드 방식 이 등장했다.

9.1. SSR + CSR 하이브리드

Hydration

서버에서 렌더링된 HTML 에 JavaScript 를 "주입"하여 인터랙티브하게 만드는 과정

하이브리드 방식의 특징:

  • 초기 로딩: 서버에서 완성된 HTML 제공 (SSR의 장점)
  • 인터랙션: 클라이언트에서 JavaScript로 처리 (CSR의 장점)
  • Hydration: 서버에서 렌더링된 HTML 에 JavaScript 를 "주입"하여 인터랙티브하게 만듦

이 방식은 Next.js, Nuxt.js 같은 메타 프레임워크에서 널리 사용된다.




관련 포스팅

728x90

'Web' 카테고리의 다른 글

[Web] SSR (Server-Side Rendering)  (0) 2025.12.25
[Web] 다국어 웹(i18n Web) 개발하기  (0) 2025.06.04
[Web] Responsive Web (반응형 웹) 개발하기  (0) 2025.04.18