두 컴포넌트의 경계와 합성 규칙을 익힌다.
서버 컴포넌트와 클라이언트 컴포넌트
App Router의 가장 큰 개념 변화는 컴포넌트가 기본적으로 서버에서 실행된다는 점입니다. 필요할 때만 일부를 클라이언트로 떼어 냅니다. 이 경계를 잘 그으면 빠르고 안전한 앱을 만들 수 있습니다.
학습 목표
- 서버 컴포넌트가 기본값인 이유를 이해한다.
'use client'경계가 어디까지 전파되는지 안다.- 언제 서버를, 언제 클라이언트를 쓸지 판단한다.
- 서버 컴포넌트가 클라이언트 컴포넌트를 감싸는 합성 패턴을 익힌다.
서버 컴포넌트 (기본)
지시어가 없으면 모두 서버 컴포넌트입니다. 서버에서만 실행되므로 DB 접근, 파일 읽기, 비밀 키 사용이 안전합니다. 결과 HTML만 브라우저로 보내고, 컴포넌트 코드 자체는 번들에 포함되지 않습니다.
// app/users/page.tsx — 서버 컴포넌트
import { db } from '@/lib/db';
export default async function UsersPage() {
const users = await db.user.findMany(); // 서버에서만 실행
return (
<ul>
{users.map((u) => (
<li key={u.id}>{u.name}</li>
))}
</ul>
);
}
서버 컴포넌트에서 할 수 없는 것: useState/useEffect 같은 훅, onClick 같은 이벤트 핸들러, window·localStorage 같은 브라우저 API.
클라이언트 컴포넌트
상호작용이 필요하면 파일 맨 위에 'use client' 를 붙입니다. 이 컴포넌트는 서버에서 HTML로 한 번 그려진 뒤, 브라우저에서 다시 살아나(hydration) 상호작용이 가능해집니다.
'use client';
import { useState } from 'react';
export default function LikeButton() {
const [liked, setLiked] = useState(false);
return (
<button onClick={() => setLiked(!liked)}>
{liked ? '좋아요 취소' : '좋아요'}
</button>
);
}
💡 TIP —
'use client'를 한 파일에 붙이면, 그 파일이 import 하는 모든 하위 컴포넌트도 클라이언트 경계 안으로 들어옵니다. 그래서 클라이언트 컴포넌트는 가능한 한 트리의 잎(leaf)에 두는 게 좋습니다.
언제 무엇을 쓸까
| 상황 | 선택 |
|---|---|
| 데이터 패칭, DB/파일 접근 | 서버 컴포넌트 |
| 비밀 키·토큰 사용 | 서버 컴포넌트 |
useState/useEffect 등 훅 | 클라이언트 컴포넌트 |
onClick/onChange 이벤트 | 클라이언트 컴포넌트 |
localStorage/window 등 브라우저 API | 클라이언트 컴포넌트 |
기본은 서버, 상호작용이 필요한 작은 조각만 클라이언트로 — 이 원칙만 기억하세요.
props 직렬화 규칙
서버 컴포넌트가 클라이언트 컴포넌트에 props를 넘길 때, 그 값은 직렬화 가능한 것이어야 합니다. 네트워크를 건너 전달되기 때문입니다.
// 가능: 문자열, 숫자, 불리언, 배열, 일반 객체, Date
<Chart data={[1, 2, 3]} title="매출" />
// 불가능: 함수, 클래스 인스턴스
<Widget onLoad={() => {}} /> // ❌ 함수는 못 넘김
⚠️ 주의 — 서버 → 클라이언트로 함수를 props로 넘길 수 없습니다. 이벤트 핸들러는 클라이언트 컴포넌트 내부에서 정의하세요.
서버 → 클라이언트 합성
흔한 패턴은 서버 컴포넌트가 데이터를 읽어 클라이언트 컴포넌트에 children으로 끼워 넣는 것입니다. 클라이언트 경계 안에서도 서버에서 렌더된 내용을 그대로 보여줄 수 있습니다.
// app/page.tsx — 서버 컴포넌트
import Tabs from './tabs'; // 'use client'
import { getNews } from '@/lib/news';
export default async function Home() {
const news = await getNews(); // 서버에서 데이터
return (
<Tabs>
{/* children 으로 끼워 넣으면 클라이언트 안에서도 서버 결과 표시 */}
<ul>
{news.map((n) => (
<li key={n.id}>{n.title}</li>
))}
</ul>
</Tabs>
);
}
이 강좌 사이트도 마크다운을 서버에서 읽어 본문으로 만들고, 사이드바의 펼침/접힘만 클라이언트 컴포넌트로 처리합니다.
요약
- 지시어가 없으면 서버 컴포넌트,
'use client'가 있으면 클라이언트 컴포넌트다. 'use client'경계는 import 하는 하위 컴포넌트까지 전파된다.- 데이터·비밀은 서버에서, 상호작용은 클라이언트에서.
- 서버 → 클라이언트 props는 직렬화 가능해야 하고, 함수는 넘길 수 없다.
연습문제
- 서버 컴포넌트로 글 목록을 그리고, 각 글에
'use client'좋아요 버튼을 붙여 보세요. 'use client'가 트리 위쪽에 있을 때와 잎에 있을 때 번들 크기에 어떤 차이가 생길지 설명해 보세요.- 서버 컴포넌트에서 클라이언트 컴포넌트로 함수를 넘기려다 막혔습니다. 어떻게 해결할까요?
힌트 — 클라이언트 컴포넌트는 가능한 한 작게, 잎에 두세요. 함수가 필요하면 그 동작을 클라이언트 컴포넌트 내부로 옮기거나 서버 액션(다음 레슨들에서 다룸)을 고려합니다.
💡 연습문제 풀이
불러오는 중…
댓글 0
“Next.js” 강좌에 대한 댓글입니다.