icon
7장 : 서버, 클라이언트 컴포넌트

use client 지시어 활용

Next.js App Router에서 컴포넌트의 렌더링 환경(서버 또는 클라이언트)을 결정하는 가장 중요한 요소는 바로 "use client" 지시어입니다. 이 짧은 문자열은 파일의 맨 위에 선언되어, 해당 파일이 클라이언트 컴포넌트이며 브라우저에서 실행되어야 함을 Next.js와 번들러에게 명시적으로 알려줍니다. 이 절에서는 'use client' 지시어의 정확한 역할, 사용법, 그리고 그 뒤에 숨겨진 메커니즘을 상세히 알아보겠습니다.


'use client' 지시어의 역할과 중요성

Next.js App Router의 기본 동작은 모든 컴포넌트가 서버 컴포넌트로 렌더링되는 것입니다. 이는 초기 로딩 성능, SEO, 보안 등 여러 이점을 제공합니다. 하지만 상호작용성, 브라우저 API 접근, 클라이언트 측 상태 관리 등 전통적인 React 애플리케이션의 핵심 기능들은 클라이언트 환경에서만 가능합니다. 'use client' 지시어는 이 두 환경 사이의 명확한 경계를 설정하여, 필요한 컴포넌트만 클라이언트 번들에 포함되도록 합니다.

'use client'의 핵심 역할

  • 컴파일러 지시어: Next.js 컴파일러와 번들러(Webpack, Turbopack)에게 해당 파일이 클라이언트 컴포넌트 그래프의 시작점임을 알립니다. 이 지시어가 없으면 파일은 서버 컴포넌트로 간주됩니다.
  • JavaScript 번들 포함: 'use client'가 선언된 파일과 그 하위에 임포트되는 모든 컴포넌트(명시적으로 서버 컴포넌트로 분류되지 않은 경우)는 클라이언트 측 JavaScript 번들에 포함되어 사용자 브라우저로 전송됩니다.
  • 하이드레이션(Hydration) 트리거: 서버에서 렌더링된 HTML이 클라이언트에 도착하면, 'use client'로 표시된 컴포넌트들은 클라이언트 측 JavaScript로 "하이드레이션"되어 상호작용 가능한 상태가 됩니다.
  • React 훅 및 브라우저 API 사용 가능: 해당 파일 내에서 useState, useEffect, window, document 등의 클라이언트 전용 기능들을 사용할 수 있게 합니다.

'use client' 사용법

'use client' 지시어는 매우 간단하게, 컴포넌트 파일의 가장 상단에 위치해야 합니다. 다른 임포트 문이나 코드보다 먼저 와야 합니다.

src/app/my-component/InteractiveButton.tsx
// src/app/my-component/InteractiveButton.tsx
"use client"; // 🚨 이 지시어는 항상 파일 맨 위에 와야 합니다.

import React, { useState } from 'react';

export default function InteractiveButton() {
  const [clicked, setClicked] = useState(false);

  const handleClick = () => {
    setClicked(true);
    alert('버튼이 클릭되었습니다!');
  };

  return (
    <button
      onClick={handleClick}
      style={{
        padding: '10px 20px',
        fontSize: '1em',
        backgroundColor: clicked ? '#28a745' : '#007bff',
        color: 'white',
        border: 'none',
        borderRadius: '5px',
        cursor: 'pointer',
        transition: 'background-color 0.3s'
      }}
    >
      {clicked ? '클릭됨!' : '클릭하세요'}
    </button>
  );
}

중요 사항

  • 정확한 위치: 'use client'는 파일의 첫 번째 비-주석(non-comment) 라인이어야 합니다.
  • 따옴표: 반드시 작은따옴표나 큰따옴표로 감싸야 합니다. ('use client', "use client")
  • 한 번만 선언: 한 번 선언되면 해당 파일뿐만 아니라, 이 파일에서 임포트하는 모든 자식 컴포넌트(명시적으로 서버 컴포넌트로 정의되지 않는 한)에도 클라이언트 컴포넌트 속성이 전파됩니다. 즉, 'use client'는 컴포넌트 트리에서 클라이언트 경계를 정의하는 역할을 합니다.

'use client'가 없는 경우 vs. 있는 경우

특징'use client' 지시어 없음 (서버 컴포넌트)'use client' 지시어 있음 (클라이언트 컴포넌트)
실행 환경서버 (빌드 시 또는 요청 시)클라이언트 (브라우저)
JavaScript 번들포함되지 않음 (제로 번들)포함됨
React 훅 사용❌ (useState, useEffect 등 사용 불가)✅ (모든 React 훅 사용 가능)
이벤트 핸들러❌ (onClick, onChange 등 사용 불가)✅ (모든 이벤트 핸들러 사용 가능)
브라우저 API❌ (window, document 등 접근 불가)✅ (window, document 등 접근 가능)
데이터 페칭async/await로 서버에서 직접 페칭useEffectfetch (또는 SWR 등)으로 클라이언트에서 페칭
민감 정보 접근✅ (DB, API 키 등 안전하게 접근)❌ (클라이언트에 노출될 위험)
SEO서버에서 HTML 생성, 우수초기 HTML은 서버에서, 동적 콘텐츠는 클라이언트에서 생성

