티스토리 뷰
1. 기본 개념
Next.js에서는 레이아웃과 페이지가 기본적으로 서버 컴포넌트로 처리된다. 서버 컴포넌트는 서버에서 데이터를 가져오고 UI의 일부를 렌더링하며, 필요에 따라 결과를 캐시하거나 클라이언트로 스트리밍할 수도 있다.
반면, 상호작용이나 브라우저 API가 필요한 경우에는 클라이언트 컴포넌트(use client)를 사용하여 기능을 추가할 수 있다.
즉, 서버 컴포넌트는 기본적으로 작동하는 리액트 서버 컴포넌트(React Server Component, RSC)를 기반으로 하고, 클라이언트 컴포넌트는 Next.js에서 use client 환경을 사용하는 컴포넌트를 의미한다.
2. 서버 및 클라이언트 컴포넌트의 작동 방식
서버와 클라이언트 환경
서버 및 클라이언트 컴포넌트의 작동방식을 이해하기 위해 두 가지 개념을 기억하자.
- 애플리케이션 코드를 실행할 수 있는 환경: 서버와 클라이언트
- 서버와 클라이언트 코드를 분리하는 네트워크 경계(network boundary)

서버(Server)
- 애플리케이션 코드를 저장하고, 클라이언트 요청을 받아 계산을 수행한 뒤 응답을 반환하는 데이터 센터의 컴퓨터
- 렌더링과 데이터 페칭을 서버로 옮기면 클라이언트로 전송되는 코드 양을 줄여 성능 향상 가능.(ex. SSG)
클라이언트(Client)
- 사용자의 브라우저를 의미
- 서버로부터 받은 응답을 상호작용 가능한 UI로 변환
서버와 클라이언트 각각의 환경에 적합한 코드를 작성해야 하며, 모든 코드를 동일하게 작성할 수는 없다. 예를 들어 데이터 페칭은 서버가, DOM 업데이트나 이벤트 처리는 클라이언트가 담당한다.
네트워크 경계(network boundary)와 렌더링 과정
네트워크 경계(network boundary)는 서로 다른 환경을 구분하는 개념적 경계이다.
React에서는 컴포넌트 트리에서 네트워크 경계를 어디에 배치할지 선택할 수 있다. 예를 들어, 서버에서 데이터를 가져와 사용자 게시물을 렌더링한 다음(서버 컴포넌트 사용), LikeButton클라이언트 컴포넌트를 사용하여 클라이언트에서 각 게시물의 상호작용을 렌더링할 수 있다.
마찬가지로 Nav컴포넌트를 서버에서 렌더링하여 여러 페이지에서 공유되도록 만들 수 있지만, 링크의 활성 상태를 표시하려는 경우 Links 목록을 클라이언트에서 렌더링할 수 있다.

