iBetter Books
수정

Ch 05. 무엇을 서버로, 무엇을 클라이언트로 — 선택 기준

실수하지 않는 방법

처음 Server Component를 접하면 가장 많이 하는 실수가 있습니다. 모든 컴포넌트에 "use client"를 붙이는 것입니다. "일단 클라이언트로 만들면 모든 기능을 쓸 수 있으니까"라는 생각에서 나오는 행동입니다.

하지만 그러면 Server Component의 장점(번들 크기 감소, 직접 데이터 접근, 보안)을 모두 잃습니다. 가능한 한 Server Component로 두고, 꼭 필요한 경우에만 Client Component로 만드는 것이 바람직합니다.

선택 기준 체크리스트

다음 질문에 하나라도 "예"라면 Client Component로 만들어야 합니다.

□ useState, useReducer, useRef 등 상태 관련 훅을 사용하나요?□ useEffect, useLayoutEffect를 사용하나요?□ onClick, onChange, onSubmit 등 이벤트 핸들러를 직접 달아야 하나요?□ window, document, localStorage, navigator를 사용하나요?□ useRouter, usePathname, useSearchParams를 사용하나요?□ 실시간으로 데이터가 바뀌는 UI인가요?

모두 "아니오"라면 Server Component로 유지합니다.

데이터 페칭 → Server Component

데이터를 가져오는 작업은 Server Component에서 합니다. async/await로 직접 데이터베이스나 API를 호출합니다.

// 파일: app/products/page.tsx
// Server Component - 데이터 페칭은 여기서
import { db } from '@/lib/db';
import AddToCartButton from './AddToCartButton';

export default async function ProductsPage() {
  const products = await db.product.findMany({
    where: { active: true },
    orderBy: { createdAt: 'desc' },
  });

  return (
    <div>
      {products.map((product) => (
        <div key={product.id}>
          <h2>{product.name}</h2>
          <p>{product.price.toLocaleString()}원</p>
          <AddToCartButton productId={product.id} />  {/* Client Component */}
        </div>
      ))}
    </div>
  );
}
```text

### 상호작용 → Client Component

버튼 클릭, 폼 입력, 토글 등 사용자 상호작용이 필요한 부분만 Client Component로 분리합니다.

```tsx
// 파일: app/products/AddToCartButton.tsx
'use client';

import { useState } from 'react';

export default function AddToCartButton({ productId }: { productId: number }) {
  const [adding, setAdding] = useState(false);
  const [added, setAdded] = useState(false);

  async function handleAddToCart() {
    setAdding(true);
    await fetch('/api/cart', {
      method: 'POST',
      body: JSON.stringify({ productId }),
    });
    setAdded(true);
    setAdding(false);
  }

  return (
    <button onClick={handleAddToCart} disabled={adding}>
      {adding ? '추가 중...' : added ? '장바구니에 추가됨' : '장바구니에 추가'}
    </button>
  );
}
```text

### 환경변수 접근 → Server Component

서버 전용 환경변수(`NEXT_PUBLIC_` 접두사 없는 것)는 Server Component에서만 접근합니다.

```tsx
// 파일: app/api-test/page.tsx
// Server Component
export default async function ApiTestPage() {
  // process.env.SECRET_KEY는 서버에서만 접근 가능
  const res = await fetch('https://api.example.com/data', {
    headers: {
      Authorization: `Bearer ${process.env.SECRET_KEY}`,
    },
  });
  const data = await res.json();
  return <div>{JSON.stringify(data)}</div>;
}
```text

`NEXT_PUBLIC_`으로 시작하는 환경변수만 클라이언트에서 접근할 수 있습니다.

### 브라우저 API → Client Component

`localStorage`, `window` 같은 것들은 서버에 없습니다. 이들을 사용한다면 반드시 Client Component여야 합니다.

```tsx
// 파일: app/components/ThemeToggle.tsx
'use client';

import { useState, useEffect } from 'react';

export default function ThemeToggle() {
  const [theme, setTheme] = useState<'light' | 'dark'>('light');

  useEffect(() => {
    // localStorage는 클라이언트에서만 접근 가능
    const saved = localStorage.getItem('theme') as 'light' | 'dark' | null;
    if (saved) setTheme(saved);
  }, []);

  function toggleTheme() {
    const next = theme === 'light' ? 'dark' : 'light';
    setTheme(next);
    localStorage.setItem('theme', next);
    document.documentElement.setAttribute('data-theme', next);
  }

  return (
    <button onClick={toggleTheme}>
      {theme === 'light' ? '다크 모드' : '라이트 모드'}
    </button>
  );
}

실무 패턴 요약

해야 할 일 어느 쪽 이유
데이터베이스 쿼리 Server 보안, 성능
API 호출 (비밀 키 포함) Server 키 노출 방지
정적 UI 렌더링 Server 번들 크기 절약
버튼 클릭 처리 Client 이벤트는 브라우저에서
폼 상태 관리 Client useState 필요
localStorage 접근 Client 브라우저 전용 API
실시간 데이터 폴링 Client setInterval, useEffect

다음 챕터에서는

다음 챕터에서는 Server Component와 Client Component 사이의 경계를 넘을 때 발생하는 제약, "직렬화"에 대해 이야기합니다. 함수나 클래스 인스턴스를 prop으로 넘기려 할 때 에러가 발생하는 이유를 이해합니다.

Ch 05. 무엇을 서버로, 무엇을 클라이언트로 — 선택 기준 — 소설처럼 읽는 Next.js | iBetter Books