서버 컴포넌트
서버 컴포넌트가 브라우저 번들과 비밀 정보 노출을 줄이며 서버에서 데이터와 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 결과와 초기 HTML, 브라우저 첫 표시·RSC 트리 결합·JS 인터랙션 가능 상태를 논리적 준비 단계로 보여 줄 수 있습니다. 원문의 세 행을 네트워크 파일 도착 완료 순서라고 설명하지 않습니다.
NEXT · CH7-1
첫 방문은 HTML·RSC·Client JS의 역할이 다릅니다
초기 HTML은 화면을 보여주고, RSC Payload는 서버 결과·Client 참조·props로 트리를 맞추며, Client JavaScript는 해당 컴포넌트를 하이드레이션합니다.
각 행은 전달값의 역할이며 다운로드 완료 순서가 아닙니다. 서버 컴포넌트 자체 코드는 브라우저 번들에 들어가지 않지만 결과는 전달됩니다. 후속 클라이언트 탐색이 첫 방문의 HTML 전달을 그대로 반복하지는 않습니다.
이 예의 조건
- 두 박스는 같은 실행의 진행 정보와 자료·결과를 관찰하는 고정 역할입니다. 박스의 좌우·위아래 위치를 별도 호출 순서로 해석하지 않습니다.
- 조건별 종료는 대안입니다. 실패 뒤 정상 후속 단계를 실행하거나 독립 비교 사례를 같은 요청으로 연결하지 않습니다.
- 서버 컴포넌트는 RSC Payload로 렌더됩니다. Next.js는 이 결과와 클라이언트 컴포넌트로 초기 HTML을 만듭니다. 브라우저는 HTML을 표시하고 RSC Payload로 트리를 맞추며 클라이언트 JavaScript로 해당 컴포넌트의 이벤트를 연결합니다. 세 행은 전송 완료 순서가 아닙니다.
관계도의 보충 설명
| 순번 | 원문 표시 |
|---|---|
| 3 | 첫 방문에서 HTML·RSC·Client JS의 역할은 서로 다릅니다 |
| 4 | 초기 HTML은 화면을 보여 주고, RSC Payload는 서버 결과·Client 참조·props로 트리를 맞추며, Client JavaScript는 해당 컴포넌트를 하이드레이션합니다. |
| 장면 | 관찰 값 | 전환 이유 |
|---|---|---|
| 서버 결과 준비 Server Component를 RSC Payload로 렌더하고 Client Component와 함께 초기 HTML을 준비합니다. | 서버 산출물: RSC·초기 HTML · 논리: 렌더 준비 | 브라우저 첫 표시의 자료가 준비됩니다. |
| 브라우저 첫 표시 브라우저가 HTML을 표시하고 RSC Payload로 트리를 맞춥니다. | 화면: HTML 표시 · 트리: RSC 결합 · 전송 완료 순서: 이 단계의 계약 아님 | 정적인 첫 화면과 컴포넌트 트리가 준비됩니다. |
| Client 이벤트 연결 Client JavaScript가 해당 컴포넌트의 이벤트를 연결합니다. | Client: 이벤트 연결 · Server Component: 브라우저 실행 아님 | Client 경계가 상호작용 가능한 상태로 바뀝니다. |
첫 방문에서 브라우저로 전달되는 세 역할
서버의 RSC 결과와 초기 HTML, 브라우저 첫 표시·RSC 트리 결합·JS 인터랙션 가능 상태를 논리적 준비 단계로 보여 줄 수 있습니다. 원문의 세 행을 네트워크 파일 도착 완료 순서라고 설명하지 않습니다.
NEXT · CH7-1
첫 방문은 HTML·RSC·Client JS의 역할이 다릅니다
초기 HTML은 화면을 보여주고, RSC Payload는 서버 결과·Client 참조·props로 트리를 맞추며, Client JavaScript는 해당 컴포넌트를 하이드레이션합니다.
각 행은 전달값의 역할이며 다운로드 완료 순서가 아닙니다. 서버 컴포넌트 자체 코드는 브라우저 번들에 들어가지 않지만 결과는 전달됩니다. 후속 클라이언트 탐색이 첫 방문의 HTML 전달을 그대로 반복하지는 않습니다.
이 예의 조건
- 두 박스는 같은 실행의 진행 정보와 자료·결과를 관찰하는 고정 역할입니다. 박스의 좌우·위아래 위치를 별도 호출 순서로 해석하지 않습니다.
- 조건별 종료는 대안입니다. 실패 뒤 정상 후속 단계를 실행하거나 독립 비교 사례를 같은 요청으로 연결하지 않습니다.
- 서버 컴포넌트는 RSC Payload로 렌더됩니다. Next.js는 이 결과와 클라이언트 컴포넌트로 초기 HTML을 만듭니다. 브라우저는 HTML을 표시하고 RSC Payload로 트리를 맞추며 클라이언트 JavaScript로 해당 컴포넌트의 이벤트를 연결합니다. 세 행은 전송 완료 순서가 아닙니다.
관계도의 보충 설명
| 순번 | 원문 표시 |
|---|---|
| 3 | 첫 방문에서 HTML·RSC·Client JS의 역할은 서로 다릅니다 |
| 4 | 초기 HTML은 화면을 보여 주고, RSC Payload는 서버 결과·Client 참조·props로 트리를 맞추며, Client JavaScript는 해당 컴포넌트를 하이드레이션합니다. |
| 장면 | 관찰 값 | 전환 이유 |
|---|---|---|
| 서버 결과 준비 Server Component를 RSC Payload로 렌더하고 Client Component와 함께 초기 HTML을 준비합니다. | 서버 산출물: RSC·초기 HTML · 논리: 렌더 준비 | 브라우저 첫 표시의 자료가 준비됩니다. |
| 브라우저 첫 표시 브라우저가 HTML을 표시하고 RSC Payload로 트리를 맞춥니다. | 화면: HTML 표시 · 트리: RSC 결합 · 전송 완료 순서: 이 단계의 계약 아님 | 정적인 첫 화면과 컴포넌트 트리가 준비됩니다. |
| Client 이벤트 연결 Client JavaScript가 해당 컴포넌트의 이벤트를 연결합니다. | Client: 이벤트 연결 · Server Component: 브라우저 실행 아님 | Client 경계가 상호작용 가능한 상태로 바뀝니다. |
RSC Payload는 서버 컴포넌트의 렌더 결과, 클라이언트 컴포넌트 위치·모듈 참조, 전달 props를 담습니다. React가 지원하는 직렬화 형식이며 단순히 모든 데이터를 JSON으로 바꾸는 과정은 아닙니다. 후속 클라이언트 탐색에서는 RSC Payload를 받아 트리를 갱신하며 첫 방문의 HTML 전달 과정을 그대로 반복하지 않을 수 있습니다.
서버 컴포넌트 예시 (복습)
이전 장에서 다뤘던 많은 페이지 컴포넌트와 레이아웃 컴포넌트가 이미 서버 컴포넌트로 작성되었습니다.
아래 레이아웃은 children만 받는 일반 대시보드 대안입니다. 앞 장의 병렬 슬롯 실습을 같은 프로젝트에 누적했다면 기존 슬롯 props와 렌더 부분을 유지하며 사용자 표시 부분을 옮깁니다. 파일 전체를 그대로 바꾸면 기존 슬롯 UI를 빼게 됩니다.
// "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를 호출합니다. 원문은 500 ms 지연 뒤 고정된 이름·역할을 반환하는 모의 구현이며 실제 DB 조회나 권한 검증은 구현하지 않았습니다.
getUserInfo와 레이아웃의 서버 계산 자체는 브라우저에서 다시 실행할 코드가 아닙니다. 계산한 이름·역할은 HTML·RSC 결과에 들어가며, <Link>나 포함된 Client 컴포넌트와 Next.js 런타임에는 별도의 브라우저 JavaScript가 필요할 수 있습니다. 서버 코드가 번들에서 빠진다는 말과 페이지 전체의 JavaScript가 0이라는 말은 구분합니다.
서버에 남길 값 판단하기
DB의 모의 결과에 username, role, internalNote가 있고 마지막 값은 공개하면 안 되는 내부 메모라고 가정합니다. 서버 함수에서 읽었다는 이유로 이 객체 전체를 Client 컴포넌트의 props에 넣어도 될까요? 원문의 role이 admin이라는 표시만으로 관리 작업의 실행 권한까지 확인했다고 말할 수 있는지도 설명하세요.
코드 위치와 공개 결과 해설
서버 모듈을 브라우저 번들에서 제외해도 props나 렌더 결과에 넣은 값은 전달됩니다. 화면에 필요한 공개 이름·표시용 역할만 선택하고 내부 메모는 전달하지 않아야 합니다. role 표시도 권한 검증을 실행한 증거가 아닙니다. 실제 관리 작업은 신뢰할 수 있는 서버 경계에서 현재 사용자의 권한과 작업 조건을 검사해야 하지만, 원문은 고정 표시 값을 돌려주는 모의입니다.