SSR와 CSR은 동일한 React 컴포넌트를 어디서 렌더링하느냐(서버 vs 브라우저)에 따라 사용자 경험, SEO, 성능 프로파일이 달라집니다. 프로젝트 특성과 트래픽 패턴에 따라 전략을 섞는 것이 실무에서 일반적입니다.
1. 개념 요약
CSR(Client-Side Rendering)은 서버가 최소한의 HTML과 번들 JS를 보내고, 브라우저가 최초 렌더와 데이터 요청, 이벤트 바인딩까지 모두 수행합니다. 초기 TTFB는 빠를 수 있으나, JS 다운로드/파싱/데이터 로딩 전까지 빈 화면이 보일 수 있습니다.
SSR(Server-Side Rendering)은 서버가 요청 시 HTML을 만들어 보내고, 브라우저는 즉시 마크업을 표시한 후 JS가 로드되면 하이드레이션으로 상호작용을 활성화합니다. 초기 인지 성능과 SEO에 유리합니다. 서버 부하와 캐시 전략이 중요합니다.
2. 사용자 경험과 성능 차이
- 초기 표시: SSR이 빠르게 첫 화면을 보여줍니다. CSR은 JS/데이터 로딩 전까지 비어 있거나 스켈레톤만 보일 수 있습니다.
- 상호작용 가능 시점: CSR은 번들 로딩 후 즉시, SSR은 하이드레이션 완료 후입니다. 컴포넌트가 무거울수록 SSR의 하이드레이션 비용이 커집니다.
- SEO: 검색엔진 크롤러는 SSR/SSG에 유리합니다. CSR만으로도 구글은 대부분 렌더링하지만, 렌더 대기시간이 길면 인덱싱이 지연될 수 있습니다.
- 서버/인프라: SSR은 요청마다 렌더링 비용이 발생합니다. 캐싱, 스트리밍, 엣지 런타임 도입으로 완화합니다. CSR은 CDN 캐시와 정적 호스팅이 단순합니다.
3. 최소 예시: CSR 구현
// index.jsx (CSR)
import React, { useEffect, useState } from 'react'
import { createRoot } from 'react-dom/client'
function App() {
const [posts, setPosts] = useState(null)
useEffect(() => {
fetch('/api/posts').then(r => r.json()).then(setPosts)
}, [])
if (!posts) return <div>로딩중...</div>
return (
<ul>
{posts.map(p => <li key={p.id}>{p.title}</li>)}
</ul>
)
}
createRoot(document.getElementById('root')).render(<App />)특징: 서버는 정적 파일만 제공하고, 최초 데이터는 클라이언트에서 가져옵니다. CDN 캐싱이 쉽고 서버 부하가 낮습니다.
4. 최소 예시: Express + React SSR
// server.mjs (SSR)
import express from 'express'
import React from 'react'
import { renderToPipeableStream } from 'react-dom/server'
import App from './App.js'
const app = express()
app.use('/static', express.static('dist'))
app.get('/posts', async (req, res) => {
const data = await fetch('https://example.com/api/posts').then(r => r.json())
const { pipe } = renderToPipeableStream(
<html lang="ko">
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="preload" href="/static/client.js" as="script" />
<title>게시글</title>
</head>
<body>
<div id="root"><App initialData={data} /></div>
<script>window.__INITIAL_DATA__ = ${JSON.stringify(data)};</script>
<script src="/static/client.js" defer></script>
</body>
</html>,
{
onShellReady() { res.status(200); pipe(res) },
onError(err) { console.error(err) }
}
)
})
app.listen(3000)// App.jsx (공용)
import React, { useEffect, useState } from 'react'
export default function App({ initialData }) {
const [posts, setPosts] = useState(initialData || null)
useEffect(() => {
if (!posts) { fetch('/api/posts').then(r => r.json()).then(setPosts) }
}, [posts])
if (!posts) return <div>로딩중...</div>
return <ul>{posts.map(p => <li key={p.id}>{p.title}</li>)}</ul>
}특징: 서버가 HTML을 즉시 전송하고, 클라이언트는 같은 트리로 하이드레이션합니다. window.__INITIAL_DATA__로 이중 요청을 회피합니다.
5. Next.js 관점(현행 App Router)
Next.js는 기본적으로 서버 컴포넌트 기반 SSR과 스트리밍을 제공합니다. 데이터는 서버에서 fetch하여 캐시/ISR를 적용하고, 클라이언트 컴포넌트는 상호작용을 담당합니다.
// app/page.jsx (서버 컴포넌트)
export default async function Page() {
const res = await fetch('https://example.com/api/posts', { next: { revalidate: 60 } })
const posts = await res.json()
return (
<ul>{posts.map(p => <li key={p.id}>{p.title}</li>)}</ul>
)
}장점: 자동 스트리밍, 캐시 제어(next.revalidate), SEO 최적화, 엣지 배포 용이성입니다. 클라이언트 컴포넌트는 'use client'로 분리합니다.
6. 하이드레이션 문제와 실무 팁
- 불일치 방지: 서버와 클라이언트 렌더 결과가 달라지지 않게 브라우저 전용 API(Date, window, 랜덤 등)는 useEffect에서만 사용합니다.
- 키 안정성: 리스트 key가 서버/클라이언트에서 동일해야 합니다. 임의 랜덤 키를 피합니다.
- 시간/로케일: 날짜 포맷이 타임존에 의존하면 mismatch가 발생합니다. 서버에서 포맷하거나 통일된 포맷을 사용합니다.
- 환경 분기: typeof window 체크로 분기하고, 조건부 렌더링으로 서버에서 제외할 UI를 명확히 합니다.
7. 캐싱과 배포 전략
- SSR HTML 캐시: 트래픽이 큰 페이지는 CDN에서 HTML을 단기 캐시(stale-while-revalidate)하고 사용자별 콘텐츠는 부분만 CSR로 전환합니다.
- 데이터 캐시: API 응답에 Cache-Control을 설정하고, 서버에서 재검증 헤더(ETag)를 사용합니다.
- 스트리밍: renderToPipeableStream 또는 Next.js 스트리밍으로 TTFB를 낮추면서도 큰 트리를 점진적으로 전송합니다.
- 엣지: 퍼스널라이제이션이 적고 지연 시간이 중요한 페이지는 엣지 런타임(Next.js Edge, Cloudflare Workers)에서 SSR합니다.
8. 선택 가이드
- 즉시 가시성/SEO가 핵심(상품 상세, 블로그): SSR/SSG 우선, 상호작용 영역만 CSR로 분리합니다.
- 대시보드/내부도구: CSR 우선, 필요 영역만 부분 SSR 또는 서버 컴포넌트로 데이터 프리패치합니다.
- 빈번히 변하는 공개 리스트: SSR + 짧은 캐시/ISR 조합으로 TTFB와 신선도를 균형 있게 유지합니다.
9. 측정 지표와 디버깅
- 지표: TTFB(SSR 민감), LCP, INP, CLS, Hydration 시간. 페이지별로 분리 측정합니다.
- 도구: Chrome DevTools Performance, Lighthouse, WebPageTest, Next.js 분석(next build --profile), React Profiler를 사용합니다.
- 로그: 서버 SSR 렌더 시간, 캐시 히트율, 하이드레이션 경고를 수집합니다.
10. 마이그레이션 체크리스트
- 데이터 패칭 위치 이동: useEffect에서 서버 패칭(Next 서버 컴포넌트 또는 getServerSideProps/route handlers)으로 이동합니다.
- 링크/라우팅: SSR 라우팅은 동적 세그먼트와 캐시 정책을 명확히 설계합니다.
- 에셋 최적화: critical CSS, preconnect, preload로 초기 경로를 최적화합니다.
- 점진 도입: 상단 폴드 영역만 SSR로 전환하고 나머지는 CSR 유지하는 하이브리드 접근을 권장합니다.
정리
SSR은 초기 표시와 SEO에, CSR은 단순 배포와 상호작용 속도에 강점이 있습니다. React 생태계(특히 Next.js)는 스트리밍, 서버 컴포넌트, 캐시를 통해 두 세계의 장점을 결합하도록 발전했습니다. 실제 프로젝트에서는 페이지 성격과 트래픽, 팀 운영 비용을 기준으로 하이브리드 전략을 설계하는 것이 최적입니다.
'React' 카테고리의 다른 글
| React 컴포넌트를 Storybook에서 문서화하고 테스트하기 (1) | 2026.05.08 |
|---|---|
| React 앱에서 데이터 페칭 시 Skeleton UI 구현하기 (0) | 2026.05.07 |
| React 앱에 OAuth 2.0 로그인 기능 추가하기 (Authorization Code + PKCE) (0) | 2026.05.06 |
| React에서 WebRTC를 활용한 실시간 영상 통화 구현하기 (0) | 2026.05.06 |
| React에서 Intersection Observer API로 요소 감시하기 (0) | 2026.05.04 |