(리액트) 클라이언트 컴포넌트
use client 지시어와 클라이언트 컴포넌트의 동작 방식, 서버 컴포넌트와의 경계를 이해합니다.
이 문서에서 다루는 것
앞 문서에서 서버 컴포넌트를 봤고, 인터랙션이 필요한 부분은 클라이언트 컴포넌트로 분리한다고 했습니다.
이 문서에서는 클라이언트 컴포넌트를 실제로 어떻게 만들고, 어떤 규칙을 지켜야 하는지 확인합니다.
'use client' 지시어
클라이언트 컴포넌트를 만드는 방법은 단순합니다.
파일 맨 위에 'use client'를 적습니다.
'use client';
import { useState } from 'react';
export function LikeButton() {
const [liked, setLiked] = useState(false);
return (
<button onClick={() => setLiked(!liked)}>
{liked ? '❤️' : '🤍'}
</button>
);
}'use client'는 "이 파일의 JS를 브라우저에도 보내라"는 경계선입니다.
이 지시어가 있는 파일과 그 파일이 import하는 모든 모듈은 브라우저 번들에 포함됩니다.
클라이언트 컴포넌트가 할 수 있는 것
| 기능 | 용도 |
|---|---|
useState, useReducer | 상태 관리 |
useEffect | 마운트 후 작업 (API 호출, 타이머 등) |
onClick, onChange, onSubmit | 이벤트 핸들러 |
window, document | 브라우저 API |
localStorage, sessionStorage | 브라우저 저장소 |
Context (useContext) | 클라이언트 상태 공유 |
리액트 기초 트랙에서 배운 거의 모든 React 기능이 여기에 해당합니다.
useState로 상태를 만들고, useEffect로 사이드 이펙트를 처리하고, 이벤트 핸들러로 사용자 입력을 받는 것.
이게 전부 클라이언트 컴포넌트의 영역입니다.
흔한 오해 — 클라이언트 컴포넌트는 서버에서 안 돌아간다?
아닙니다. 클라이언트 컴포넌트도 서버에서 한 번 실행됩니다.
Next.js는 첫 페이지 요청 시 클라이언트 컴포넌트까지 포함해서 서버에서 HTML을 생성합니다.
그래야 사용자가 빈 화면 대신 내용이 있는 HTML을 받을 수 있기 때문입니다.
서버 컴포넌트와의 차이는 그 다음에 있습니다.
서버 컴포넌트는 서버에서 실행되고 끝이지만, 클라이언트 컴포넌트는 브라우저에서 한 번 더 실행(Hydration)되고 JS 번들도 브라우저에 전달됩니다.
서버: LikeButton을 실행해서 HTML 생성 (초기 상태: 🤍)
↓
브라우저: HTML을 바로 표시 (🤍 버튼이 보임)
↓
브라우저: JS 로드 후 Hydration (클릭 가능해짐)'use client'는 "이 컴포넌트의 JS를 브라우저에 보내서 인터랙션을 활성화하라"는 뜻이지,
"서버에서 아예 실행하지 마라"는 뜻이 아닙니다.
그래서 클라이언트 컴포넌트에서도 window나 document에 접근할 때는 주의가 필요합니다.
'use client';
import { useEffect, useState } from 'react';
export function WindowSize() {
const [width, setWidth] = useState(0);
useEffect(() => {
setWidth(window.innerWidth);
}, []);
return <span>화면 너비: {width}px</span>;
}window.innerWidth를 컴포넌트 본문에 바로 쓰면, 서버에서 실행될 때 에러가 납니다.
useEffect 안에 넣으면 브라우저에서만 실행되므로 안전합니다.
서버 컴포넌트와 클라이언트 컴포넌트의 조합
실제 페이지는 두 종류가 섞여 있습니다.
// app/page.tsx — 서버 컴포넌트
import { LikeButton } from './LikeButton';
export default async function ArticlePage() {
const article = await db.article.findUnique({ where: { id: 1 } });
return (
<article>
<h1>{article.title}</h1>
<p>{article.content}</p>
<LikeButton articleId={article.id} />
</article>
);
}ArticlePage는 서버 컴포넌트입니다. 데이터를 직접 조회합니다.
LikeButton은 클라이언트 컴포넌트입니다. 클릭 이벤트를 처리합니다.
서버 컴포넌트 안에 클라이언트 컴포넌트를 넣을 수 있습니다.
반대로 클라이언트 컴포넌트 안에서 서버 컴포넌트를 import하면, 그 서버 컴포넌트는 클라이언트 컴포넌트로 전환됩니다.
에러가 나지 않고 조용히 번들에 포함되기 때문에, DB 접근 같은 서버 전용 기능을 쓰고 있었다면 런타임에서야 문제가 드러납니다.
경계 규칙 정리
- 서버 → 서버: 가능
- 서버 → 클라이언트: 가능
- 클라이언트 → 클라이언트: 가능
- 클라이언트 → 서버 (import): 의도대로 동작하지 않음 (서버 컴포넌트가 클라이언트로 전환됨)
언제 어떤 컴포넌트를 쓸까
| 상황 | 선택 |
|---|---|
| 데이터를 가져와서 보여주기만 | 서버 컴포넌트 |
| 마크다운 변환, 구문 강조 같은 무거운 처리 | 서버 컴포넌트 |
| 버튼 클릭, 폼 입력, 토글 | 클라이언트 컴포넌트 |
useState, useEffect 사용 | 클라이언트 컴포넌트 |
window, localStorage 접근 | 클라이언트 컴포넌트 |
기본 원칙은 가능한 한 서버 컴포넌트를 쓰고, 인터랙션이 필요한 부분만 클라이언트 컴포넌트로 분리하는 것입니다.
필수이론 트랙을 마치며
여기까지 CSS, 비동기, 런타임 환경, Flux 패턴, Context, SSR/CSR, 서버/클라이언트 컴포넌트를 다뤘습니다.
리액트 기초 트랙에서 Vite + React로 간단한 앱을 만들어봤고,
필수이론 트랙에서 그 위에 깔린 개념들을 하나씩 풀어봤습니다.
이로써 필수이론 트랙을 마칩니다.
여기까지의 내용을 숙달하셨다면, Next.js는 매우 쉽습니다.
직접 해보기
- 리액트 기초 트랙에서 만든 투두리스트를 Next.js로 옮긴다고 가정하면, 어떤 컴포넌트가 서버 컴포넌트이고 어떤 컴포넌트가 클라이언트 컴포넌트인지 생각해보기
- Next.js 프로젝트에서
'use client'없이useState를 쓰면 어떤 에러가 나는지 확인해보기