개요

React Server Components(RSC)는 React 18에서 도입된 혁신적인 렌더링 패러다임입니다. 서버에서만 실행되는 컴포넌트를 통해 번들 크기를 줄이고 초기 로딩 성능을 크게 향상시킬 수 있습니다.

핵심 개념

RSC는 기존 SSR과는 근본적으로 다른 접근 방식을 취합니다:

  • 제로 번들 임팩트: 서버 컴포넌트 코드는 클라이언트 번들에 포함되지 않습니다.
  • 직접 데이터 접근: 데이터베이스나 파일 시스템에 직접 접근할 수 있습니다.
  • 자동 코드 스플리팅: 클라이언트 컴포넌트를 자동으로 분리합니다.
  • 스트리밍 지원: Suspense와 결합하여 점진적 렌더링이 가능합니다.

서버 컴포넌트 vs 클라이언트 컴포넌트

올바른 컴포넌트 타입 선택이 중요합니다:

// ✅ 서버 컴포넌트 (기본)
// - 데이터 페칭
// - 민감한 정보 접근
// - 큰 의존성 사용

async function ProductList() {
  const products = await db.product.findMany();
  return <div>{products.map(p => <ProductCard {...p} />)}</div>;
}

// ✅ 클라이언트 컴포넌트
// - 인터랙티브 UI
// - 브라우저 API 사용
// - 상태 관리

'use client';

function AddToCartButton({ productId }) {
  const [isLoading, setIsLoading] = useState(false);

  const handleClick = async () => {
    setIsLoading(true);
    await addToCart(productId);
    setIsLoading(false);
  };

  return <button onClick={handleClick}>{isLoading ? '추가 중...' : '장바구니 담기'}</button>;
}

컴포넌트 구성 패턴

서버와 클라이언트 컴포넌트를 효과적으로 조합하는 패턴들입니다:

1. 서버 컴포넌트에서 클라이언트 컴포넌트로 데이터 전달

// app/products/page.tsx (서버 컴포넌트)
async function ProductsPage() {
  const products = await fetchProducts();

  return (
    <div>
      <ProductFilter products={products} />
    </div>
  );
}

// components/ProductFilter.tsx (클라이언트 컴포넌트)
'use client';

function ProductFilter({ products }) {
  const [filter, setFilter] = useState('all');
  const filtered = products.filter(p => filter === 'all' || p.category === filter);

  return (
    <div>
      <select onChange={(e) => setFilter(e.target.value)}>
        <option value="all">전체</option>
        <option value="electronics">전자제품</option>
      </select>
      {filtered.map(p => <ProductCard key={p.id} {...p} />)}
    </div>
  );
}

2. 클라이언트 컴포넌트에 서버 컴포넌트 삽입 (children 패턴)

// ClientTabs.tsx (클라이언트 컴포넌트)
'use client';

export function ClientTabs({ children }) {
  const [activeTab, setActiveTab] = useState(0);

  return (
    <div>
      <div className="tabs">
        <button onClick={() => setActiveTab(0)}>탭 1</button>
        <button onClick={() => setActiveTab(1)}>탭 2</button>
      </div>
      <div>{children[activeTab]}</div>
    </div>
  );
}

// page.tsx (서버 컴포넌트)
async function Page() {
  const data1 = await fetchData1();
  const data2 = await fetchData2();

  return (
    <ClientTabs>
      <ServerContent data={data1} />
      <ServerContent data={data2} />
    </ClientTabs>
  );
}

데이터 페칭 최적화

서버 컴포넌트에서 효율적인 데이터 페칭 전략:

import { cache } from 'react';

// 요청 메모이제이션
const getUser = cache(async (id: string) => {
  return await db.user.findUnique({ where: { id } });
});

// 여러 컴포넌트에서 호출해도 한 번만 실행됨
async function UserProfile({ userId }) {
  const user = await getUser(userId);
  return <div>{user.name}</div>;
}

async function UserPosts({ userId }) {
  const user = await getUser(userId); // 캐시된 결과 재사용
  const posts = await db.post.findMany({ where: { authorId: userId } });
  return <div>{posts.length} posts by {user.name}</div>;
}

스트리밍과 병렬 데이터 로딩

Suspense를 활용하여 사용자 경험을 개선합니다:

import { Suspense } from 'react';

async function SlowComponent() {
  const data = await slowQuery();
  return <div>{data}</div>;
}

async function FastComponent() {
  const data = await fastQuery();
  return <div>{data}</div>;
}

export default function Page() {
  return (
    <div>
      {/* 빠른 컴포넌트는 먼저 표시 */}
      <Suspense fallback={<Skeleton />}>
        <FastComponent />
      </Suspense>

      {/* 느린 컴포넌트는 준비되면 표시 */}
      <Suspense fallback={<Skeleton />}>
        <SlowComponent />
      </Suspense>
    </div>
  );
}

서버 액션 활용

서버 액션을 통해 API 라우트 없이 서버 로직을 실행할 수 있습니다:

// actions.ts
'use server';

export async function createPost(formData: FormData) {
  const title = formData.get('title') as string;
  const content = formData.get('content') as string;

  const post = await db.post.create({
    data: { title, content }
  });

  revalidatePath('/blog');
  redirect(`/blog/${post.id}`);
}

// CreatePostForm.tsx
'use client';

import { createPost } from './actions';

export function CreatePostForm() {
  return (
    <form action={createPost}>
      <input name="title" required />
      <textarea name="content" required />
      <button type="submit">작성</button>
    </form>
  );
}

활용 팁

  • 가능한 한 서버 컴포넌트를 사용하여 클라이언트 번들을 최소화하세요.
  • 클라이언트 컴포넌트는 트리의 가능한 한 아래쪽(리프)에 배치하세요.
  • 서버 컴포넌트에서는 시리얼라이즈 가능한 props만 전달하세요 (함수, Date 객체 등은 불가).
  • React.cache()를 활용하여 중복 요청을 방지하세요.
  • Suspense 경계를 전략적으로 배치하여 사용자 경험을 최적화하세요.

마무리

React Server Components는 웹 개발의 패러다임을 바꾸는 혁신적인 기술입니다. 서버와 클라이언트의 경계를 명확히 이해하고 적절히 활용하면 성능과 개발 경험을 모두 향상시킬 수 있습니다.