Next.js 기초 입문 8주차: 서버 컴포넌트와 클라이언트 컴포넌트 완벽 이해

📚
제8주차 학습 목표

서버 컴포넌트와 클라이언트 컴포넌트

Next.js의 핵심 개념인 서버 컴포넌트와 클라이언트 컴포넌트의 차이를 이해합니다.

Next.js 기초 입문 8회차: 서버 컴포넌트와 클라이언트 컴포넌트

안녕하세요! Next.js 기초 입문 과정의 여덟 번째 시간입니다. 이번 회차에서는 Next.js 13 버전부터 도입된 핵심 개념인 서버 컴포넌트(Server Components)클라이언트 컴포넌트(Client Components)에 대해 심도 있게 학습하겠습니다. 이 두 가지 컴포넌트의 차이점을 명확히 이해하는 것은 Next.js 애플리케이션의 성능 최적화와 효율적인 개발에 필수적입니다.

📌 이번 회차 학습 목표

  • Next.js 서버 컴포넌트의 개념과 특징을 설명할 수 있습니다.
  • Next.js 클라이언트 컴포넌트의 개념과 특징을 설명할 수 있습니다.
  • 두 컴포넌트의 주요 차이점을 비교하고 적절한 사용 시나리오를 파악할 수 있습니다.
  • 'use client' 지시어의 역할과 사용법을 이해하고 적용할 수 있습니다.
  • 서버 컴포넌트와 클라이언트 컴포넌트를 조합하여 효율적인 애플리케이션을 구성할 수 있습니다.

💡 잠시만요! Next.js 13 이전에는 어땠을까요?

Next.js 13 이전에는 모든 React 컴포넌트가 기본적으로 클라이언트 컴포넌트처럼 동작했습니다. 즉, 서버에서 HTML을 미리 렌더링(SSR)하더라도, 최종적으로는 브라우저에서 JavaScript를 통해 상호작용 가능한 형태로 ‘하이드레이션(Hydration)’되는 방식이었습니다. 서버 컴포넌트는 이러한 전통적인 방식을 넘어, 서버에서만 실행되고 클라이언트 번들에 포함되지 않는 새로운 패러다임을 제시합니다.

📝 개념 설명: 서버 컴포넌트와 클라이언트 컴포넌트

1. 서버 컴포넌트 (Server Components)

서버 컴포넌트는 이름 그대로 서버에서만 렌더링되고 실행되는 React 컴포넌트입니다. Next.js 13의 App Router에서는 모든 컴포넌트가 기본적으로 서버 컴포넌트로 간주됩니다. 이는 Next.js 애플리케이션의 성능과 사용자 경험을 크게 향상시키는 중요한 변화입니다.

  • 주요 특징:
    • 기본값: App Router에서 생성되는 모든 컴포넌트는 명시적으로 'use client' 지시어를 사용하지 않는 한 서버 컴포넌트입니다.
    • 서버에서만 실행: 데이터 페칭, 파일 시스템 접근, 데이터베이스 쿼리 등 서버 전용 작업을 수행할 수 있습니다.
    • 클라이언트 번들 크기 감소: 서버 컴포넌트의 코드는 클라이언트(브라우저)로 전송되지 않으므로, JavaScript 번들 크기가 줄어들어 페이지 로딩 속도가 빨라집니다.
    • 보안 강화: 민감한 API 키나 데이터베이스 자격 증명 등을 서버 컴포넌트 내에서 안전하게 사용할 수 있습니다.
    • 상태(State) 및 효과(Effect) 없음: useState, useEffect와 같은 React Hooks를 사용할 수 없습니다. 이는 서버 컴포넌트가 상호작용을 담당하지 않기 때문입니다.
    • 이벤트 핸들러 없음: onClick, onChange와 같은 클라이언트 측 이벤트 핸들러를 직접 사용할 수 없습니다.
  • 주요 사용 시나리오:
    • 데이터베이스에서 데이터를 가져와 표시하는 경우
    • 파일 시스템에서 데이터를 읽어오는 경우
    • 외부 API를 호출하여 데이터를 가져오는 경우
    • 정적인 UI 요소를 렌더링하는 경우
    • 클라이언트 측 JavaScript가 필요 없는 모든 경우

2. 클라이언트 컴포넌트 (Client Components)

