서버 컴포넌트 vs 클라이언트 컴포넌트 — 무엇을 언제 쓸까
Next.js App Router의 핵심 개념인 서버 컴포넌트와 클라이언트 컴포넌트의 차이를 완벽히 이해합니다. 각각의 기능, 제약, 'use client' 경계 동작, 실전 선택 기준을 다룹니다.
지난 글에서 template.tsx의 재마운트 특성을 알아봤습니다. App Router를 제대로 쓰려면 그 밑바닥에 있는 서버 컴포넌트(RSC)와 클라이언트 컴포넌트의 차이를 명확히 이해해야 합니다. 이 두 가지를 언제 쓸지 혼동하면 불필요한 클라이언트 번들이 커지거나 서버 기능을 제대로 활용하지 못하게 됩니다.
두 가지 컴포넌트 모델
App Router에서 모든 컴포넌트는 기본적으로 서버 컴포넌트입니다. 'use client' 지시문을 추가해야만 클라이언트 컴포넌트가 됩니다.
// Server Component (기본값 — 선언 없음)
export default async function UserProfile({ id }: { id: string }) {
const user = await db.user.findUnique({ where: { id } }); // DB 직접 접근
return <div>{user.name}</div>;
}
// Client Component
'use client';
import { useState } from 'react';
export default function Counter() {
const [count, setCount] = useState(0); // useState 사용 가능
return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}
서버 컴포넌트의 능력과 제약
서버 컴포넌트는 Node.js 환경에서 렌더링됩니다. 다음이 가능합니다.
async/await를 컴포넌트에서 직접 사용- 데이터베이스, 파일시스템에 직접 접근
- API 키, 시크릿 환경 변수 안전하게 사용
- 큰 의존성을 클라이언트 번들에서 제외
하지만 브라우저 API와 React 인터랙티비티 기능은 쓸 수 없습니다.
// 서버 컴포넌트에서 불가능한 것들
// ❌ import { useState } from 'react';
// ❌ onClick, onChange 등 이벤트 핸들러
// ❌ window, document, localStorage
// ❌ useEffect, useContext, useReducer
‘use client’ 경계
'use client'는 파일 최상단에 선언합니다. 이 선언은 모듈 그래프에서 경계(boundary)를 만듭니다. 해당 파일과 그 파일에서 import하는 모든 모듈이 클라이언트 번들에 포함됩니다.
// app/components/ProductList.tsx
'use client'; // 이 파일부터 클라이언트 경계
import { useState } from 'react';
import { AddToCart } from './AddToCart'; // 자동으로 클라이언트
export function ProductList({ products }) {
const [filter, setFilter] = useState('all');
// ...
}
AddToCart가 'use client'를 직접 선언하지 않아도 ProductList가 import하므로 클라이언트 컴포넌트가 됩니다.
언제 서버, 언제 클라이언트?
실전에서 판단 기준은 단순합니다.
데이터 페칭, DB 접근, 민감 정보 처리
→ 서버 컴포넌트 (기본값 유지)
useState, useEffect, 이벤트 핸들러, 브라우저 API
→ 클라이언트 컴포넌트 ('use client')
일반적인 Next.js 앱의 컴포넌트 비율은 서버 70% + 클라이언트 30% 정도가 이상적입니다. 클라이언트 컴포넌트는 상호작용이 필요한 최소 단위에만 적용하세요.
서버 컴포넌트를 클라이언트에 전달하기
클라이언트 컴포넌트 내부에서 서버 컴포넌트를 직접 import할 수 없습니다. 하지만 props(children)로 전달하는 것은 가능합니다.
// ✅ 올바른 패턴: children으로 전달
// app/page.tsx (Server)
import { Modal } from '@/components/Modal'; // Client
import { UserInfo } from '@/components/UserInfo'; // Server
export default function Page() {
return (
<Modal>
<UserInfo /> {/* 서버 컴포넌트를 children으로 전달 */}
</Modal>
);
}
// ❌ 잘못된 패턴: 클라이언트에서 서버 컴포넌트 import
// components/Modal.tsx
'use client';
import { UserInfo } from './UserInfo'; // ❌ 서버 전용 코드가 클라이언트로 끌려옴
이 패턴은 **컴포넌트 합성(Composition)**의 핵심입니다. 다음 글에서 더 깊이 다룹니다.
클라이언트 컴포넌트의 서버 사이드 렌더링
'use client'가 “브라우저에서만 실행”을 의미하지 않습니다. 클라이언트 컴포넌트도 초기 페이지 로드 시 서버에서 HTML로 사전 렌더링됩니다. ‘use client’는 “인터랙티비티를 위해 클라이언트에서도 실행”을 의미합니다.
서버 컴포넌트: 서버에서만 실행 (HTML)
클라이언트 컴포넌트: 서버에서도 실행 (HTML) + 클라이언트에서 Hydration
지난 글: Next.js template.tsx — 페이지마다 초기화되는 특별한 레이아웃
다음 글: 서버·클라이언트 컴포넌트 합성 패턴
읽어주셔서 감사합니다. 😊