'use client' 사용 시 고려사항 및 최적화

'use client'를 남용하면 Next.js App Router의 성능 이점을 잃을 수 있습니다. 가능한 한 최소한의 컴포넌트에만 'use client'를 선언하여 클라이언트 번들 크기를 작게 유지하는 것이 중요합니다.

최적화 전략

  1. "리프까지 내려가기" (Move Client Components to the Leaves): 상호작용이 필요한 클라이언트 컴포넌트를 가능한 한 컴포넌트 트리의 **가장 깊은 곳(리프 노드)**으로 옮기세요. 예를 들어, 전체 페이지가 상호작용할 필요는 없고 헤더의 토글 버튼만 상호작용이 필요하다면, 전체 헤더를 클라이언트 컴포넌트로 만들지 말고 토글 버튼만 클라이언트 컴포넌트로 분리하세요.

    components/Header.tsx
    // Bad (전체 헤더가 클라이언트)
    // components/Header.tsx
    "use client";
    export default function Header() {
      // ... 복잡한 정적 콘텐츠와 작은 토글 버튼
      return <header>... <ToggleButton /> ...</header>;
    }
    
    // Good (토글 버튼만 클라이언트)
    // components/Header.tsx (서버 컴포넌트)
    import ToggleButton from './ToggleButton';
    export default function Header() {
      return <header>... <ToggleButton /> ...</header>;
    }
    
    // components/ToggleButton.tsx
    "use client";
    export default function ToggleButton() {
      const [open, setOpen] = useState(false);
      return <button onClick={() => setOpen(!open)}>Toggle</button>;
    }

    이렇게 하면 헤더의 대부분의 정적 HTML은 서버에서 렌더링되고, 오직 토글 버튼의 작은 JavaScript 코드만 클라이언트에 전송됩니다.

  2. children prop 활용: 클라이언트 컴포넌트가 서버 컴포넌트의 children prop을 받는 경우, children은 이미 서버에서 HTML로 렌더링된 결과물입니다. 이 HTML은 클라이언트 컴포넌트의 JavaScript 번들에 포함되지 않습니다. 이 패턴을 활용하여 클라이언트 컴포넌트가 서버 컴포넌트를 직접 임포트할 수 없는 제약을 우회하고, 서버에서 생성된 콘텐츠를 클라이언트에서 동적으로 조작할 수 있습니다.

    layout.tsx
    // layout.tsx (서버 컴포넌트)
    import ClientWrapper from './ClientWrapper';
    import ServerOnlyContent from './ServerOnlyContent';
    
    export default function Layout({ children }: { children: React.ReactNode }) {
      return (
        <ClientWrapper>
          <ServerOnlyContent /> {/* 서버에서 렌더링되어 children으로 전달됨 */}
          {children} {/* 페이지 콘텐츠도 children으로 전달될 수 있음 */}
        </ClientWrapper>
      );
    }
    ClientWrapper.tsx
    // ClientWrapper.tsx
    "use client";
    // 이 파일은 클라이언트 컴포넌트입니다.
    
    export default function ClientWrapper({ children }: { children: React.ReactNode }) {
      // children은 이미 서버에서 생성된 HTML 또는 클라이언트 컴포넌트
      return (
        <div>
          {/* 여기서 children을 조건부 렌더링하거나 다른 상호작용 추가 */}
          {children}
        </div>
      );
    }
  3. 서버 전용 코드 분리: 클라이언트 컴포넌트 내에서 서버 전용 코드를 실수로 포함하지 않도록 주의하세요. 예를 들어, 클라이언트 컴포넌트가 민감한 API 키를 직접 포함하거나, 데이터베이스 연결 로직을 포함해서는 안 됩니다. 데이터 페칭이 필요하다면, 클라이언트에서 사용할 수 있는 /api 라우트 핸들러를 호출하거나, 서버 컴포넌트에서 데이터를 받아 props로 전달하는 방식을 사용해야 합니다.

'use client' 지시어는 Next.js App Router에서 서버와 클라이언트 컴포넌트의 명확한 분리를 가능하게 하는 핵심 도구입니다. 이 지시어의 작동 원리와 최적화 전략을 이해하고 적용함으로써, 성능, 보안, 그리고 사용자 경험 모두를 향상시키는 효율적인 Next.js 애플리케이션을 구축할 수 있습니다.