Next.js App Router를 공부하다 보면 가장 자주 접하게 되는 개념이 Server Component와 Client Component이다.
처음에는 단순히
- Server Component = 서버에서 실행
- Client Component = 브라우저에서 실행
정도로 이해했는데, 실제로 코드를 작성하다 보면 useState는 왜 사용할 수 없는지, "use client"는 어디에 붙여야 하는지, 모든 컴포넌트를 Client Component로 만들면 안 되는지 등이 헷갈렸다.
이번 글에서는 두 컴포넌트의 차이와 각각 언제 사용하는지 정리해보려고 한다.
1. Server Component란?
Next.js의 App Router에서는 컴포넌트가 기본적으로 Server Component로 동작한다.
따라서 별도의 설정 없이 아래처럼 컴포넌트를 작성하면 Server Component가 된다.
export default function Page() {
return <h1>Hello Next.js</h1>;
}
Server Component는 서버 환경에서 렌더링되며, 브라우저에서 실행할 필요가 없는 JavaScript를 클라이언트로 보내지 않을 수 있다.
또한 서버에서 실행되기 때문에 데이터베이스나 서버 내부 리소스에 접근하는 로직을 작성할 수 있다.
예를 들어 다음처럼 서버에서 데이터를 조회할 수 있다.
export default async function Page() {
const res = await fetch("https://api.example.com/posts");
const posts = await res.json();
return (
{post.title}
))}
);
}
별도의 useEffect 없이 컴포넌트 내부에서 async/await를 사용해 데이터를 가져올 수 있다.
Server Component의 장점
- 서버에서 직접 데이터 조회 가능
- 데이터베이스에 직접 접근 가능
- API Key 등 민감한 정보를 클라이언트에 노출하지 않을 수 있음
- 클라이언트로 전달되는 JavaScript 양을 줄일 수 있음
- 데이터 조회 로직을 UI와 가까운 위치에서 작성할 수 있음
React 공식 문서에서도 Server Component는 빌드 시점이나 요청 시점 등 클라이언트 앱과 분리된 환경에서 실행될 수 있다고 설명하고 있다.
2. Server Component에서 할 수 없는 것
Server Component는 브라우저에 존재하는 컴포넌트가 아니기 때문에 사용자와 직접 상호작용하는 기능을 사용할 수 없다.
대표적으로 다음 기능을 사용할 수 없다.
useState
useEffect
onClick
onChange
window
localStorage
예를 들어 아래 코드는 App Router에서 오류가 발생한다.
import { useState } from "react";
export default function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
{count}
</button>
);
}
useState는 브라우저에서 상태를 유지하면서 다시 렌더링해야 하기 때문이다.
Server Component는 렌더링이 끝난 뒤 브라우저에서 상태를 유지하는 컴포넌트가 아니기 때문에 이런 상호작용을 처리할 수 없다.
이럴 때 사용하는 것이 Client Component이다.
3. Client Component란?
Client Component는 브라우저에서 사용자와 상호작용해야 하는 부분에 사용하는 컴포넌트이다.
Client Component로 만들기 위해 파일 최상단에 "use client"를 선언한다.
"use client";
import { useState } from "react";
export default function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
클릭 {count}
</button>
);
}
이제 해당 컴포넌트에서는 상태와 이벤트를 사용할 수 있다.
Client Component가 필요한 경우
다음과 같은 기능을 사용하는 경우 Client Component가 필요하다.
- useState
- useEffect
- useReducer
- 이벤트 핸들러
- onClick
- onChange
- onSubmit
- 브라우저 API
- window
- document
- localStorage
- 사용자 인터랙션
- 일부 클라이언트 전용 훅
즉,
사용자가 클릭하거나 입력하고 그 결과에 따라 화면이 변해야 한다면 Client Component가 필요한 경우가 많다.
4. "use client"는 정확히 무엇일까?
처음에는 "use client"를
이 컴포넌트를 클라이언트에서 렌더링해라
정도로 생각했다.
하지만 조금 더 정확히 보면 "use client"는 Server Component와 Client Component 사이의 경계(boundary)를 정의하는 역할을 한다.
예를 들어 다음과 같은 구조가 있다고 해보자.
Page
├─ Header
├─ ProductList
└─ LikeButton
Page, Header, ProductList는 사용자 상호작용이 필요하지 않다고 가정한다.
반면 LikeButton은 클릭할 때 상태가 바뀐다.
그렇다면 굳이 전체 페이지를 Client Component로 만들 필요가 없다.
// app/page.tsx
import LikeButton from "./LikeButton";
export default async function Page() {
const products = await getProducts();
return (
<>
<Header />
<ProductList products={products} />
<LikeButton />
</>
);
}
// LikeButton.tsx
"use client";
import { useState } from "react";
export default function LikeButton() {
const [liked, setLiked] = useState(false);
return (
<button onClick={() => setLiked(!liked)}>
{liked ? "좋아요 취소" : "좋아요"}
</button>
);
}
이 경우
Page → Server Component
Header → Server Component
ProductList → Server Component
LikeButton → Client Component
처럼 구성할 수 있다.
React 공식 문서에서는 "use client"가 선언된 모듈과 해당 모듈이 불러오는 의존성들이 클라이언트 영역에 포함된다고 설명한다.
5. 왜 모든 컴포넌트에 "use client"를 붙이면 안 될까?
처음에는
그냥 전부 Client Component로 만들면 편한 거 아닌가?
라는 생각이 들 수 있다.
실제로 그렇게 만들어도 동작하는 경우는 많다.
하지만 이렇게 하면 Next.js App Router가 Server Component를 제공하는 장점을 제대로 활용하기 어렵다.
예를 들어 페이지 전체에 "use client"를 선언하면 브라우저에서 처리해야 하는 JavaScript가 많아질 수 있다.
"use client";
export default function Page() {
// ...
}
그리고 이 페이지에서 불러오는 컴포넌트들도 클라이언트 영역에 포함될 수 있다.
따라서 일반적으로는
가능하면 Server Component를 유지하고, 사용자 상호작용이 필요한 작은 부분만 Client Component로 분리하는 방식
이 좋다.
6. Server Component와 Client Component 비교
구분Server ComponentClient Component
| 기본 여부 | App Router 기본값 | "use client" 필요 |
| 실행 환경 | 서버 | 브라우저 + Next.js의 사전 렌더링 과정 |
| useState | 사용 불가 | 사용 가능 |
| useEffect | 사용 불가 | 사용 가능 |
| 이벤트 처리 | 불가 | 가능 |
| DB 직접 접근 | 가능 | 불가 |
| 서버 비밀 정보 접근 | 가능 | 노출 위험 |
| 브라우저 API | 사용 불가 | 사용 가능 |
| JS 번들 영향 | 클라이언트 JS 감소에 유리 | 클라이언트 JS에 포함 |
여기서 주의해야 할 점은 Client Component라고 해서 처음부터 무조건 브라우저에서만 HTML이 만들어지는 것은 아니라는 것이다.
Next.js는 초기 페이지 요청 시 Client Component도 서버 측 렌더링 과정에 참여할 수 있다.
따라서
Client Component = CSR
이라고 단순하게 동일시하면 정확하지 않다.
Client Component라는 표현은 브라우저에서 실행되어야 하는 React 코드의 경계에 더 가깝다.
7. Server Component와 Client Component는 어떻게 나누면 될까?
컴포넌트를 나눌 때 다음과 같은 기준으로 생각하면 이해하기 쉽다.
Server Component가 적합한 경우
데이터 조회
DB 접근
정적인 UI
게시글 목록
상품 목록
서버 내부 정보 사용
예를 들어 상품 목록 자체는 Server Component로 만들 수 있다.
export default async function ProductList() {
const products = await getProducts();
return (
<ul>
{products.map((product) => (
<li key={product.id}>
{product.name}
</li>
))}
</ul>
);
}
Client Component가 적합한 경우
버튼 클릭
폼 입력
모달
드롭다운
탭
검색 필터
실시간 상태 변경
예를 들어 검색 입력창은 사용자가 입력한 값을 상태로 관리해야 하므로 Client Component가 적합하다.
"use client";
import { useState } from "react";
export default function SearchInput() {
const [keyword, setKeyword] = useState("");
return (
<input
value={keyword}
onChange={(e) => setKeyword(e.target.value)}
/>
);
}
8. 실제 페이지에서는 둘을 함께 사용한다
Server Component와 Client Component 중 하나만 선택해야 하는 것은 아니다.
실제로는 한 페이지 안에서 두 방식을 조합해 사용하는 경우가 많다.
예를 들어 쇼핑몰 상품 페이지를 생각해보면
ProductPage
│
├─ ProductInfo
├─ ProductDescription
├─ ReviewList
│
├─ QuantitySelector
└─ AddToCartButton
다음과 같이 나눌 수 있다.
ProductPage Server Component
ProductInfo Server Component
ProductDescription Server Component
ReviewList Server Component
QuantitySelector Client Component
AddToCartButton Client Component
상품 정보나 리뷰 목록은 서버에서 가져와 출력하면 된다.
반면 수량 선택과 장바구니 버튼은 사용자의 클릭이나 상태 변경이 필요하므로 Client Component로 만들 수 있다.
결국 핵심은
페이지 전체를 Server 또는 Client 중 하나로 선택하는 것이 아니라, 컴포넌트별 역할에 따라 서버와 클라이언트의 경계를 나누는 것이다.
9. "use server"를 붙이면 Server Component일까?
여기서 헷갈리기 쉬운 부분이 하나 있다.
"use server";
라는 문법도 존재하기 때문에
"use client" → Client Component
"use server" → Server Component
라고 생각하기 쉽다.
하지만 그렇지 않다.
Server Component에는 별도의 "use server" 선언이 필요하지 않다.
App Router에서는 기본적으로 Server Component이기 때문이다.
"use server"는 Server Function을 정의하기 위한 지시어이다.
따라서 다음처럼 기억하는 편이 좋다.
Server Component
→ 기본값
Client Component
→ "use client"
Server Function
→ "use server"
정리
Next.js App Router에서는 Server Component가 기본값이다.
Server Component는 서버에서 데이터 조회나 렌더링을 처리할 수 있고, 클라이언트에 전달하는 JavaScript를 줄이는 데 도움이 된다.
반대로 useState, useEffect, onClick처럼 브라우저에서 사용자와 상호작용해야 하는 기능은 Client Component에서 처리해야 한다.
따라서 모든 컴포넌트를 Client Component로 만드는 것보다
Server Component
↓
대부분의 UI와 데이터 처리
+
Client Component
↓
상호작용이 필요한 부분
처럼 필요한 부분만 Client Component로 분리하는 것이 Next.js App Router를 사용하는 핵심이라고 이해했다.
처음에는 Server Component와 Client Component를 단순히 SSR과 CSR의 차이라고 생각했지만, 실제로는 컴포넌트 트리에서 서버와 클라이언트의 실행 경계를 어떻게 나눌 것인가에 대한 개념에 더 가깝다.
참고
- Next.js 공식 문서 - Server and Client Components
- React 공식 문서 - Server Components
- React 공식 문서 - 'use client'



