본문 바로가기

개발 공부/React

Server Components, Client Components

 

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가 필요할 때만 클라이언트.

 


 

 

서버 컴포넌트가 유리한 경우

  1. 데이터 패칭이 핵심일 때
    • DB, GraphQL, REST를 서버에서 바로 호출
  2. SEO가 중요할 때
    • 초기 HTML에 풍부한 데이터가 있어야 함
  3. 보안 자원 접근이 필요할 때
    • 세션, 쿠키, 토큰 등
  4. 리스트/카탈로그형 페이지
    • 대량 데이터를 스트리밍으로 점진적 렌더링
  5. 상호작용이 거의 없는 경우
    • 정적 콘텐츠, 문서, 블로그
// 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>
  );
}

 

 


 

클라이언트 컴포넌트가 유리한 경우

  1. 상태/이벤트가 필요할 때
    • 폼 입력, 드롭다운, 모달, 토글
  2. 브라우저 API 사용 시
    • localStorage, IntersectionObserver 등
  3. 리치 UI 인터랙션
    • 드래그&드롭, 차트 줌/팬, 에디터
  4. 서드파티 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