안동민 개발노트

안동민 개발노트

서버 컴포넌트클라이언트 컴포넌트 사용법서버·클라이언트 컴포넌트 조합use client 지시어 활용
본문 시작
  1. 홈
  2. 문서
  3. Next.js
  4. 7장 : 서버, 클라이언트 컴포넌트
  5. 서버 컴포넌트
  1. Next.js
  2. 서버 컴포넌트

서버 컴포넌트

서버 컴포넌트가 브라우저 번들과 비밀 정보 노출을 줄이며 서버에서 데이터와 UI를 만드는 과정을 이해합니다.

Next.js 16 App Router는 서버 컴포넌트(Server Components)를 기본으로 데이터와 UI를 준비합니다.

이 아키텍처는 React 애플리케이션의 성능, 보안, 개발 경험을 함께 개선하는 방향을 제시했습니다.

클라이언트 컴포넌트 중심 개발에 익숙하다면, 서버 컴포넌트의 개념과 작동 원리를 정확히 이해하는 것이 App Router를 제대로 활용하는 첫걸음입니다.

이 절에서는 서버 컴포넌트가 무엇이며, 왜 필요한지, 그리고 어떻게 동작하는지에 대해 심도 있게 알아보겠습니다.


서버 컴포넌트란 무엇인가요?

서버 컴포넌트(Server Components)는 이름 그대로 서버 환경에서 렌더링되고 실행되는 React 컴포넌트입니다.

클라이언트 컴포넌트는 브라우저에서 상태·이벤트를 처리하지만 첫 방문의 초기 HTML 생성에는 서버도 참여합니다. 컴포넌트의 모듈 경계와 요청·빌드 중 언제 렌더하는지는 서로 다른 기준입니다.

서버 컴포넌트의 핵심 특징
  • 기본값: 페이지와 레이아웃은 기본적으로 서버 컴포넌트입니다. "use client"에서 이어지는 import 그래프의 모듈은 지시어가 없어도 클라이언트에서 평가될 수 있습니다.
  • 서버에서만 실행: 컴포넌트 내부의 코드가 클라이언트 번들에 포함되지 않고 서버에서만 실행됩니다.
  • 제로(0) 번들 사이즈: 서버 컴포넌트 코드는 클라이언트에 전송되지 않으므로, 클라이언트 측 JavaScript 번들 크기를 획기적으로 줄일 수 있습니다. 이는 특히 모바일 환경에서 초기 로딩 속도에 큰 영향을 줍니다.
  • 직접적인 백엔드 접근: 데이터베이스, 파일 시스템, 내부 API 등 서버 전용 자원에 직접 접근하고 데이터를 가져올 수 있습니다. Route Handler를 만들 필요 없이 컴포넌트 내에서 바로 데이터를 페칭할 수 있다는 의미입니다.
  • async/await 지원: 비동기 함수로 정의할 수 있어 async/await를 사용하여 데이터 페칭을 매우 직관적으로 작성할 수 있습니다.
  • 상태 및 효과 없음: useState, useEffect와 같은 React 훅을 사용할 수 없습니다. (이는 클라이언트 컴포넌트의 역할입니다.)
  • 이벤트 핸들러 없음: onClick, onChange와 같은 클라이언트 사이드 이벤트 핸들러를 직접 추가할 수 없습니다.
  • React Context 사용 불가: React Context를 직접 사용할 수 없습니다. (하지만 서버 컴포넌트 간에는 prop 드릴링 또는 서버 전용 캐싱 패턴을 사용할 수 있습니다.)

서버 컴포넌트가 필요한 이유

서버 컴포넌트는 기존의 클라이언트 중심 렌더링 방식이 가졌던 여러 문제점을 해결하고 다음과 같은 이점을 제공합니다.

성능 최적화 (번들 크기 감소 및 초기 로딩 속도 향상)
  • JavaScript 번들 크기 감소: 서버 컴포넌트의 코드는 클라이언트에 전송되지 않으므로, 사용자가 다운로드해야 하는 JavaScript 양이 줄어듭니다. 이는 특히 초기 페이지 로딩 속도에 결정적인 영향을 미칩니다.
  • 데이터 페칭 워터폴(Waterfall) 방지: 독립적인 요청을 먼저 시작하고 함께 기다리면 순차 요청을 줄일 수 있습니다. 서버 컴포넌트에 순차 await를 쓰면 워터폴이 생길 수 있으며 자동 병렬화되는 것은 아닙니다.
  • 스트리밍 및 점진적 로딩: Next.js는 서버 컴포넌트의 HTML을 청크(Chunk) 단위로 클라이언트에 스트리밍할 수 있습니다. 이는 페이지의 일부가 먼저 보인 후 데이터 로딩이 완료되는 대로 점진적으로 콘텐츠가 나타나게 하여 사용자 경험을 향상시킵니다.
보안 강화
  • 민감 정보 노출 방지: 서버 전용 코드를 브라우저 번들에서 제외할 수 있습니다. 그래도 렌더 결과·props로 보낸 비밀값은 노출되므로 공개할 값만 선택해야 합니다.
  • 직접적인 백엔드 접근: Route Handler를 따로 만들 필요 없이 서버 컴포넌트에서 직접 백엔드 로직을 실행할 수 있어 개발 복잡성을 줄이고 안전하게 데이터를 처리할 수 있습니다.
