지금까지 React로 화면을 만드는 방법을 배웠습니다. 그런데 React로 만든 화면은 어디서 실제로 그려질까요? 브라우저일 수도 있고, 서버일 수도 있습니다. 이 차이가 사용자가 첫 화면을 보는 속도에 직접 영향을 줍니다.
오늘은 렌더링 방식이 어떻게 진화해왔는지와, 퍼포먼스를 숫자로 측정하는 법을 봅니다. 7주차에 짧게 언급했던 Next.js가 왜 이 선택들을 제공하는지 이해하는 시간이기도 합니다.
React로 만든 앱을 아무 설정 없이 배포하면 CSR(Client-Side Rendering) 방식으로 동작합니다. 서버는 빈 HTML과 JS 파일만 보내고, 브라우저가 JS를 실행해서 직접 화면을 만듭니다.
서버 → 빈 HTML 전송 → JS 다운로드 → JS 실행 → DOM 생성 → 화면 표시
<!-- 서버가 보내는 HTML (실제로 이게 전부) -->
<html>
<body>
<div id="root"></div>
<script src="/bundle.js"></script>
</body>
</html>
사용자는 JS가 다운로드되고 실행되기 전까지 빈 화면을 봅니다. JS가 클수록, 인터넷이 느릴수록, 화면이 늦게 뜹니다.
SSR(Server-Side Rendering)은 서버가 HTML을 미리 완성해서 보내줍니다. 사용자는 JS가 로드되기 전에 이미 화면 내용을 볼 수 있습니다.
서버에서 HTML 완성 → 완성된 HTML 전송 → 화면 즉시 표시 → JS 다운로드 → Hydration
| 지표 | CSR | SSR |
|---|---|---|
| FCP (첫 콘텐츠가 보이는 시점) | 느림 (JS 실행 후) | 빠름 (HTML 도착 즉시) |
| SEO | 불리 (빈 HTML) | 유리 (완성된 HTML) |
| 서버 부하 | 없음 | 매 요청마다 생성 |
SSR이 "빠르다"는 건 정확히는 사용자가 화면을 처음 보는 시점이 빠르다는 뜻입니다. 클릭이나 입력 같은 인터랙션이 되려면 Hydration이 끝나야 합니다.
서버가 보낸 HTML은 그림처럼 보이기만 합니다. 클릭해도 반응이 없습니다. Hydration은 이 정적 HTML에 JS 이벤트 핸들러를 붙여서 실제로 동작하게 만드는 과정입니다.
정적 HTML (보이지만 클릭해도 반응 없음)
↓ JS 다운로드 + Hydration
동적 HTML (클릭, 입력 등 가능)
Hydration이 진행되는 동안 사용자는 화면은 보이지만 인터랙션이 안 되는 어색한 구간을 경험할 수 있습니다. 이 구간을 줄이는 것이 현대 렌더링 최적화의 핵심 중 하나입니다.
Next.js는 SSR만 하는 게 아닙니다. 페이지마다 다른 렌더링 전략을 선택할 수 있고, 그 전략들이 이렇게 진화해왔습니다.
CSR → SSR → SSG → ISR → Streaming SSR → PPR
SSG (Static Site Generation) — 빌드 시 미리 만들기
서버가 요청마다 HTML을 만드는 SSR과 달리, SSG는 빌드할 때 HTML을 미리 만들어둡니다. 요청이 오면 서버가 만들 필요 없이 미리 만든 파일을 바로 보냅니다.
[SSR] 요청 → 서버가 HTML 생성 → 전송 (매번)
[SSG] 빌드 → HTML 생성 → CDN에 올림 → 요청 → 바로 전송 (빠름)
ISR (Incremental Static Regeneration) — 필요할 때만 갱신
SSG의 단점을 해결합니다. 일정 시간마다 그 페이지만 조용히 재생성합니다. 1000페이지 중 요청이 온 1페이지만 갱신되는 식입니다.
// Next.js에서 ISR 설정
export const revalidate = 60; // 60초마다 재생성
0~60초: 캐시 그대로 반환 (SSG처럼 빠름)
60초 후: 요청이 오면 기존 캐시 반환 + 백그라운드에서 새 HTML 생성
→ 다음 요청부터 새 HTML 서빙
일반 SSR은 페이지 전체가 준비될 때까지 기다렸다가 한번에 보냅니다. DB 조회가 느린 경우 사용자는 그만큼 기다려야 합니다. Streaming SSR은 준비된 부분부터 먼저 보내고, 느린 부분은 나중에 채워줍니다.
<Layout>
<Header /> {/* 즉시 전송 */}
<Suspense fallback={<CommentSkeleton />}>
<Comments /> {/* DB 조회 끝나면 전송, 그 전엔 Skeleton 표시 */}
</Suspense>
</Layout>
Suspense는 "아직 준비 안 됐으면 대신 이걸 보여줘"를 선언하는
React 컴포넌트입니다. Skeleton은 콘텐츠가 들어올 자리를 미리 잡아두는 빈 뼈대
UI입니다.
PPR (Partial Prerendering) — 컴포넌트별로 정적/동적 분리
ISR은 페이지 전체를 정적 또는 동적으로 취급합니다. PPR은 한 페이지 안에서 컴포넌트 단위로 정적/동적을 나눕니다. 상품 이름·이미지는 정적으로, 가격·재고는 동적으로.
export default function ProductPage() {
return (
<div>
{/* 정적: 빌드 시 생성, CDN에서 즉시 전송 */}
<ProductName />
<ProductImage />
{/* 동적: 요청 시 서버에서 생성, 준비되면 Streaming */}
<Suspense fallback={<PriceSkeleton />}>
<Price />| 속도 | 데이터 신선함 | 어울리는 콘텐츠 | |
|---|---|---|---|
| SSG | 가장 빠름 | 빌드 시점 고정 | 블로그, 문서, 변경 없는 페이지 |
| SSR | 느림 | 항상 최신 | 로그인 필요, 실시간 데이터 |
| ISR | SSG와 동일 |
Next.js App Router에서는 컴포넌트가 어디서 실행되는지를 직접 선택할 수 있습니다. 이것이 RSC(React Server Components)입니다.
// Server Component (기본값)
// 서버에서만 실행됨. 브라우저로 JS가 안 감.
async function PostList() {
const posts = await db.query("SELECT * FROM posts");
return (
<ul>
{posts.map((p) => (
<li key={p.id}>{p.title}</li>
))}
</ul
판단 기준은 하나입니다. 이 컴포넌트가 클릭, 입력, 상태 변화를 다루는가? 그렇지 않다면 Server Component로 두는 게 낫습니다. JS 번들에 포함되지 않으니 번들 크기가 줄고, Hydration 대상도 줄어 성능이 좋아집니다.
| 구분 | Server Component | Client Component |
|---|---|---|
| 실행 위치 | 서버 | 브라우저 |
| useState, onClick | ❌ 불가 | ✅ 가능 |
| DB 직접 접근 | ✅ 가능 | ❌ 불가 |
| JS 번들 포함 | ❌ 포함 안 됨 |
Google이 정한 사용자 체감 성능의 3가지 핵심 지표입니다. 검색 순위(SEO)에도 실제로 영향을 줍니다.
가장 큰 콘텐츠가 화면에 나타나는 시간. 메인 이미지, 큰 텍스트 블록이 해당합니다.
페이지 요청
│ 0.5s 헤더, 네비게이션 표시
│ 1.2s 메인 이미지 표시 ← 이게 LCP
│ 1.8s 나머지 콘텐츠
좋음: ≤ 2.5s / 개선 필요: 2.5~4s / 나쁨: > 4s
사용자가 버튼 클릭
│ JS 이벤트 핸들러 실행
│ 상태 업데이트
│ DOM 변경 + 화면 다시 그림 ← 여기까지가 INP
좋음: ≤ 200ms / 나쁨: > 500ms
페이지 로딩 중 레이아웃이 얼마나 흔들리는가. 광고가 갑자기 끼어들어서 읽던 텍스트가 밀리는 경험이 대표적입니다.
좋음: ≤ 0.1 / 나쁨: > 0.25
width/height 명시, Skeleton으로 공간 미리 확보| 지표 | 측정하는 것 | 좋음 기준 | 주요 영향 요소 |
|---|---|---|---|
| LCP | 콘텐츠 표시 속도 | ≤ 2.5s | 렌더링 방식(SSG/SSR vs CSR), 이미지 크기 |
| INP | 인터랙션 반응 속도 | ≤ 200ms | JS 번들 크기, Hydration 무게 |
| CLS | 레이아웃 안정성 |
Web Vitals는 실제로 어떻게 측정할까요? Chrome DevTools에 이미 다 있습니다.
Chrome DevTools → Lighthouse 탭 → "Analyze page load" 버튼. LCP, INP, CLS를 포함한 퍼포먼스 점수와 개선 제안을 한번에 볼 수 있습니다.
Chrome DevTools → Performance 탭 → 녹화 버튼 후 페이지 조작. 타임라인에서 어느 시점에 뭐가 실행됐는지, 어디서 병목이 생겼는지 볼 수 있습니다.
Network 탭에서 첫 HTML 응답을 열어보면 렌더링 방식을 가늠할 수 있습니다.
어떤 렌더링 방식을 쓸지는 "이 페이지의 데이터가 얼마나 자주 바뀌는가"와 "인터랙션이 중요한가"로 판단합니다.
렌더링 방식을 잘 선택하는 것 자체가 프론트엔드 성능 최적화의 절반입니다.
다음 주에는 팀 협업을 다룹니다. Git, PR, 컨벤션, 리뷰 문화처럼 혼자 공부할 때는 잘 와닿지 않지만, 팀 프로젝트에서 바로 필요한 것들입니다.
| 약간의 지연 |
| 블로그, 상품 목록 |
| PPR | SSG급 | 컴포넌트별 | 상품 상세, 대시보드 |
| ✅ 포함됨 |
| ≤ 0.1 |
| 이미지 크기, Skeleton, 폰트 |