Allra Fintech

(리액트) 클라이언트 컴포넌트

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를 브라우저에 보내서 인터랙션을 활성화하라"는 뜻이지,
"서버에서 아예 실행하지 마라"는 뜻이 아닙니다.

그래서 클라이언트 컴포넌트에서도 windowdocument에 접근할 때는 주의가 필요합니다.

'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를 쓰면 어떤 에러가 나는지 확인해보기