티스토리 뷰

웹 개발 생태계에서 Next.js App Router의 등장은 컴포넌트를 바라보는 패러다임을 완전히 바꾸어 놓았습니다. 과거에는 페이지 전체를 서버에서 렌더링할지, 클라이언트에서 렌더링할지 고민했다면 이제는 하나의 화면 안에서도 컴포넌트 단위로 그 역할을 세밀하게 분담할 수 있게 되었습니다.
이 글에서는 서버 컴포넌트와 클라이언트 컴포넌트의 본질적인 차이를 파헤치고, 실제 프로젝트에서 이들을 어떻게 조화롭게 배치해야 최적의 성능을 낼 수 있는지 깊이 있게 다뤄보겠습니다.
서버 컴포넌트(Server) vs 클라이언트 컴포넌트(Client)
Next.js App Router에서 모든 컴포넌트는 기본적으로 **서버 컴포넌트(Server Components)**입니다. 필요할 때만 선별적으로 **클라이언트 컴포넌트(Client Components)**를 사용하죠.
1. 작동 원리의 비유: 주방과 서빙 테이플
이해를 돕기 위해 레스토랑의 시스템에 비유해 보겠습니다.
- 서버 컴포넌트 (주방): 손님에게 보이지 않는 곳에서 재료(데이터)를 손질하고 요리를 완성하는 곳입니다. 무거운 칼이나 불(DB 접근, 보안 키)을 안전하게 다루며, 완성된 접시(HTML)만 손님에게 내보냅니다.
- 클라이언트 컴포넌트 (서빙 테이블): 손님 바로 앞에서 소스를 뿌려주거나 스테이크를 썰어주는 곳입니다. 손님의 반응(클릭, 입력)에 즉각 대응해야 하는 인터랙션이 일어나는 공간입니다.
2. 왜 나누어야 하는가? (Deep Dive)
- 성능 최적화: 서버 컴포넌트는 결과물인 HTML만 브라우저로 보냅니다. 자바스크립트 번들 크기가 획기적으로 줄어들어 초기 로딩 속도가 빨라집니다.
- 보안: API 키나 데이터베이스 쿼리 로직이 클라이언트로 노출될 염려가 없습니다.
- UX(사용자 경험): 클라이언트 컴포넌트는 useState나 useEffect 같은 훅을 사용하여 실시간 피드백을 제공하므로, 앱처럼 매끄러운 경험을 선사합니다.
실전 예제: 이커머스 상품 상세 페이지
단순한 이론보다는 실제 비즈니스 로직에 적용해 봅시다. 상품 정보를 불러오는 부분은 서버에서, '장바구니 담기'와 같은 버튼 클릭은 클라이언트에서 처리하는 구조입니다.
1. 서버 컴포넌트: 상품 정보 로드 (ProductDetail.tsx)
// 기본적으로 서버 컴포넌트입니다.
// async/await를 사용하여 직접 DB나 API에서 데이터를 가져올 수 있습니다.
import AddToCartButton from './AddToCartButton';
export default async function ProductDetail({ productId }: { productId: string }) {
// 1. 서버에서 직접 데이터를 페칭 (보안상 안전하고 빠름)
const response = await fetch(`https://api.example.com/products/${productId}`);
const product = await response.json();
return (
{product.name}
{product.description}
${product.price} {/* 2. 인터랙션이 필요한 부분만 클라이언트 컴포넌트로 분리 */} );
}
2. 클라이언트 컴포넌트: 장바구니 버튼 (AddToCartButton.tsx)
'use client'; // 클라이언트 컴포넌트임을 명시하는 지시어
import { useState } from 'react';
export default function AddToCartButton({ productId }: { productId: string }) {
const [isAdded, setIsAdded] = useState(false);
const handleAddToCart = () => {
// 장바구니 담기 로직 (브라우저 이벤트 처리)
console.log(`${productId}번 상품이 장바구니에 담겼습니다.`);
setIsAdded(true);
};
return (
<button
onClick={handleAddToCart}
className={isAdded ? "bg-green-500" : "bg-blue-500"}
>
{isAdded ? "담기 완료!" : "장바구니 담기"}
</button>
);
}
트러블슈팅: 자주 겪는 실수와 해결책
1. "Server Component에서 hook을 사용할 수 없습니다"
가장 흔한 에러입니다. useState나 useEffect를 사용하려는데 'use client'를 작성하지 않았을 때 발생합니다.
- Tip: 데이터 로직과 UI 로직을 분리하세요. 데이터를 가져오는 상위 부모는 서버로, 상태 변화가 필요한 하위 자식은 클라이언트로 쪼개는 것이 정석입니다.
2. 서버 컴포넌트에서 클라이언트 컴포넌트로 함수 전달
서버 컴포넌트에서 정의한 함수를 클라이언트 컴포넌트의 props로 직접 넘길 수 없습니다. 함수는 직렬화(Serialization)가 불가능하기 때문입니다.
- Tip: 함수 대신 데이터(ID 등)만 넘기고, 클라이언트 컴포넌트 내부에서 처리하거나 Server Actions를 활용하세요.
장단점 및 고려사항 (Trade-offs)
| 구분 | 서버 컴포넌트 (Server) | 클라이언트 컴포넌트 (Client) |
| 데이터 페칭 | 매우 빠름 (서버-DB 직접 연결) | 상대적으로 느림 (네트워크 요청 필요) |
| 번들 사이즈 | 0 (브라우저로 JS 안 보냄) | 컴포넌트 크기만큼 JS 포함 |
| 인터랙션 | 불가능 (정적 HTML) | 가능 (클릭, 타이핑, 상태 관리) |
| 사용 시점 | 데이터 로드, 레이아웃 구성 시 | 폼 입력, 차트 애니메이션, 브라우저 API 사용 시 |
가장 중요한 원칙: 최대한 서버 컴포넌트를 먼저 고려하고, 인터랙션이 반드시 필요한 지점에서만 클라이언트 컴포넌트를 사용하세요. 이를 "클라이언트 컴포넌트를 잎(Leaf) 노드로 밀어내기" 전략이라고 부릅니다.
결론 및 제언
서버와 클라이언트 컴포넌트의 구분은 단순히 기술적인 선택이 아니라, 사용자에게 더 빠른 화면을 보여주면서도 풍부한 기능을 제공하기 위한 설계의 핵심입니다.
처음에는 경계선을 나누는 것이 어색할 수 있지만, "이 데이터가 사용자 상호작용 없이도 보여질 수 있는가?"를 자문해 보세요. 답이 'Yes'라면 그것은 서버 컴포넌트의 몫입니다.
'Frontend > Next.js' 카테고리의 다른 글
| Loading UI와 Suspense를 활용한 사용자 경험 개선 (0) | 2026.03.12 |
|---|---|
| page.js에서 데이터 페칭(Fetching)과 캐싱 전략 (0) | 2026.03.12 |
| Link 컴포넌트와 프로그래밍 방식의 페이지 이동 (useRouter) (0) | 2026.03.12 |
| Layout과 Template을 활용한 공통 UI 설계 (0) | 2026.03.12 |
| App Router 기반의 파일 시스템 라우팅 이해 (0) | 2026.03.12 |
- Total
- Today
- Yesterday
- HBM
- CSS
- AI
- SSR
- MSA
- 협력
- on-device ai
- 웹기초
- 카카오
- It용어
- 엣지컴퓨팅
- Nextjs
- 구글
- HTML
- sLLM
- LLM
- prompt engineering
- Rag
- Javascript
- CSR
- TypeScript
- react
- 멀티모달
- 스마트안경
- java
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | |||
| 5 | 6 | 7 | 8 | 9 | 10 | 11 |
| 12 | 13 | 14 | 15 | 16 | 17 | 18 |
| 19 | 20 | 21 | 22 | 23 | 24 | 25 |
| 26 | 27 | 28 | 29 | 30 |