내부적으로 구성 요소는 두 개의 모듈 그래프로 나뉜다. 서버 모듈 그래프(또는 트리)에는 서버에서 렌더링되는 모든 서버 구성 요소가 포함되고, 클라이언트 모듈 그래프(또는 트리) 에는 모든 클라이언트 구성 요소가 포함된다.*4번 항목 참조
서버 컴포넌트가 렌더링된 후, React 서버 컴포넌트 페이로드(RSC) 라는 특수 데이터 형식이 클라이언트로 전송된다. RSC 페이로드에는 다음이 포함된다.
- 서버 컴포넌트의 렌더링된 결과
- 클라이언트 컴포넌트를 렌더링해야 하는 Placeholders (or holes) 와 해당 JavaScript 파일에 대한 참조
React는 이 정보를 사용하여 서버와 클라이언트 구성 요소를 통합하고 클라이언트의 DOM을 업데이트한다.
3. 실습: 클라이언트 컴포넌트 만들기
실제로 서버 컴포넌트에서 클라이언트 컴포넌트로 분리하는 과정을 단계별로 따라해보자.
🚨 문제 상황
처음에 모든 코드를 한 파일에 작성했더니 오류가 발생
// app/page.js - 오류 발생!
import { useState } from 'react';
export default function HomePage() {
// ❌ Error: useState는 서버 컴포넌트에서 사용할 수 없습니다
const [likes, setLikes] = useState(0);
const names = ['Ada Lovelace', 'Grace Hopper', 'Margaret Hamilton'];
function handleClick() {
setLikes(likes + 1);
}
return (
<div>
<h1>개발. 미리보기. 배포.</h1>
<ul>
{names.map((name) => (
<li key={name}>{name}</li>
))}
</ul>
{/* ❌ 서버 컴포넌트에서는 onClick 이벤트 처리 불가 */}
<button onClick={handleClick}>좋아요 ({likes})</button>
</div>
);
}
오류 메시지: You're importing a component that needs useState. It only works in a Client Component
🔧 해결 과정
1단계: 새 파일 생성
상호작용이 필요한 부분을 별도 파일로 분리한다.
// app/like-button.js - 새 파일 생성
export default function LikeButton() {
// 아직 비어있는 상태
}
2단계: 기능 이동
기존 page.js에서 상호작용 관련 코드를 모두 이동한다.
// app/like-button.js
export default function LikeButton() {
// handleClick 함수 이동
function handleClick() {
setLikes(likes + 1);
}
// 버튼 JSX 이동
return <button onClick={handleClick}>좋아요 ({likes})</button>;
}
3단계: 상태와 import 추가
// app/like-button.js
import { useState } from 'react'; // React hooks import
export default function LikeButton() {
const [likes, setLikes] = useState(0); // 상태 관리 추가
function handleClick() {
setLikes(likes + 1);
}
return <button onClick={handleClick}>좋아요 ({likes})</button>;
}
4단계: 클라이언트 컴포넌트로 지정
// app/like-button.js
'use client'; // 🎯 이 한 줄이 핵심!
import { useState } from 'react';
export default function LikeButton() {
const [likes, setLikes] = useState(0);
function handleClick() {
setLikes(likes + 1);
}
return <button onClick={handleClick}>좋아요 ({likes})</button>;
}
5단계: 서버 컴포넌트에서 사용
// app/page.js - 깔끔해진 서버 컴포넌트
import LikeButton from './like-button'; // 클라이언트 컴포넌트 import
function Header({ title }) {
return <h1>{title ? title : 'Default title'}</h1>;
}
export default function HomePage() {
const names = ['Ada Lovelace', 'Grace Hopper', 'Margaret Hamilton'];
return (
<div>
<Header title="개발. 미리보기. 배포." />
<ul>
{names.map((name) => (
<li key={name}>{name}</li>
))}
</ul>
{/* ✅ 클라이언트 컴포넌트 사용 */}
<LikeButton />
</div>
);
}
결과 확인
- 정적 콘텐츠는 서버에서 렌더링
- 상호작용은 클라이언트에서 처리
- 파일을 저장하면 브라우저가 자동으로 업데이트
4. 'use client' 분석
https://react.dev/reference/rsc/use-client
앞서 클라이언트 컴포넌트와 서버 컴포넌트를 살펴봤지만, 헷갈리는 점이 분명 있을 것이다. 이를테면 클라이언트 트리와 서버 컴포넌트 트리가 구분되었다고 했는데, 예시 트리에서는 왜 서버와 섞여 있는지? 리액트의 'use client'를 분석함으로써 클라이언트 컴포넌트를 좀 더 명확하게 알아보자. 그에 대한 해답이 될 수 있다.
기본 사용법
'use client'는 해당 모듈과 그의 모든 종속성을 클라이언트 코드로 표시하는 지시어이다
서버 컴포넌트와 클라이언트 컴포넌트의 경계
서버 컴포넌트에서 'use client'으로 표시된 파일을 가져올 때, 호환 번들러는 모듈 가져오기(import module)를 서버 실행 코드와 클라이언트 실행 코드 간의 경계로 간주한다.
// page.tsx (Server Component)
import Button from './Button';
export default function Page() {
return <Button />;
}
// Button.tsx (Client Component)
'use client';
export default function Button() {
return <button onClick={() => alert('clicked!')}>Click</button>;
}
여기서 Page는 서버 컴포넌트이고 Button은 클라이언트 컴포넌트지만, 서버 컴포넌트 안에서 클라이언트 컴포넌트를 import하고 있다.
이때 번들러는 Page는 서버에서, Button은 클라이언트에서 실행돼야 함을 구분하여 import Button from './Button'; 지점을 서버와 클라이언트의 경계로 취급한다. 서버는 Button을 직접 실행하지 않고, 클라이언트에서 렌더링해야 한다는 지시만 전달한다.
'use client' 사용 주의사항
✅ 올바른 사용법
- 최상단 선언 구문
'use client';
// 또는
"use client";
- 주석은 위에 있을 수 있다.
// 이것은 클라이언트 컴포넌트입니다
'use client';
import React from 'react';
❌ 잘못된 사용법
import React from 'react';
'use client'; // import 아래에 있으면 안됨
`use client`; // 백틱 사용 불가
function MyComponent() {
'use client'; // 함수 내부에 있으면 안됨
}
'use client'는 어떻게 클라이언트 코드를 표시하는가
React 앱에서는 컴포넌트를 보통 별도의 파일이나 모듈로 분리한다.
React Server Components를 사용하는 앱의 경우 기본적으로 서버에서 렌더링된다. 'use client'는 모듈 의존성 트리에서 서버와 클라이언트의 경계를 만들고, 사실상 클라이언트 모듈의 하위 트리를 형성한다.
이를 더 잘 설명하기 위해 다음 React Server Components 앱을 살펴보자.
이 예시 앱의 모듈 의존성 트리에서, InspirationGenerator.js에 있는 'use client' 지시문은 해당 모듈과 그에 연쇄적으로 의존하는 모든 모듈을 클라이언트 모듈로 표시한다. 따라서 InspirationGenerator.js를 시작으로 한 하위 트리는 이제 클라이언트 모듈로 표시된다.