클라이언트 컴포넌트브라우저(클라이언트)에서 렌더링되고 실행되는 React 컴포넌트입니다. 기존 React 애플리케이션에서 사용하던 방식과 유사하며, 사용자 상호작용을 처리하는 데 특화되어 있습니다.

  • 주요 특징:
    • 'use client' 지시어: 파일 상단에 'use client'를 명시해야 클라이언트 컴포넌트로 지정됩니다.
    • 브라우저에서 실행: 모든 JavaScript 코드가 브라우저로 전송되어 실행됩니다.
    • 상태(State) 및 효과(Effect) 사용 가능: useState, useEffect, useContext 등 모든 React Hooks를 사용할 수 있습니다.
    • 이벤트 핸들러 사용 가능: onClick, onChange 등 사용자 상호작용을 위한 이벤트 핸들러를 정의할 수 있습니다.
    • 브라우저 API 접근 가능: window, document와 같은 브라우저 전역 객체에 접근할 수 있습니다.
    • 번들 크기 증가: 코드가 클라이언트 번들에 포함되므로, 불필요한 클라이언트 컴포넌트 사용은 번들 크기를 증가시킬 수 있습니다.
  • 주요 사용 시나리오:
    • 사용자 상호작용이 필요한 UI (카운터, 토글 버튼, 폼 입력 등)
    • useState, useEffect와 같은 React Hooks를 사용하는 경우
    • 브라우저 전용 API (예: Geolocation API, Web Storage API)를 사용하는 경우
    • 애니메이션, 드래그 앤 드롭 등 복잡한 클라이언트 측 로직이 필요한 경우

3. 'use client' 지시어

'use client'는 Next.js App Router에서 파일의 최상단에 선언하여 해당 모듈과 그 하위 모듈들을 클라이언트 컴포넌트로 지정하는 지시어입니다. 이 지시어가 없으면 모든 컴포넌트는 기본적으로 서버 컴포넌트로 간주됩니다.

'use client';

import React, { useState } from 'react';

export default function Counter() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>현재 카운트: {count}</p>
      <button onClick={() => setCount(count + 1)}>증가</button>
    </div>
  );
}

⚠️ 주의사항: 'use client'는 파일 단위로 적용됩니다.

'use client' 지시어는 해당 파일뿐만 아니라, 그 파일에서 import하는 모든 모듈(컴포넌트, 유틸리티 함수 등)도 클라이언트 환경에서 실행될 수 있음을 의미합니다. 따라서 클라이언트 컴포넌트 내부에 서버 컴포넌트를 직접 import하여 사용할 수는 없습니다. 대신, 서버 컴포넌트가 클라이언트 컴포넌트를 자식으로 포함하는 방식으로 구성해야 합니다.

⚖️ 서버 컴포넌트 vs 클라이언트 컴포넌트 비교
항목서버 컴포넌트 (기본값)클라이언트 컴포넌트 ('use client')
실행 환경서버클라이언트 (브라우저)
번들 포함 여부클라이언트 번들에 포함되지 않음클라이언트 번들에 포함됨
React Hooks 사용불가능 (useState, useEffect 등)가능 (모든 Hooks)
이벤트 핸들러불가능 (onClick 등)가능
데이터 접근서버 자원 (DB, 파일 시스템, 서버 API)에 직접 접근 가능클라이언트 자원 (브라우저 API)에 직접 접근 가능
보안민감한 정보 안전하게 처리민감한 정보 노출 위험
성능초기 로딩 속도 빠름, 번들 크기 작음초기 로딩 후 상호작용 빠름, 번들 크기 큼
지시어없음 (기본값)파일 최상단에 'use client' 명시

💡 예제 & 실습: 서버 컴포넌트와 클라이언트 컴포넌트 조합하기

이제 실제 Next.js 프로젝트에서 서버 컴포넌트와 클라이언트 컴포넌트를 어떻게 함께 사용하는지 살펴보겠습니다. 목표는 서버에서 데이터를 가져와 표시하고, 클라이언트에서 사용자 상호작용을 처리하는 간단한 페이지를 만드는 것입니다.

프로젝트 설정

새로운 Next.js 프로젝트를 생성합니다 (App Router 사용).

npx create-next-app@latest my-app --typescript --app
cd my-app

1단계: 서버 컴포넌트 생성 (데이터 페칭)

app/page.tsx 파일은 기본적으로 서버 컴포넌트입니다. 여기에 가상의 사용자 목록을 가져오는 로직을 추가해 보겠습니다.

// app/page.tsx

// 이 파일은 'use client' 지시어가 없으므로 서버 컴포넌트입니다.

