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으로 넘기려 할 때 에러가 발생하는 이유를 이해합니다.