Next.js App Router에서 컴포넌트는 기본적으로 Server Component로 동작한다. 그러나 버튼 클릭, 폼 입력, 애니메이션처럼 브라우저와 상호작용이 필요한 순간에는 Client Component가 필요하다. 'use client' 선언 하나로 컴포넌트의 실행 환경이 서버에서 브라우저로 전환된다.
파일 최상단에 'use client'를 추가하면 해당 파일과 그 하위 import 트리 전체가 클라이언트 번들에 포함된다.
'use client'
import { useState } from 'react'
export default function Counter() {
const [count, setCount] = useState(0)
return (
<button onClick={() => setCount(count + 1)}>
클릭 횟수: {count}
</button>
)
}
'use client' — React가 이 모듈을 클라이언트 번들 진입점으로 표시하는 디렉티브. Next.js는 이 경계를 기준으로 서버/클라이언트 번들을 분리한다.
| 필요 상황 | 이유 |
|---|---|
useState, useReducer | 브라우저 메모리에 상태 유지 필요 |
useEffect | DOM 마운트 이후 사이드 이펙트 실행 |
이벤트 핸들러 (onClick 등) | 브라우저 이벤트 루프에서만 동작 |
window, document 접근 | 서버 환경에는 해당 전역 객체 없음 |
| 외부 클라이언트 라이브러리 | 브라우저 API에 의존하는 패키지 |
Client Component는 가능한 한 트리의 말단(leaf)에 배치하는 것이 핵심 원칙이다. 데이터 페칭과 레이아웃은 Server Component가 담당하고, 상호작용이 필요한 작은 부분만 Client Component로 분리한다.
Page (Server Component)
├── Header (Server Component)
│ └── NavMenu (Server Component)
├── ArticleList (Server Component) ← DB 쿼리
│ └── LikeButton (Client Component) ← 클릭 이벤트
└── Footer (Server Component)
Server Component가 Client Component를 children prop으로 전달하면, Server Component의 렌더링 결과(직렬화된 JSX)가 클라이언트로 스트리밍된다.
// layout.tsx (Server Component)
import Modal from './Modal' // Client Component
import ProductInfo from './ProductInfo' // Server Component
export default async function Page() {
const product = await fetchProduct()
return (
<Modal>
<ProductInfo product={product} />
</Modal>
)
}
children prop 패턴 — Server Component를 Client Component의 자식으로 넘기면 서버에서 미리 렌더링된 결과가 클라이언트 번들을 늘리지 않고 전달된다.
빌드 타임
└── 번들러가 'use client' 경계 감지
└── 해당 모듈 → 클라이언트 JS 번들에 포함
요청 타임
Server Component → HTML 생성 (스트리밍)
│
└── Client Component placeholder 삽입
│
브라우저 Hydration
└── React가 placeholder에 이벤트 핸들러 연결
Hydration — 서버에서 생성된 정적 HTML에 React가 이벤트 리스너와 상태를 연결하는 과정. Client Component만 이 과정을 거친다.
useEffect는 서버에서 실행되지 않는다. 브라우저 전용 API(localStorage, navigator 등)는 반드시 useEffect 내부에서 접근해야 한다.
'use client'
import { useState, useEffect } from 'react'
export default function ThemeToggle() {
const [theme, setTheme] = useState('light')
useEffect(() => {
const saved = localStorage.getItem('theme')
if (saved) setTheme(saved)
}, [])
return <button onClick={() => setTheme(t => t === 'light' ? 'dark' : 'light')}>{theme}</button>
}
Client Component를 최소한으로 유지하고 Server Component와 올바르게 조합하면, 번들 크기를 줄이면서도 풍부한 상호작용을 구현할 수 있다.