Next.js 13 버전 이상(App Router)을 도입해 개발을 시작하면 가장 먼저 마주치는 거대한 벽이 있습니다. 바로 컴포넌트 파일 맨 상단에 습관처럼 붙이게 되는 'use client'; 선언입니다.
저 역시 초기에는 컴포넌트마다 에러(예: useState can not be used in Server Component)가 뜰 때마다 귀찮다는 이유로 파일마다 상단에 무지성으로 'use client';를 붙여가며 개발했습니다.
하지만 나중에 프로젝트 번들 사이즈(Bundle Size)를 측정해 보니, 서버에서 가볍게 처리되어야 할 정적 UI들까지 전부 클라이언트(브라우저)로 자바스크립트 코드가 내려가면서 초기 로딩 속도가 뚝 떨어지는 심각한 부작용을 마주쳤습니다.
오늘은 서버 컴포넌트(RSC)와 클라이언트 컴포넌트(RCC)의 근본적인 차이점과, 실무에서 어떤 기준으로 분리해야 하는지 명확한 가이드를 공유해 보려고 합니다.
1. 내가 마주친 문제: "왜 모든 컴포넌트에 'use client'를 붙이면 안 될까?"
Next.js App Router의 기본 원칙은 모든 컴포넌트가 기본적으로 서버 컴포넌트(RSC)라는 점입니다.
- 서버 컴포넌트 (RSC): HTML 렌더링과 데이터베이스(DB) 조회를 서버에서 끝내고, 가벼운 HTML 결과물만 브라우저로 보냅니다. (자바스크립트 번들 사이즈 0KB!)
- 클라이언트 컴포넌트 (RCC): 'use client';를 선언하는 순간, 해당 컴포넌트와 그 하위에 딸린 모든 자식 코드가 브라우저로 다운로드되어 실행됩니다.
만약 페이지 전체를 감싸는 최상단 부모나 단순 마크업 컴포넌트에 무심코 클라이언트 선언을 해버리면, 서버의 강력한 성능 이점을 전혀 살리지 못하고 브라우저 과부하를 유발하게 됩니다.
2. 언제 '서버 컴포넌트(RSC)'를 써야 할까? (기본값)
대부분의 일반적인 컴포넌트는 서버 컴포넌트로 두는 것이 가장 이상적입니다.
- 가장 적합한 곳: 백엔드 API나 DB에서 데이터를 직접 가져와야 하는 경우, 민감한 시크릿 키(API Key)를 다뤄야 하는 경우, 무거운 서드파티 라이브러리를 서버에서 미리 렌더링하고 싶을 때.
// ⭕ 서버 컴포넌트 예시 (async/await로 서버에서 직접 데이터 페칭 가능)
// 'use client' 선언이 없음 = 기본 서버 컴포넌트
export default async function ProductDetailPage({ params }: { params: { id: string } }) {
// 서버 내부에서 직접 안전하게 DB 또는 API 호출
const res = await fetch(`https://api.example.com/products/${params.id}`);
const product = await res.json();
return (
<div className="p-6">
<h1 className="text-xl font-bold">{product.title}</h1>
<p className="text-slate-400">{product.description}</p>
</div>
);
}
클라이언트 브라우저가 자바스크립트를 다운로드하고 실행할 필요 없이, 서버가 이미 완성된 HTML을 보내주므로 SEO 최적화와 초기 로딩 속도(FCP)가 극적으로 빨라집니다.

3. 언제 '클라이언트 컴포넌트(RCC)'를 써야 할까?
브라우저의 동적 기능이나 사용자 인터랙션이 반드시 필요한 순간에만 제한적으로 'use client';를 붙여야 합니다.
- 가장 적합한 곳: useState, useEffect, useContext 등의 훅을 써야 할 때, onClick, onChange 같은 브라우저 이벤트 리스너가 필요할 때, 브라우저 전용 API(localStorage, window)를 쓸 때.
'use client'; // 👈 브라우저 상호작용이 필요할 때만 명시!
import { useState } from 'react';
export default function CounterButton() {
const [count, setCount] = useState<number>(0);
return (
<button
onClick={() => setCount((prev) => prev + 1)}
className="px-4 py-2 bg-indigo-600 text-white rounded-xl"
>
클릭 횟수: {count}
</button>
);
}
4. 실무 아키텍처 패턴: "서버 컴포넌트 안에 클라이언트 컴포넌트 배치하기"
실무에서 가장 권장되는 패턴은 큰 틀(뼈대와 데이터 페칭)은 서버 컴포넌트로 만들고, 클릭 이벤트가 발생하는 아주 작은 버튼 조각만 클라이언트 컴포넌트로 분리하는 것입니다.
// 1. 부모 페이지 (서버 컴포넌트 - 데이터 로딩 담당)
import CounterButton from './CounterButton';
export default async function Page() {
const data = await fetch('...').then((res) => res.json());
return (
<div>
<h1>{data.title}</h1>
{/* 2. 인터랙션이 필요한 작은 조각만 클라이언트 컴포넌트로 주입 */}
<CounterButton initialCount={data.count} />
</div>
);
}
이렇게 하면 전체 페이지의 90%는 서버에서 가볍게 렌더링되고, 오직 버튼 컴포넌트만 클라이언트에서 작동하여 번들 크기를 최소화할 수 있습니다.

5. 결론 및 느낀 점
Next.js App Router를 다룰 때 'use client';는 마법의 열쇠가 아니라 성능을 깎아먹을 수 있는 양날의 검입니다.
- 데이터 조회와 정적 마크업은 기본 서버 컴포넌트(RSC)로 유지할 것
- 상태(State)나 이벤트(Event)가 필요한 최소한의 하위 조각에만 클라이언트 컴포넌트(RCC)를 적용할 것
이 원칙을 지키는 것만으로도 웹 사이트의 번들 사이즈를 대폭 줄이고 구글 성능 점수를 크게 끌어올릴 수 있습니다!
'프론트엔드' 카테고리의 다른 글
| [CSS] 이미지가 로딩되면서 버튼 위치가 쿵 내려앉는다? CLS(누적 레이아웃 이동) 해결기 (0) | 2026.08.28 |
|---|---|
| [Web Storage] 새로고침하면 다 날아간다고? LocalStorage와 SessionStorage로 브라우저 데이터 유지한 후기 (0) | 2026.08.24 |
| [State] Props Drilling의 고통: Zustand로 10분 만에 끝내는 전역 상태 관리 도입기 (0) | 2026.08.21 |
| [VS Code] 프론트엔드 개발 생산성을 2배로 올려주는 필수 확장 프로그램 5선 (0) | 2026.08.11 |
| [React] useState와 useEffect 실무 적용 중 겪었던 3가지 초보적 실수와 해결책 (0) | 2026.07.28 |