interface User {
  id: number;
  name: string;
  email: string;
}

// 서버에서 데이터를 비동기적으로 가져오는 함수
async function getUsers(): Promise<User[]> {
  // 실제 API 호출 대신 가상의 데이터 반환
  const users = [
    { id: 1, name: '김철수', email: 'chulsoo@example.com' },
    { id: 2, name: '이영희', email: 'younghee@example.com' },
    { id: 3, name: '박민수', email: 'minsu@example.com' },
  ];
  // 네트워크 지연을 시뮬레이션
  await new Promise(resolve => setTimeout(resolve, 1000));
  return users;
}

export default async function HomePage() {
  const users = await getUsers(); // 서버에서 데이터 페칭

  return (
    <div style={{ padding: '20px' }}>
      <h1>사용자 목록 (서버 컴포넌트)</h1>
      <ul>
        {users.map(user => (
          <li key={user.id}>
            {user.name} ({user.email})
          </li>
        ))}
      </ul>
      {/* 클라이언트 컴포넌트를 자식으로 포함 */}
      <hr style={{ margin: '20px 0' }} />
      <h2>클라이언트 컴포넌트 예시</h2>
      <InteractiveButton />
    </div>
  );
}
해설:
  • app/page.tsx'use client' 지시어가 없으므로 서버 컴포넌트입니다.
  • getUsers 함수는 async로 정의되어 서버에서 데이터를 비동기적으로 가져오는 역할을 시뮬레이션합니다. 실제 환경에서는 데이터베이스 쿼리나 외부 API 호출이 이 위치에 들어갈 수 있습니다.
  • HomePage 컴포넌트 내에서 await getUsers()를 통해 데이터를 가져오고, 이 데이터를 기반으로 사용자 목록을 렌더링합니다. 이 모든 과정은 서버에서 이루어지며, 클라이언트 번들에는 해당 데이터 페칭 로직이 포함되지 않습니다.
  • <InteractiveButton />은 아직 정의되지 않은 클라이언트 컴포넌트입니다. 서버 컴포넌트가 클라이언트 컴포넌트를 자식으로 렌더링하는 방식으로 함께 사용됩니다.

2단계: 클라이언트 컴포넌트 생성 (상호작용)

이제 사용자 상호작용을 처리할 클라이언트 컴포넌트를 생성해 보겠습니다. components/InteractiveButton.tsx 파일을 생성합니다.

// components/InteractiveButton.tsx

'use client'; // 이 지시어가 클라이언트 컴포넌트임을 명시합니다.

import React, { useState } from 'react';

export default function InteractiveButton() {
  const [message, setMessage] = useState('버튼을 눌러보세요!');

  const handleClick = () => {
    setMessage('버튼이 눌렸습니다! ' + new Date().toLocaleTimeString());
  };

  return (
    <div style={{ border: '1px solid #ccc', padding: '15px', borderRadius: '5px' }}>
      <p>{message}</p>
      <button onClick={handleClick}>클릭하세요!</button>
    </div>
  );
}
해설:
  • 파일 최상단에 'use client' 지시어가 명시되어 이 컴포넌트가 클라이언트 컴포넌트임을 Next.js에게 알려줍니다.
  • useState Hook을 사용하여 message 상태를 관리합니다.
  • handleClick 함수는 버튼 클릭 시 message 상태를 업데이트하며, 이는 클라이언트 측에서만 동작하는 상호작용입니다.
  • 이 컴포넌트는 클라이언트 번들에 포함되어 브라우저에서 JavaScript를 통해 상호작용이 가능해집니다.

3단계: 클라이언트 컴포넌트 임포트

이제 app/page.tsx 파일에서 방금 만든 InteractiveButton 클라이언트 컴포넌트를 임포트하여 사용합니다.

// app/page.tsx (수정된 부분)

import InteractiveButton from '../components/InteractiveButton'; // 클라이언트 컴포넌트 임포트

// ... (기존 getUsers 함수 및 User 인터페이스)

export default async function HomePage() {
  const users = await getUsers();

  return (
    <div style={{ padding: '20px' }}>
      <h1>사용자 목록 (서버 컴포넌트)</h1>
      <ul>
        {users.map(user => (
          <li key={user.id}>
            {user.name} ({user.email})
          </li>
        ))}
      </ul>
      <hr style={{ margin: '20px 0' }} />
      <h2>클라이언트 컴포넌트 예시</h2>
      <InteractiveButton /> { /* 클라이언트 컴포넌트 렌더링 */ }
    </div>
  );
  );
}
실행 결과:

