React 서버 컴포넌트(RSC)는 브라우저에 자체 JavaScript를 한 바이트도 전송하지 않습니다. 서버에서만 실행되고, HTML을 클라이언트에 스트리밍하며, API 레이어 없이 직접 데이터를 가져올 수 있습니다. Next.js App Router에서는 모든 컴포넌트가 기본적으로 서버 컴포넌트입니다. 이 가이드는 멘탈 모델, 서버와 클라이언트 간 경계, 대규모로 잘 동작하는 패턴을 설명합니다.
핵심 아이디어
RSC 이전에는 모든 React 컴포넌트가 브라우저에 JavaScript를 전송했습니다. 데이터베이스 행을 가져오고, 날짜를 포맷하고, 카드를 렌더링하는 컴포넌트는 데이터베이스 드라이버, 날짜 라이브러리, 렌더링 로직을 모두 브라우저 번들에 포함시켰습니다 — 브라우저는 데이터베이스를 호출하지 않는데도.
RSC는 이를 바꿉니다: 서버 컴포넌트는 서버에서만 실행됩니다. 출력이 특수 형식(RSC 페이로드)으로 직렬화되어 클라이언트에 스트리밍됩니다. 클라이언트는 렌더링된 HTML과 작은 재조정 페이로드를 받습니다 — 컴포넌트의 소스 코드가 아닌.
서버 vs 클라이언트 컴포넌트
| 기능 | 서버 컴포넌트 | 클라이언트 컴포넌트 |
|---|---|---|
| 서버에서 실행 | 예 | 예 (하이드레이션) |
| 클라이언트에서 실행 | 아니오 | 예 |
| 브라우저에 JS 전송 | 아니오 | 예 |
| 직접 DB/파일시스템 접근 | 예 | 아니오 |
| useState, useEffect | 아니오 | 예 |
| 이벤트 핸들러 (onClick 등) | 아니오 | 예 |
| 브라우저 API (window, document) | 아니오 | 예 |
파일 최상단에 "use client"를 추가하여 클라이언트 컴포넌트로 표시합니다. 이것이 옵트인 경계입니다. 클라이언트 컴포넌트를 임포트하는 모든 컴포넌트도 클라이언트 번들의 일부가 됩니다 — 경계는 아래 방향으로 전파됩니다.
서버 컴포넌트에서의 데이터 페칭
서버 컴포넌트는 async 함수가 될 수 있습니다. 데이터를 직접 await할 수 있습니다:
async function ProductPage({ id }) { const product = await db.products.findById(id); return <ProductCard product={product} />; }
useEffect도, 로딩 상태도, API 라우트도 없습니다. 컴포넌트는 페칭하는 동안 일시 중지되고, React는 준비되면 결과를 클라이언트에 스트리밍합니다. 대기 중 폴백을 표시하려면 <Suspense>로 감싸세요.
Suspense 스트리밍 패턴
Suspense 경계는 Next.js가 HTML을 청크로 스트리밍하게 합니다. 빠른 컴포넌트는 즉시 렌더링되어 전송되고; 느린 컴포넌트는 나중에 렌더링되어 이미 그려진 셸에 스트리밍됩니다:
- 페이지 셸 — 내비게이션, 레이아웃, 헤더 — 즉시 렌더링.
- 히어로 콘텐츠 — 중요 데이터, 빠른 DB 쿼리 — ~50ms에 렌더링.
- 추천, 리뷰 — 느린 쿼리 — 준비되면 렌더링되어 스켈레톤 플레이스홀더를 교체.
사용자는 빈 화면 이후 갑작스러운 전체 렌더 대신 점진적으로 완성되는 페이지를 봅니다.
일반적인 패턴
서버 데이터를 클라이언트 컴포넌트에 props로 전달
서버 컴포넌트는 클라이언트 컴포넌트를 임포트하고 데이터를 전달할 수 있습니다. 데이터는 직렬화되어(JSON 직렬화 가능해야 함 — 클래스 인스턴스, 함수 불가) props로 전달됩니다.
이벤트 핸들러는 클라이언트 컴포넌트에 유지
onClick, onChange, useState, 브라우저 API를 사용하는 컴포넌트는 "use client"로 표시해야 합니다. 이 컴포넌트들을 작게 유지하고 컴포넌트 트리의 잎으로 밀어 번들 크기를 최소화하세요.
성능 영향
RSC를 채택한 실제 Next.js 앱은 클라이언트 사이드에서 데이터를 가져오던 페이지에서 JavaScript 번들 크기가 30~60% 감소합니다. 초기 HTML에 로딩 스켈레톤 대신 실제 콘텐츠가 포함되어 LCP가 개선됩니다. 로드 시 브라우저가 파싱하고 실행해야 할 JavaScript가 적어 INP도 개선됩니다.