개발 경험 향상
  • 단순화된 데이터 페칭: async/await를 사용하여 useState나 useEffect 없이도 직관적으로 데이터를 가져올 수 있습니다. 이는 getServerSideProps나 getStaticProps보다 유연합니다.
  • 클라이언트-서버 간 경계 추상화: 개발자는 어떤 컴포넌트가 서버에서 실행될지, 어떤 컴포넌트가 클라이언트에서 실행될지 명시적으로 제어할 수 있습니다.
  • 서버 렌더링 모델: 서버 컴포넌트는 빌드 시 프리렌더와 요청 시 렌더 모두에 사용됩니다. 서버 컴포넌트라는 이유만으로 매 요청 SSR이 되는 것은 아닙니다.

서버 컴포넌트 작동 원리

첫 방문에서 서버 컴포넌트는 RSC Payload로 표현됩니다. Next.js는 이 결과와 클라이언트 컴포넌트를 함께 사용해 초기 HTML을 만듭니다.

첫 방문에서 브라우저로 전달되는 세 역할

서버 컴포넌트는 RSC Payload로 렌더됩니다. Next.js는 이 결과와 클라이언트 컴포넌트로 초기 HTML을 만듭니다. 브라우저는 HTML을 표시하고 RSC Payload로 트리를 맞추며 클라이언트 JavaScript로 해당 컴포넌트의 이벤트를 연결합니다. 세 행은 전송 완료 순서가 아닙니다.

첫 방문에서 브라우저로 전달되는 세 역할서버 컴포넌트는 RSC Payload로 렌더됩니다. Next.js는 이 결과와 클라이언트 컴포넌트로 초기 HTML을 만듭니다. 브라우저는 HTML을 표시하고 RSC Payload로 트리를 맞추며 클라이언트 JavaScript로 해당 컴포넌트의 이벤트를 연결합니다. 세 행은 전송 완료 순서가 아닙니다.서버에서 준비브라우저의 역할초기 HTMLRSC + Client로 생성화면 표시비상호작용 미리보기RSC Payload서버 결과 · 참조 · props트리 맞추기서버·Client 결과 조정Client JS클라이언트 모듈이벤트 연결Client hydration
첫 방문에서 브라우저로 전달되는 세 역할서버 컴포넌트는 RSC Payload로 렌더됩니다. Next.js는 이 결과와 클라이언트 컴포넌트로 초기 HTML을 만듭니다. 브라우저는 HTML을 표시하고 RSC Payload로 트리를 맞추며 클라이언트 JavaScript로 해당 컴포넌트의 이벤트를 연결합니다. 세 행은 전송 완료 순서가 아닙니다.서버에서 준비브라우저의 역할초기 HTML초기 화면화면 표시HTML 표시RSC Payload서버 결과·참조트리 맞추기RSC 결과 조정Client JSClient 모듈이벤트 연결상태·이벤트

RSC Payload는 서버 컴포넌트의 렌더 결과, 클라이언트 컴포넌트 위치·모듈 참조, 전달 props를 담습니다. React가 지원하는 직렬화 형식이며 단순히 모든 데이터를 JSON으로 바꾸는 과정은 아닙니다. 후속 클라이언트 탐색에서는 RSC Payload를 받아 트리를 갱신하며 첫 방문의 HTML 전달 과정을 그대로 반복하지 않을 수 있습니다.


서버 컴포넌트 예시 (복습)

이전 장에서 다뤘던 많은 페이지 컴포넌트와 레이아웃 컴포넌트가 이미 서버 컴포넌트로 작성되었습니다.

src/app/dashboard/layout.tsx (서버 컴포넌트 예시)
// "use client" 지시어가 없으므로 서버 컴포넌트입니다.

import Link from 'next/link';

// 이 함수는 서버에서 실행됩니다.
async function getUserInfo() {
  console.log('Fetching user info on server...');
  // 실제 DB 접근 또는 내부 API 호출
  await new Promise(resolve => setTimeout(resolve, 500)); // 지연 시뮬레이션
  return { username: '서버 사용자', role: 'admin' };
}

export default async function DashboardLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  const userInfo = await getUserInfo(); // 서버에서 데이터 페칭

  return (
    <div style={{ display: 'flex', minHeight: '100vh' }}>
      <aside style={{ width: '250px', padding: '20px', backgroundColor: '#f0f4f8', borderRight: '1px solid #e0e0e0' }}>
        <h2>관리자 대시보드</h2>
        <p>환영합니다, {userInfo.username}님!</p> {/* 서버에서 가져온 데이터 사용 */}
        <p>권한: {userInfo.role}</p>
        <nav style={{ marginTop: '20px' }}>
          <ul style={{ listStyle: 'none', padding: 0 }}>
            <li style={{ marginBottom: '10px' }}><Link href="/dashboard">홈</Link></li>
            <li style={{ marginBottom: '10px' }}><Link href="/dashboard/settings">설정</Link></li>
            {/* 여기에 다른 서버 컴포넌트나 클라이언트 컴포넌트를 자식으로 렌더링 */}
          </ul>
        </nav>
      </aside>
      <main style={{ flexGrow: 1, padding: '20px' }}>
        {children} {/* 하위 페이지나 레이아웃이 여기에 렌더링됩니다. */}
      </main>
    </div>
  );
}

위 DashboardLayout은 서버에서 getUserInfo를 호출합니다. 원문은 500ms 지연 뒤 고정된 이름·역할을 반환하는 모의이며 실제 DB 조회나 권한 검증은 구현하지 않았습니다.

이 모든 과정은 클라이언트의 JavaScript 번들과는 무관하게 서버에서 처리됩니다.

클라이언트 컴포넌트에서 데이터 페칭

이전 페이지

클라이언트 컴포넌트 사용법

다음 페이지

이 페이지의 목차

서버 컴포넌트란 무엇인가요?서버 컴포넌트가 필요한 이유서버 컴포넌트 작동 원리서버 컴포넌트 예시 (복습)