프로젝트를 실행하고 브라우저에서 확인하면, 초기 페이지 로딩 시 서버에서 가져온 사용자 목록이 먼저 보이고, 그 아래에 ‘클릭하세요!’ 버튼이 보일 것입니다. 이 버튼을 클릭하면 메시지가 변경되는 것을 확인할 수 있습니다. 사용자 목록은 서버에서 정적으로 렌더링되어 빠르게 표시되고, 버튼은 클라이언트에서 상호작용을 제공하는 구조입니다.

🔄 서버/클라이언트 컴포넌트 흐름
1. 사용자 요청
(브라우저)
2. Next.js 서버
(서버 컴포넌트 렌더링, 데이터 페칭)
3. HTML/CSS 생성
(클라이언트 컴포넌트 자리 비워둠)
4. 브라우저로 전송
(초기 페이지 로드)
5. 클라이언트 컴포넌트 하이드레이션
(JS 번들 다운로드 및 실행, 상호작용 가능)

⚠️ 자주 틀리는 것 / 주의사항

  • 서버 컴포넌트에서 useState, useEffect 사용: 서버 컴포넌트에서는 React Hooks를 사용할 수 없습니다. 이를 시도하면 런타임 오류가 발생합니다. 상호작용이 필요한 부분은 반드시 클라이언트 컴포넌트로 분리해야 합니다.
  • 클라이언트 컴포넌트 내에서 서버 전용 코드 사용: 'use client'가 선언된 파일 내에서는 fs 모듈 접근이나 데이터베이스 직접 쿼리 등 서버 전용 코드를 사용할 수 없습니다. 이는 클라이언트 번들에 포함되어 브라우저에서 실행될 수 없기 때문입니다.
  • 클라이언트 컴포넌트의 과도한 사용: 모든 컴포넌트에 'use client'를 붙이면 Next.js의 서버 컴포넌트 이점을 잃게 됩니다. 필요한 경우에만 클라이언트 컴포넌트를 사용하고, 기본적으로는 서버 컴포넌트를 활용하여 번들 크기를 최적화하는 것이 중요합니다.
  • 서버 컴포넌트가 클라이언트 컴포넌트를 임포트하는 방식: 서버 컴포넌트가 클라이언트 컴포넌트를 임포트하여 자식으로 렌더링하는 것은 가능하지만, 클라이언트 컴포넌트가 서버 컴포넌트를 직접 임포트하는 것은 불가능합니다. 클라이언트 컴포넌트 내에서 서버 컴포넌트의 기능을 사용하려면, 서버 컴포넌트에서 데이터를 가져와 props로 전달하거나, API 라우트를 통해 데이터를 페칭하는 방식을 사용해야 합니다.
✅ 이번 회차 핵심 정리
  • 서버 컴포넌트는 서버에서 렌더링되고 클라이언트 번들에 포함되지 않아 성능과 보안에 유리합니다. 기본적으로 모든 컴포넌트는 서버 컴포넌트입니다.
  • 클라이언트 컴포넌트'use client' 지시어를 통해 명시되며, 브라우저에서 렌더링되어 useState, useEffect와 같은 React Hooks 및 이벤트 핸들러를 사용하여 사용자 상호작용을 처리합니다.
  • Next.js 애플리케이션은 서버 컴포넌트와 클라이언트 컴포넌트를 적절히 조합하여 성능과 상호작용성을 모두 최적화할 수 있습니다.
  • 서버 컴포넌트는 클라이언트 컴포넌트를 자식으로 포함할 수 있지만, 클라이언트 컴포넌트가 서버 컴포넌트를 직접 임포트하는 것은 불가능합니다.

🔗 다음 회차 예고

이번 회차에서는 Next.js의 핵심인 서버 컴포넌트와 클라이언트 컴포넌트의 개념과 활용법을 익혔습니다. 다음 9회차에서는 데이터 페칭 전략에 대해 더 깊이 있게 다룰 예정입니다. 서버 컴포넌트에서 데이터를 효율적으로 가져오는 방법, 캐싱 전략, 그리고 클라이언트 컴포넌트에서 데이터를 가져오는 다양한 방식(SWR, React Query 등)을 학습하여 실제 애플리케이션에 적용할 수 있는 능력을 키울 것입니다. 기대해 주세요!

댓글 남기기