Server Components vs Client Components
| Server Component | Client Component | |
| 실행 위치 | Node.js 런타임(서버) | 브라우저 |
| 번들 크기 | 클라이언트 번들에 포함 X | 포함 |
| 상태/이벤트 | useState, useEffect 불가 | 가능 |
| 브라우저 API | 사용 불가 | 사용 가능 (window, document 등) |
| 데이터 패칭 | DB/서버액션 직접 호출 가능 | 보통 fetch/API 경유 |
| 보안 자원 접근 | 쿠키, 세션, 비밀키 접근 안전 | 노출 위험 |
| 스트리밍/Suspense | 서버 스트리밍 강점 | 클라 lazy 지원 |
| 서드파티 UI | 대부분 불가 | React state/event 필요 시 유리 |
기본은 서버이고, 상태/이벤트·브라우저 API·리치 UI가 필요할 때만 클라이언트.
서버 컴포넌트가 유리한 경우
- 데이터 패칭이 핵심일 때
- DB, GraphQL, REST를 서버에서 바로 호출
- SEO가 중요할 때
- 초기 HTML에 풍부한 데이터가 있어야 함
- 보안 자원 접근이 필요할 때
- 세션, 쿠키, 토큰 등
- 리스트/카탈로그형 페이지
- 대량 데이터를 스트리밍으로 점진적 렌더링
- 상호작용이 거의 없는 경우
- 정적 콘텐츠, 문서, 블로그
// app/products/page.tsx (Server Component)
import { fetchProducts } from '@/lib/data';
export default async function ProductsPage() {
const products = await fetchProducts();
return (
<ul>
{products.map(p => <li key={p.id}>{p.name}</li>)}
</ul>
);
}
클라이언트 컴포넌트가 유리한 경우
- 상태/이벤트가 필요할 때
- 폼 입력, 드롭다운, 모달, 토글
- 브라우저 API 사용 시
- localStorage, IntersectionObserver 등
- 리치 UI 인터랙션
- 드래그&드롭, 차트 줌/팬, 에디터
- 서드파티 UI 라이브러리 활용
- React state/event에 의존
// app/products/AddToCartButton.tsx (Client Component)
'use client';
import { useTransition } from 'react';
type Props = { productId: string; addToCart: (id: string)=>Promise<void> };
export default function AddToCartButton({ productId, addToCart }: Props) {
const [isPending, start] = useTransition();
return (
<button
disabled={isPending}
onClick={() => start(() => addToCart(productId))}
>
{isPending ? 'Adding...' : 'Add to Cart'}
</button>
);
}
- 가능한 한 상위는 서버, 하위에서 꼭 필요한 부분만 클라로 분리
- 클라 컴포넌트는 작은 “섬(islands)”처럼 국소적으로 존재
“서버-클라 분리” 패턴
// app/products/[id]/page.tsx
import { getProduct } from '@/lib/data';
import { AddToCartButton } from './_components/AddToCartButton'; // 클라 컴포넌트
import { addToCart } from './_actions'; // Server Action
export default async function ProductDetail({ params }: { params: { id: string }}) {
const product = await getProduct(params.id);
return (
<div>
<h1>{product.name}</h1>
<AddToCartButton productId={product.id} addToCart={addToCart} />
</div>
);
}
// app/products/[id]/_components/AddToCartButton.tsx (Client)
'use client';
import { useOptimistic, useTransition } from 'react';
export function AddToCartButton({ productId, addToCart }: {
productId: string;
addToCart: (id: string)=>Promise<void>;
}) {
const [isPending, start] = useTransition();
const [count, setCount] = useOptimistic(0, (c) => c + 1);
return (
<button
disabled={isPending}
onClick={() => start(async () => {
setCount(null as any); // 낙관적 업데이트 트리거
await addToCart(productId);
})}
>
{isPending ? 'Adding...' : `Add to Cart (${count})`}
</button>
);
}
// app/products/[id]/_actions.ts (Server Action)
'use server';
import { cookies } from 'next/headers';
import { addCartItem } from '@/lib/cart';
export async function addToCart(productId: string) {
const user = cookies().get('user-id')?.value;
if (!user) throw new Error('Unauthenticated');
await addCartItem(user, productId);
}
- 데이터는 서버에서 가져오고, 상호작용만 클라로 최소화
- Server Action으로 클라→서버 호출을 타입 안전하게
'개발 공부 > React' 카테고리의 다른 글
| custom modal 만들기 (0) | 2025.10.21 |
|---|---|
| React App Router, Page Route (0) | 2025.10.19 |
| useOptimistic (0) | 2025.08.28 |
| Zustand (1) (2) | 2025.08.17 |
| React Recoil (3) | 2025.08.16 |