React 서버 컴포넌트(React Server Components, RSC)는 서버에서만 실행되고, 컴포넌트 코드가 아니라 실행 결과(직렬화된 엘리먼트 트리)만 클라이언트로 보내는 컴포넌트다. Next.js App Router에서는 'use client'를 붙이지 않은 컴포넌트가 기본으로 서버 컴포넌트다.
export default async function ProductList() {
const products = await db.product.findMany() // useEffect·fetch API 없이 바로
return <ul>{products.map(p => <li key={p.id}>{p.name}</li>)}</ul>
}왜 나왔나
CSR(Client-Side Rendering)은 JS(JavaScript)를 받고, 실행하고, 그제야 API(Application Programming Interface)를 불러 화면을 그렸다. SSR(Server-Side Rendering)은 HTML(HyperText Markup Language)을 먼저 주지만 데이터는 페이지 단위(getServerSideProps)로만 가져올 수 있었고, 하이드레이션이 필요 없는 부분까지 모든 컴포넌트의 JS를 보내야 했다.
- 번들 0: 서버 컴포넌트와 거기서 쓴 라이브러리(DB(Database) 클라이언트, 마크다운·코드 하이라이터)는 클라이언트 번들에 들어가지 않는다. 하이드레이션할 컴포넌트도 줄어 쓸 수 있는 시점이 당겨진다(하이드레이션)
- 컴포넌트 단위 데이터 접근: 트리 어디서든 DB·파일 시스템·내부 API에 직접 접근한다. 비밀 키가 브라우저로 새지 않는다
- 클라이언트-서버 워터폴(Waterfall) 감소: 부모가 받아 온 결과로 자식이 다시 요청하는 왕복을 서버 안에서 끝낸다
- 한 번만 실행된다: 요청당 한 번 실행되고 다시 렌더되지 않는다. 그래서 의존성 배열, 낡은 클로저, 메모이제이션 고민이 사라진다(부수 효과와 Effect)
sequenceDiagram participant B as 브라우저 participant S as 서버 Note over B,S: CSR B->>S: 페이지 요청 S-->>B: 모든 컴포넌트의 JS 번들 Note over B: JS 실행 B->>S: API 호출 S-->>B: 데이터 Note over B: 그제야 화면을 그림 Note over B,S: RSC B->>S: 페이지 요청 Note left of S: 서버 컴포넌트 실행, DB 직접 조회 S-->>B: 실행 결과 (직렬화된 엘리먼트 트리) + 클라이언트 컴포넌트 JS만 Note over B: 서버 컴포넌트 코드·라이브러리는 번들에 없음
못 하는 것
state, Effect, 이벤트 핸들러, window·localStorage 같은 브라우저 API. 이런 상호작용이 필요한 부분만 클라이언트 컴포넌트로 떼어 낸다.
SSR과의 관계
SSR은 HTML을 만드는 기술이고, RSC는 컴포넌트를 서버에서 실행하는 모델이다. 둘은 함께 쓰인다. 서버 컴포넌트가 RSC 페이로드를 만들면, 첫 방문에는 그걸로 HTML까지 만들어 스트리밍하고, 이후 이동에는 페이로드만 받아 클라이언트 state를 유지한 채 바뀐 부분을 합친다. 쓰기는 서버 함수와 서버 액션, 렌더링 방식 전체는 CSR·SSR·SSG·ISR.
'use client'는 SSR을 끄지 않는다. 이 지시어는 모듈이 서버와 클라이언트 중 어느 쪽 그래프에 속하는지(번들 경계)를 정할 뿐이다. 클라이언트 컴포넌트도 첫 로드 때는 서버에서 HTML로 미리 렌더되고 브라우저에서 하이드레이션된다. 그래서 렌더 중 window를 바로 읽으면 클라이언트 컴포넌트여도 서버에서 에러가 난다(서버 렌더에서 훅이 하는 일).
출처: Josh W. Comeau - Making Sense of React Server Components · Next.js - Server and Client Components: On the server