렌더링 과정에서 프레임워크는 루트 컴포넌트를 서버에서 렌더링하고 렌더 트리를 따라 내려가며, 클라이언트로 표시된 코드에서 import된 부분은 실행(evaluating)하지 않고 건너뛴다.
서버에서 렌더링된 트리의 부분은 클라이언트로 전송되고, 클라이언트는 필요한 클라이언트 전용 코드를 내려받은 후 나머지 트리의 렌더링을 완성한다.

다음의 정의를 소개한다.
- 클라이언트 컴포넌트는 클라이언트에서 렌더링되는 렌더 트리의 컴포넌트
- 서버 컴포넌트는 서버에서 렌더링되는 렌더 트리의 컴포넌트
예제 앱을 살펴보면, App, FancyText, Copyright는 모두 서버에서 렌더링되며 서버 컴포넌트로 간주된다. InspirationGenerator.js와 그 연쇄 의존성은 클라이언트 코드로 표시되어 있으므로, InspirationGenerator와 그 자식 컴포넌트 FancyText는 클라이언트 컴포넌트로 분류된다.
5. 헷갈리는 개념 체크
서버 컴포넌트인 동시에 클라이언트 컴포넌트?
위의 정의에 따르면, FancyText는 서버 컴포넌트이자 클라이언트 컴포넌트인데, 어떻게 그럴 수 있을까?
먼저, “컴포넌트(component)”라는 용어 자체가 엄밀하지 않다는 점을 명확히 할 필요가 있다. 일반적으로 “컴포넌트”는 두 가지 의미로 이해될 수 있다:
컴포넌트 정의(component definition)
대부분 함수로 정의된다.
// 컴포넌트 정의 예시
function MyComponent() {
return <p>My Component</p>
}
컴포넌트 사용(법)(component usage)
정의된 컴포넌트를 import하여 실제 렌더링에 사용하는 경우.
import MyComponent from './MyComponent';
function App() {
// 컴포넌트 사용 예시
return <MyComponent />;
}
대부분의 경우에는 이 구분이 크게 중요하지 않지만, 서버/클라이언트 컴포넌트를 설명할 때는 매우 중요하다.
서버 또는 클라이언트 컴포넌트에 대해 이야기할 때는 컴포넌트 사용법을 말한다.
- 컴포넌트가 'use client' 지시문이 있는 모듈에서 정의되었거나, 클라이언트 컴포넌트 안에서 import되어 사용되는 경우, 해당 컴포넌트 사용은 클라이언트 컴포넌트가 된다.
- 그렇지 않으면, 해당 컴포넌트 사용은 서버 컴포넌트로 간주된다.

