React Server Components — 2021 RFC, 2023 Next.js App Router 의 default 가 되며 mainstream 진입. 이름이 헷갈리는 점 — "server 에서 render" 는 SSR 와 다른 의미. RSC 는 "client 에 JS 전송하지 않는 component". 이 가이드는 server vs client boundary, RSC payload format, props serialization 함정을 정리한다.
SSR vs RSC — 다른 것
SSR (Server-Side Rendering, ~2014):
1. server 가 HTML render
2. browser 가 HTML 받음 (TTFB)
3. JS bundle download (component code 다)
4. hydration — server HTML 위에 React 부착
→ 모든 component 코드가 client 에 전송
RSC (React Server Component, 2021+):
1. server 가 React tree render → RSC payload (JSON-like)
2. client 가 payload 로 DOM update
→ Server Component 의 코드는 client 에 전송 X
→ JS bundle 작아짐 (server-only 코드 제외)
차이:
SSR = "server 에서 HTML 만들기" (모든 component JS 는 여전히 client)
RSC = "server 에서만 도는 component" (그 코드는 client 안 감)Server vs Client Boundary
// app/page.tsx — server component (default in Next.js App Router)
import {db} from "@/lib/db";
export default async function Page() {
const posts = await db.posts.findAll(); // server-only — DB 호출
return <PostList posts={posts} />;
}
// components/PostList.tsx — server component (server context 상속)
export function PostList({posts}: {posts: Post[]}) {
return (
<ul>
{posts.map(p => <PostCard key={p.id} post={p} />)}
</ul>
);
}
// components/Counter.tsx — client component (명시)
"use client";
import {useState} from "react";
export function Counter() {
const [n, setN] = useState(0); // useState 사용 → client 필수
return <button onClick={() => setN(n+1)}>{n}</button>;
}
규칙:
- "use client" directive 박힌 파일 = client component
- 그 파일이 import 하는 module 도 client bundle 에 박힘
- server component 는 client component 를 import 할 수 있음 (그 반대 X 직접)
- 단 prop 으로 server → client 전달은 가능"use client" 의 정확한 의미
잘못된 멘탈 모델:
"use client" = "이 component 는 client 에서 render"
→ 사실 서버에서도 render (SSR HTML 만들기 위해)
정확:
"use client" = "이 module 부터 client boundary 시작"
→ 이 file 의 코드는 client bundle 에 박힘
→ 자식 component 의 default 가 client 로 바뀜
→ useState / useEffect / event handler 같은 client-only API 가능
→ "use client" 는 boundary 표시. 위치 (component tree 의 어디) 가 중요.
권장 패턴:
Page (server) → ServerComponentA (server) → LeafInteractive (client)
client boundary 를 leaf 가까이 → bundle 최소화.RSC Payload Format
server 가 client 에 보내는 것은 HTML 아닌 RSC payload (text format).
예 (간략):
1:I["@/components/PostList","default"]
2:I["@/components/Counter","default"]
M0:["$","ul",null,{"children":[
["$","li",null,{"children":["Post 1"]}],
["$","li",null,{"children":["Post 2"]}]
]}]
M1:["$","$L2",null,{}]
J0:[M0, M1]
특징:
- JSON-like 이지만 React-aware
- I (import) line: client component 의 reference
- M (model) line: React tree
- 직렬화 가능한 props 만 (function 안 됨, Date / Map 도 정확히 안 됨)
- streaming — payload 가 chunk 로 전달, browser 가 받자마자 render
vs HTML:
- HTML: 초기 paint 만 표현, 이후 React 가 "다시 render" 필요
- RSC payload: incremental update 가능, "이 트리의 일부만 갱신"Props Serialization 함정
server → client component 에 prop 전달 시:
✓ OK:
- primitive (string, number, boolean, null, undefined)
- plain object / array
- Date (Next 15+에서 가능)
- Promise (server component 가 await 안 한 promise 도 stream)
✗ 불가:
- function (closure 직렬화 불가)
- class instance (prototype 잃음)
- Map / Set (또는 일부 framework 에서만)
- DOM node, React element
흔한 함정:
// ❌ 안 됨
<Button onClick={() => fetchData()} /> ← function prop
// → "Functions cannot be passed directly to Client Components"
// ✓ 해결 — Server Action
async function handleClick() {
"use server";
await fetchData();
}
<Button action={handleClick} />Server Action — server function 을 prop 으로
// app/actions.ts
"use server";
export async function createPost(formData: FormData) {
await db.posts.create({title: formData.get("title")});
revalidatePath("/posts");
}
// app/page.tsx
import {createPost} from "./actions";
export default function Page() {
return (
<form action={createPost}> ← server action 을 form action 으로
<input name="title" />
<button type="submit">Create</button>
</form>
);
}
→ "use server" function 은 자동으로 endpoint 생성.
client 가 form submit 시 그 endpoint 에 POST → server 에서 실행.
API 직접 안 짜고 server logic 호출 가능.
주의: "use server" 도 module-level directive. 같은 file 의 export 모두 server action.RSC 의 실제 효과
| 측정 | SSR 만 | RSC |
|---|---|---|
| Initial JS bundle | 3 MB (모든 component) | 1 MB (client 만) |
| library cost (markdown parser 등) | client 에도 download | server 만 → 0 client cost |
| DB query | API endpoint 거쳐 | component 안에서 직접 |
| SEO | OK (SSR HTML) | OK + 더 적은 JS |
흔한 함정
- "use client" 너무 위에 — Page 에 박으면 모든 tree 가 client → RSC 효과 0. leaf 가까이.
- server component 에서 hook 사용 — useState / useEffect 호출 시 build error. 그 코드는 client 로 이전.
- function prop 전달 — error. Server Action 또는 client component 안에서 직접 정의.
- environment variable leak — server component 의 process.env.DB_PASSWORD 가 무심코 client prop 으로 가면 RSC payload 에 박혀 leak.
- 같은 component 가 server/client 양쪽 사용 — server 와 client 가 다른 export 같은 layout 어색. 명확한 boundary.
마무리
RSC 의 본질 — "client 에 안 보내는 component". server 에서 render + 그 코드는 client bundle 에 박지 않음. SSR 이 "HTML 미리" 였다면 RSC 는 "component 의 일부를 server 만의 것으로".
실용 — default server, leaf 만 "use client". DB / 외부 API 호출은 server component 에서 직접 (API endpoint 생략). Server Action 으로 form / mutation. 큰 markdown parser / DB driver 같은 library 는 server 만에 → bundle 큰 폭 감소.