목록으로
React

React Server Components 이해하기

서버 컴포넌트의 개념과 클라이언트 컴포넌트와의 차이점. 언제 어떤 것을 사용해야 하는지 알아봅니다.

React Server Components(RSC)는 React 18에서 도입된 새로운 패러다임입니다. Next.js App Router에서 기본으로 사용됩니다.

서버 컴포넌트란

서버에서만 렌더링되는 컴포넌트입니다. JavaScript 번들에 포함되지 않습니다.

// 서버 컴포넌트 (기본값)
async function ProductList() {
  // 서버에서 직접 데이터베이스 접근 가능
  const products = await db.product.findMany();

  return (
    <ul>
      {products.map(product => (
        <li key={product.id}>{product.name}</li>
      ))}
    </ul>
  );
}

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

서버 컴포넌트
✓ async/await 직접 사용
✓ 데이터베이스 직접 접근
✓ 파일 시스템 접근
✓ API 키 등 민감한 정보 사용
✓ 번들 크기에 영향 없음
✗ useState, useEffect 사용 불가
✗ 이벤트 핸들러 사용 불가
✗ 브라우저 API 사용 불가

클라이언트 컴포넌트 ('use client')
✓ useState, useEffect 사용
✓ 이벤트 핸들러
✓ 브라우저 API (window, document)
✓ 사용자 상호작용
✗ 번들 크기 증가

클라이언트 컴포넌트 선언

파일 최상단에 'use client' 지시어를 추가합니다.

'use client';

import { useState } from 'react';

function Counter() {
  const [count, setCount] = useState(0);

  return (
    <button onClick={() => setCount(count + 1)}>
      클릭: {count}
    </button>
  );
}

컴포넌트 조합 패턴

서버 컴포넌트 안에 클라이언트 컴포넌트를 넣을 수 있습니다.

// 서버 컴포넌트
async function ProductPage({ id }) {
  const product = await getProduct(id);

  return (
    <div>
      <h1>{product.name}</h1>
      <p>{product.description}</p>

      {/* 클라이언트 컴포넌트 */}
      <AddToCartButton productId={id} />
    </div>
  );
}
'use client';

function AddToCartButton({ productId }) {
  const [loading, setLoading] = useState(false);

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

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

주의: 클라이언트 컴포넌트에서 서버 컴포넌트 import

'use client';

// 이렇게 하면 ServerComponent도 클라이언트 번들에 포함됨
import ServerComponent from './ServerComponent';

function ClientComponent() {
  return <ServerComponent />; // 서버 컴포넌트가 아닌 일반 컴포넌트가 됨
}

대신 children prop을 사용합니다.

// page.tsx (서버 컴포넌트)
import ClientWrapper from './ClientWrapper';
import ServerContent from './ServerContent';

function Page() {
  return (
    <ClientWrapper>
      <ServerContent />
    </ClientWrapper>
  );
}

데이터 페칭 패턴

// 서버 컴포넌트에서 직접 페칭
async function UserProfile({ userId }) {
  const user = await fetch(`/api/users/${userId}`).then(r => r.json());

  return (
    <div>
      <h1>{user.name}</h1>
      <UserPosts userId={userId} />
    </div>
  );
}

async function UserPosts({ userId }) {
  const posts = await fetch(`/api/users/${userId}/posts`).then(r => r.json());

  return (
    <ul>
      {posts.map(post => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  );
}

병렬로 데이터를 가져오려면 Promise.all을 사용합니다.

async function Dashboard() {
  const [user, posts, analytics] = await Promise.all([
    getUser(),
    getPosts(),
    getAnalytics()
  ]);

  return (
    <div>
      <UserInfo user={user} />
      <PostList posts={posts} />
      <Analytics data={analytics} />
    </div>
  );
}

어떤 것을 사용해야 할까

서버 컴포넌트 사용
- 데이터 표시만 하는 컴포넌트
- 데이터베이스/API에서 데이터 페칭
- 민감한 로직 (API 키 등)
- 큰 의존성 사용 (마크다운 파서 등)

클라이언트 컴포넌트 사용
- 상호작용이 필요한 컴포넌트
- useState, useEffect 필요
- 이벤트 핸들러 (onClick, onChange)
- 브라우저 API 사용
- 서드파티 라이브러리 (대부분 클라이언트용)

마무리

서버 컴포넌트는 성능과 보안에 큰 이점이 있습니다. 클라이언트에 전송되는 JavaScript 양이 줄어들고, 민감한 로직을 서버에 둘 수 있습니다.

기본은 서버 컴포넌트, 상호작용이 필요한 부분만 클라이언트 컴포넌트로 만드는 것이 좋은 전략입니다.