FancyText의 예
- FancyText 정의에는 'use client'가 없다.
- 하지만 FancyText는 두 번 사용된다:
- App의 자식으로 사용 → 서버 컴포넌트로 처리
- InspirationGenerator의 자식으로 사용 → 클라이언트 컴포넌트로 처리 (InspirationGenerator에 'use client' 존재)
FancyText의 컴포넌트 정의 자체는 서버에서 평가(evaluated)되고, 클라이언트가 클라이언트 컴포넌트(사용)를 렌더링하기 위해 다운로드할 수도 있다. 즉 클라이언트 컴포넌트에서 import될 땐 클라이언트 컴포넌트로 사용되고 , 기본적으로는 서버 컴포넌트다.
왜 Copyright는 서버 구성 요소일까?
Copyright가 클라이언트 컴포넌트인 InspirationGenerator의 자식으로 렌더링되기 때문에, 서버 컴포넌트라는 사실이 놀랍게 느껴질 수 있다.
기억할 점은, 'use client'는 렌더 트리가 아니라 모듈 의존성 트리에서 서버 코드와 클라이언트 코드의 경계를 정의한다는 것이다.

'use client'는 모듈 의존성 트리에서 서버 코드와 클라이언트 코드의 경계를 정의한다.
모듈 의존성 트리를 보면, App.js가 Copyright.js 모듈에서 Copyright를 import하고 호출하는 것을 확인할 수 있다. Copyright.js에는 'use client' 지시문이 없으므로, 해당 컴포넌트 사용은 서버에서 렌더링된다. App은 루트 컴포넌트이므로 서버에서 렌더링된다.
클라이언트 컴포넌트는 서버 컴포넌트를 렌더링할 수 있다. 왜냐하면 JSX를 props로 전달할 수 있기 때문이다. 이번 경우, InspirationGenerator는 Copyright를 children으로 받는다. 하지만 InspirationGenerator 모듈 자체는 Copyright 모듈을 직접 import 하거나 컴포넌트를 호출하지 않는다. 모든 처리는 App에서 이루어진다. 실제로 Copyright 컴포넌트는 InspirationGenerator가 렌더링을 시작하기 전에 완전히 실행된다.
핵심 요점은, 컴포넌트 간의 부모-자식 렌더링 관계가 같은 실행 환경을 보장하지는 않는다는 것이다.
서버/클라이언트 컴포넌트 판단 기준 요약
- 'use client'가 있는 모듈에서 정의되었거나, 클라이언트 컴포넌트 안에서 import되어 사용될 경우 → 클라이언트 컴포넌트
- 그 외 → 서버 컴포넌트
6. 서버/클라이언트 컴포넌트 구성 패턴
React 애플리케이션을 빌드할 때, 애플리케이션의 어떤 부분이 서버에서 렌더링되어야 하고 어떤 부분이 클라이언트에서 렌더링되어야 하는지 고려해야 한다. Server Components와 Client Components를 사용할 때 권장되는 구성 패턴을 소개한다.
언제 서버 컴포넌트/클라이언트 컴포넌트를 사용해야 할까?
https://nextjs-ko.org/docs/app/building-your-application/rendering/composition-patterns
| 할 일 | Server Component | Client Component |
|---|---|---|
| 데이터 페칭 | ✅ | ❌ |
| 백엔드 리소스 직접 접근 | ✅ | ❌ |
| 민감한 정보 서버에 보관 (액세스 토큰, API 키 등) | ✅ | ❌ |
| 대형 종속성을 서버에 유지하여 클라이언트 측 JS 줄이기 | ✅ | ❌ |
상호작용 및 이벤트 리스너 추가 (onClick, onChange 등) |
❌ | ✅ |
상태 및 생명주기 훅 사용 (useState, useReducer, useEffect 등) |
❌ | ✅ |
| 브라우저 전용 API 사용 | ❌ | ✅ |
| 상태, 효과 또는 브라우저 전용 API에 의존하는 커스텀 훅 사용 | ❌ | ✅ |
| React 클래스 컴포넌트 사용 | ❌ | ✅ |
여담
Next.js의 서버 컴포넌트 / 클라이언트 컴포넌트를 사용할 때 많이 헷갈렸기 때문에, Next.js 프로젝트를 보완하는 김에 완전히 개념을 파악해두려고 하다보니 글이 길어졌다. Next.js가 React를 기반으로 한 프레임워크인만큼 결국 React를 이해해야 Next.js를 사용할 수 있다는 점을 깨달았다. 이해하고 보니, 기존 Next.js 프로젝트 대부분의 컴포넌트에서 SSR이 이루어지지 않았다(대부분 클라이언트 컴포넌트로 사용됨).. 고칠 점이 굉장히 많이 보인다..마음이 아프다.. 그래도 알게 되어 기쁘다..
