'use client'를 단순히 "이 파일을 클라이언트에서 실행해라"로 이해하면 절반만 맞습니다. 정확한 의미는 아래와 같습니다.
'use client'는 파일 단위가 아니라 모듈 의존성 트리의 경계를 만듭니다. 이 경계 아래에서 import된 모든 모듈은, 그 모듈에 'use client'가 없더라도 클라이언트 번들에 포함됩니다.
모듈 의존성 트리에서의 import 규칙:
서버 컴포넌트
├── import 서버 컴포넌트 ✅ (당연히 가능)
├── import 클라이언트 컴포넌트 ✅ (가능 - 경계 생성)
└── 서버 → 클라이언트 혼합 가능
클라이언트 컴포넌트 ('use client' 경계 아래)
├── import 클라이언트 컴포넌트 ✅ (당연히 가능)
└── import 서버 컴포넌트 ❌ (불가능 - 이미 클라이언트 영역)
핵심 규칙은 하나입니다: 클라이언트 컴포넌트가 import한 것은 모두 클라이언트가 된다.
예시로 살펴봅시다:
// utils/format.ts - 'use client' 없음
export function formatDate(date: string) {
return new Date(date).toLocaleDateString('ko-KR');
}
'use client';
import { formatDate } from '@/utils/format';
// ↑ format.ts에 'use client'가 없지만,
// 클라이언트 컴포넌트에서 import했으므로 클라이언트 번들에 포함됨
export default function PostDate({ date }: { date: string }) {
return <time>{formatDate(date)}</time>;
}
'use client' 경계가 트리 위쪽에 있을수록 더 많은 코드가 클라이언트 번들에 포함됩니다. 반대로 경계를 가능한 한 컴포넌트 트리의 끝(leaf) 가까이 내려놓으면 서버 컴포넌트의 이점을 최대로 살릴 수 있습니다.
모듈 의존성 트리와 컴포넌트 트리는 무엇이 다른가요?
모듈 의존성 트리는 파일 간 import 관계로 이루어진 의존성 구조입니다. 어떤 파일이 어떤 파일을 import하는지를 기준으로 번들러가 모듈들을 연결합니다. 컴포넌트 트리는 React가 실행될 때 JSX를 기반으로 만들어지는 컴포넌트 계층 구조입니다.
BlogLayout ('use client') ← 경계가 최상단에!
├── 사이드바 (카테고리 목록) → 클라이언트 번들에 포함
├── 토글 버튼 (useState) → 클라이언트 번들에 포함
└── children (하위 페이지) → props로 전달되므로 서버 컴포넌트 유지 가능
Ch.1에서 만든 블로그 레이아웃은 useState 하나 때문에 전체가 클라이언트 컴포넌트가 되었습니다.
BlogLayout (서버 컴포넌트) ← 서버에서 실행
├── CategorySidebar ('use client') ← 인터랙션이 필요한 부분만
│ ├── 토글 버튼 (useState)
│ └── 카테고리 목록
└── children (하위 페이지) ← 서버 컴포넌트 유지 가능
코드로 비교해 봅시다:
// components/CategorySidebar.tsx
'use client';
import { useState } from 'react';
const categories = ['React', 'Next.js', 'TypeScript'];
export default function CategorySidebar() {
const [isOpen, setIsOpen] = useState(true);
return (
<aside className="w-60 p-4 border-r">
<button onClick={() => setIsOpen(!isOpen)}>
카테고리 {isOpen ? '▲' : '▼'}
</button>
{isOpen && (
<ul>
{categories.map(cat => (
<li key={cat}>{cat}</li>
))}
</ul>
)}
</aside>
);
}
// app/blog/layout.tsx - 서버 컴포넌트로 유지!
import CategorySidebar from '@/components/CategorySidebar';
export default function BlogLayout({
children,
}: Readonly<{ children: React.ReactNode }>) {
return (
<div className="flex">
<CategorySidebar />
<main className="flex-1 p-6">{children}</main>
</div>
);
}
이제 BlogLayout은 서버 컴포넌트로 유지되고, 'use client' 경계는 정말 인터랙션이 필요한 CategorySidebar에만 적용됩니다.
새 컴포넌트를 만들 때 이 흐름을 따르세요:
새 컴포넌트를 만든다
│
├─ onClick, onChange 등 이벤트 핸들러가 필요한가?
│ └─ Yes → 'use client'
│
├─ useState, useReducer 등 상태가 필요한가?
│ └─ Yes → 'use client'
│
├─ useEffect, useRef 등 브라우저 Hook이 필요한가?
│ └─ Yes → 'use client'
│
├─ window, document 등 브라우저 API가 필요한가?
│ └─ Yes → 'use client'
│
└─ 위 모두 아니다
└─ 서버 컴포넌트로 유지 (아무것도 안 함)
| 개념 | 핵심 내용 |
|---|---|
'use client'의 의미 | 이 파일과 아래 모든 import를 클라이언트 번들에 포함 |
| 경계 전파 | 클라이언트가 import한 모듈은 모두 클라이언트가 됨 |
| 설계 원칙 | 경계를 가능한 한 컴포넌트 트리의 끝에 배치하기 |
| 리팩터링 전략 | 인터랙션이 필요한 부분만 별도 컴포넌트로 분리 |
그런데 서버 컴포넌트와 클라이언트 컴포넌트를 함께 쓸 때 주의해야 할 제약이 있습니다. 다음 세션에서는 children/props로 서버 컴포넌트를 전달하는 방법과 직렬화 경계를 학습합니다.