아티스트 상세 페이지 (/artists/[slug])를 대상으로 React Server Component(RSC)와 Client Component의 실제 번들 크기 및 성능을 비교 분석한 문서입니다.
- RSC 버전:
/[locale]/artists/[slug]/page.tsx - Client Component 버전:
/[locale]/artists-client/[slug]/page.tsx
아티스트 상세 페이지는 다음 데이터를 표시합니다:
- 아티스트 기본 정보 (이름, 프로필 이미지, 소개 등)
- 진행 예정 공연 목록
- 종료된 공연 목록 (있는 경우)
- 앨범 목록 (있는 경우)
- 머천다이즈 목록 (있는 경우)
각 버전 모두 4개의 API 호출을 수행:
fetchArtistBySlug()- 아티스트 정보fetchGigs()- 과거 공연 존재 여부 확인fetchAlbums()- 앨범 존재 여부 확인fetchMerchItems()- 머치 존재 여부 확인
두 버전 모두 동일한 기본 번들을 공유합니다:
| 파일 | 크기 | 설명 |
|---|---|---|
a6dad97d9634a72d.js |
110KB | Polyfills |
9f4008469d0c7cdf.js |
215KB | React + Next.js 런타임 |
96dbdc0078c3e232.js |
82KB | 공통 라이브러리 |
d4be46ecc2605493.js |
40KB | Next.js 클라이언트 |
248cba0fcb3874ed.js |
38KB | React Query |
78850c5b62a862e4.js |
20KB | 유틸리티 |
turbopack-*.js |
9.7KB | Turbopack 런타임 |
cd173717bc1c72a9.js |
4.2KB | 설정 |
| 합계 | 536KB |
공통 레이아웃 청크:
├─ 5bab93f39577e409.js 3.0KB - Analytics
├─ fb16855ef95dc533.js 27KB - Third-party scripts
├─ 65e700a5ef102917.js 50KB - 레이아웃 컴포넌트
└─ d8df01aacc27c068.js 80KB - QueryProvider, Theme 등
페이지 전용 청크:
├─ 45ec2a5ddad1be49.js 19KB - 공통 UI 컴포넌트
└─ 1fcb320cb283c4ab.js 33KB - 아티스트 페이지 Client 컴포넌트
(ArtistProfileHeader,
AlbumsByArtist,
MerchItemsByArtist)
총 페이지별 추가: ~220KB
공통 레이아웃 청크:
├─ 5bab93f39577e409.js 3.0KB - Analytics
├─ fb16855ef95dc533.js 27KB - Third-party scripts
├─ 65e700a5ef102917.js 50KB - 레이아웃 컴포넌트
└─ d8df01aacc27c068.js 80KB - QueryProvider, Theme 등
페이지 전용 청크:
├─ 45ec2a5ddad1be49.js 19KB - 공통 UI 컴포넌트
└─ bc4854728eaababe.js 38KB - Client Component 페이지 + 데이터 페칭
(페이지 레벨 로직,
GigsByArtistClient,
모든 하위 컴포넌트)
총 페이지별 추가: ~258KB
| 항목 | RSC 버전 | Client Component 버전 | 차이 |
|---|---|---|---|
| 기본 번들 | 536KB | 536KB | 0KB |
| 페이지별 청크 | 220KB | 258KB | +38KB |
| 총 클라이언트 번들 | 756KB | 794KB | +38KB (+5%) |
-
이미 Client Component로 구현된 하위 컴포넌트
ArtistProfileHeader- "use client"AlbumsByArtist- "use client" + React QueryMerchItemsByArtist- "use client" + React QueryScrollToTop- "use client"
→ RSC 버전에서도 이미 클라이언트 번들에 포함됨
-
공유되는 라이브러리
- React Query는 전역적으로 사용
clientSideAPIRequest.ts는 다른 페이지에서도 사용- openapi-fetch는 전역 의존성
→ 추가 비용 없음
-
실제 추가되는 코드
- 페이지 레벨의 데이터 페칭 로직 (~10KB)
useEffect,useState등 상태 관리 (~5KB)GigsByArtistClient컴포넌트 (~15KB)- 로딩/에러 상태 처리 (~8KB)
→ 총 ~38KB
'use client'를 붙인 Client Component도 초기 렌더링은 서버에서 수행됩니다. 차이점은:
- RSC: 서버에서만 실행, 컴포넌트 코드가 클라이언트로 전송되지 않음
- Client Component with SSR: 서버에서 초기 렌더링 + 클라이언트에서 hydration
타임라인:
┌──────────────────────────────────────────────────────────────┐
│ 1. 브라우저 요청 → 서버 │
│ ⏱️ 0ms │
├──────────────────────────────────────────────────────────────┤
│ 2. 서버에서 4개 API 호출 (병렬) │
│ - fetchArtistBySlug() │
│ - fetchGigs() (과거 공연 체크) │
│ - fetchAlbums() (앨범 체크) │
│ - fetchMerchItems() (머치 체크) │
│ ⏱️ 50-200ms (서버 간 통신, 매우 빠름) │
├──────────────────────────────────────────────────────────────┤
│ 3. 서버에서 React 컴포넌트 렌더링 │
│ - HTML 생성 (RSC 페이로드) │
│ - 모든 데이터가 포함됨 │
│ ⏱️ 20-50ms │
├──────────────────────────────────────────────────────────────┤
│ 4. HTML을 클라이언트로 전송 │
│ ⏱️ 50-100ms (네트워크 속도에 따라) │
├──────────────────────────────────────────────────────────────┤
│ 5. 브라우저에서 HTML 파싱 및 표시 │
│ ⏱️ 10-30ms │
├──────────────────────────────────────────────────────────────┤
│ 6. Client Component만 Hydration (최소화) │
│ - ArtistProfileHeader, AlbumsByArtist 등만 │
│ ⏱️ 30-80ms │
└──────────────────────────────────────────────────────────────┘
총 소요 시간: 160-460ms
사용자가 콘텐츠를 보는 시점: ~300ms ✅
Interactive 시점: ~400ms
타임라인:
┌──────────────────────────────────────────────────────────────┐
│ 1. 브라우저 요청 → 서버 │
│ ⏱️ 0ms │
├──────────────────────────────────────────────────────────────┤
│ 2. 서버에서 4개 API 호출 (병렬) │
│ - fetchArtistBySlug() │
│ - fetchGigs() (과거 공연 체크) │
│ - fetchAlbums() (앨범 체크) │
│ - fetchMerchItems() (머치 체크) │
│ ⏱️ 50-200ms (동일) │
├──────────────────────────────────────────────────────────────┤
│ 3. 서버에서 React 컴포넌트 렌더링 │
│ - HTML 생성 (데이터 포함) │
│ ⏱️ 20-50ms │
├──────────────────────────────────────────────────────────────┤
│ 4. HTML + Client Component JS를 클라이언트로 전송 │
│ ⏱️ 50-100ms (네트워크 속도에 따라) │
├──────────────────────────────────────────────────────────────┤
│ 5. 브라우저에서 HTML 파싱 및 표시 │
│ ⏱️ 10-30ms │
├──────────────────────────────────────────────────────────────┤
│ 6. 모든 Client Component Hydration (+38KB) │
│ - JS 다운로드 (페이지 + 하위 컴포넌트) │
│ - JS 파싱 및 실행 │
│ - React 이벤트 리스너 연결 │
│ - State 초기화 │
│ ⏱️ 100-250ms (RSC보다 70-170ms 더 오래 걸림) │
└──────────────────────────────────────────────────────────────┘
총 소요 시간: 230-630ms
사용자가 콘텐츠를 보는 시점: ~300ms ✅ (RSC와 동일)
Interactive 시점: ~550ms ⚠️ (RSC보다 150ms 느림)
둘 다 SSR이 되므로 초기 콘텐츠 표시 시간은 거의 동일합니다.
하지만 Interactive(상호작용 가능) 시점에서 차이가 발생합니다:
RSC:
페이지 구조:
├─ page.tsx (Server Component) ← Hydration 불필요
│ ├─ ArtistProfile (Server) ← Hydration 불필요
│ ├─ GigsByArtist (Server) ← Hydration 불필요
│ ├─ ArtistProfileHeader (Client) ← Hydration 필요
│ ├─ AlbumsByArtist (Client) ← Hydration 필요
│ └─ MerchItemsByArtist (Client) ← Hydration 필요
Hydration 대상: 일부 인터랙티브 컴포넌트만 (~30-80ms)
번들 크기: 756KB
Client Component:
페이지 구조:
├─ page.tsx (Client Component) ← Hydration 필요
│ ├─ ArtistProfile (Client) ← Hydration 필요
│ ├─ GigsByArtistClient (Client) ← Hydration 필요
│ ├─ ArtistProfileHeader (Client) ← Hydration 필요
│ ├─ AlbumsByArtist (Client) ← Hydration 필요
│ └─ MerchItemsByArtist (Client) ← Hydration 필요
Hydration 대상: 전체 페이지 트리 (~100-250ms)
번들 크기: 794KB (+38KB)
서버에서 생성된 HTML에 JavaScript를 "붙이는" 과정:
// Hydration 단계:
1. HTML이 이미 화면에 표시됨 (보이지만 클릭 안 됨)
2. JS 다운로드 (네트워크)
3. JS 파싱 (CPU)
4. React 가상 DOM 생성 (메모리)
5. 서버 HTML과 매칭 (비교)
6. 이벤트 리스너 연결 (클릭 가능해짐)더 많은 컴포넌트 = 더 오래 걸리는 Hydration
측정 지점별 비교:
| 시점 | RSC | Client Component | 차이 |
|---|---|---|---|
| HTML 표시 | ~300ms | ~300ms | 동일 ✅ |
| Hydration 시작 | ~330ms | ~330ms | 동일 |
| Hydration 완료 | ~400ms | ~550ms | +150ms |
| 완전히 Interactive | ~400ms | ~550ms | +150ms |
RSC:
0ms ────────► 300ms ────────► 400ms
로딩 콘텐츠 보임 클릭 가능
(거의 동시에 Interactive)
- 콘텐츠가 보이면 거의 바로 클릭 가능
- 부드러운 경험
Client Component:
0ms ────────► 300ms ────────────────► 550ms
로딩 콘텐츠 보임 클릭 가능
(150ms 동안 클릭 불가)
- 콘텐츠는 보이지만 클릭이 안 됨
- "버튼이 작동 안 해요!" 느낌
추가 38KB의 영향:
네트워크 속도별 다운로드 시간:
- 4G (10Mbps): +30ms
- 3G (3Mbps): +100ms
- Slow 3G (1Mbps): +300ms
파싱/실행 시간:
디바이스별 JS 파싱 시간 (38KB):
- 고사양 폰: +20ms
- 중급 폰: +50ms
- 저사양 폰: +120ms
→ 저사양 기기와 느린 네트워크에서 차이가 더 커짐
| 지표 | RSC | Client Component (SSR) | 차이 |
|---|---|---|---|
| TTFB (Time to First Byte) | 150-300ms | 150-300ms | 동일 |
| FCP (First Contentful Paint) | 250-400ms | 250-400ms | 동일 ✅ |
| LCP (Largest Contentful Paint) | 400-600ms | 400-600ms | 동일 ✅ |
| TTI (Time to Interactive) | 400-500ms | 550-700ms | RSC가 150ms 빠름 |
| TBT (Total Blocking Time) | 50-100ms | 150-300ms | RSC가 100-200ms 빠름 |
| CLS (Cumulative Layout Shift) | 낮음 | 낮음 | 동일 ✅ |
핵심 발견:
- 초기 콘텐츠 표시(FCP, LCP)는 둘 다 SSR이므로 동일
- 인터랙티브(TTI, TBT)에서 RSC가 우수 (Hydration 최소화)
- CLS도 둘 다 좋음 (둘 다 SSR)
| 항목 | RSC | Client Component (SSR) | 차이 |
|---|---|---|---|
| 콘텐츠 표시 시점 | ~300ms | ~300ms | 동일 ✅ |
| 클릭 가능 시점 | ~400ms | ~550ms | RSC가 150ms 빠름 |
| Hydration 지연 | 짧음 (30-80ms) | 김 (100-250ms) | 2-3배 차이 |
| Layout Shift | 최소 | 최소 | 동일 ✅ |
| 체감 속도 | 매우 빠름 ✅ | 빠름 (약간 지연) |
주의:
- 사용자는 콘텐츠를 보자마자 클릭하려고 함
- Client Component는 "보이는데 클릭이 안 되는" 150ms 동안 불편함
- 이 현상을 "Uncanny Valley" 효과라고 부름
요청 흐름:
┌─────────┐ ┌─────────┐ ┌─────────┐
│ Client │────1────│ Server │────4────│ Strapi │
└─────────┘ └─────────┘ └─────────┘
↓
(병렬 처리)
↓
┌─────────┐
│ HTML │
│ + Data │
└─────────┘
클라이언트가 받는 것:
✅ 완전한 HTML (모든 데이터 포함)
✅ 즉시 렌더링 가능
✅ SEO 완벽 지원
네트워크 통계:
- 클라이언트 → 서버: 1회
- 서버 → Strapi: 4회 (서버 간 통신, 1-5ms)
- 총 클라이언트 대기 시간: ~300ms
요청 흐름:
┌─────────┐ ┌─────────┐ ┌─────────┐
│ Client │────1────│ Server │────4────│ Strapi │
└─────────┘ └─────────┘ └─────────┘
↓
(병렬 처리)
↓
┌─────────┐
│ HTML │
│ + Data │
│ + 더많은JS│
└─────────┘
클라이언트가 받는 것:
✅ 완전한 HTML (모든 데이터 포함) - RSC와 동일
✅ 즉시 렌더링 가능 - RSC와 동일
⚠️ SEO 지원 (generateMetadata 없음)
⚠️ Hydration 전체 페이지 (모든 컴포넌트)
네트워크 통계:
- 클라이언트 → 서버: 1회 (RSC와 동일!)
- 서버 → Strapi: 4회 (동일!)
- 클라이언트 JS 다운로드: 794KB (+38KB)
- 총 클라이언트 대기 시간: ~300ms (동일!)
- **하지만 Hydration 시간: +150ms**
이전 분석의 오류를 수정합니다:
잘못된 가정:
- ❌ Client Component가 클라이언트에서 API 호출한다
- ❌ Client Component가 4번의 추가 왕복을 한다
올바른 이해:
- ✅ 둘 다 서버에서 데이터 페칭 (SSR)
- ✅ 둘 다 1번의 HTML 요청
- ✅ 네트워크 레이턴시는 동일
⚠️ 차이는 Hydration overhead뿐
모바일 네트워크(4G)에서의 평균 레이턴시: 50-100ms
| 시나리오 | RSC | Client Component (SSR) | 차이 |
|---|---|---|---|
| WiFi (10ms) | 320ms → 400ms | 320ms → 550ms | +150ms (Hydration) |
| 4G (50ms) | 360ms → 440ms | 360ms → 620ms | +180ms (Hydration + 38KB) |
| 3G (100ms) | 410ms → 510ms | 410ms → 730ms | +220ms (Hydration + 38KB) |
→ 네트워크가 느릴수록 추가 JS(+38KB) 다운로드 시간이 길어져 차이가 커짐
<!-- 서버에서 생성된 HTML -->
<html>
<head>
<title>아티스트명 | 인디스트릿</title>
<meta property="og:title" content="아티스트명 | 인디스트릿" />
<meta property="og:image" content="https://..." />
<meta name="description" content="아티스트 소개..." />
</head>
<body>
<h1>아티스트명</h1>
<div class="artist-profile">
<img src="..." alt="프로필 이미지" />
<p>아티스트 소개 내용...</p>
</div>
<section class="upcoming-gigs">
<h2>진행 예정 공연</h2>
<div class="gig-item">...</div>
<!-- 실제 공연 데이터 -->
</section>
</body>
</html>크롤러가 보는 것:
- ✅ 완전한 콘텐츠
- ✅ 모든 메타데이터
- ✅ 구조화된 데이터
- ✅ Open Graph 이미지
<!-- 서버에서 생성된 HTML -->
<html>
<head>
<title>인디스트릿</title>
<!-- generateMetadata가 없어서 동적 메타데이터 없음 -->
</head>
<body>
<h1>아티스트명</h1>
<div class="artist-profile">
<img src="..." alt="프로필 이미지" />
<p>아티스트 소개 내용...</p>
</div>
<section class="upcoming-gigs">
<h2>진행 예정 공연</h2>
<div class="gig-item">...</div>
<!-- 실제 공연 데이터 - SSR로 포함됨! -->
</section>
<script src="bundle.js"></script>
</body>
</html>크롤러가 보는 것:
- ✅ 완전한 콘텐츠 (SSR이므로)
⚠️ 메타데이터 불완전 (generateMetadata 없음)- ❌ Open Graph 미리보기 불가
- ✅ 콘텐츠 자체는 크롤링 가능
콘텐츠 크롤링:
- 둘 다 SSR이므로 콘텐츠 자체는 크롤링 가능 ✅
메타데이터:
- RSC:
generateMetadata로 동적 메타 태그 생성 ✅ - Client Component:
generateMetadata사용 불가 (Client Component에서 지원 안 함) ❌
| 검색 엔진 | RSC | Client Component (SSR) |
|---|---|---|
| ✅ 완벽 | ||
| Naver | ✅ 완벽 | |
| Bing | ✅ 완벽 | |
| DuckDuckGo | ✅ 완벽 |
업데이트: 둘 다 SSR이므로 콘텐츠는 크롤링되지만, Client Component는 동적 메타데이터가 없어 검색 결과에서 불리함.
| 플랫폼 | RSC | Client Component (SSR) |
|---|---|---|
| ✅ 완벽한 미리보기 | ❌ 미리보기 없음 (OG 태그 없음) | |
| ✅ 완벽한 카드 | ❌ 카드 없음 (메타 태그 없음) | |
| KakaoTalk | ✅ 완벽한 미리보기 | ❌ 미리보기 없음 (OG 태그 없음) |
| Slack | ✅ 완벽한 미리보기 | ❌ 미리보기 없음 (OG 태그 없음) |
중요: 소셜 미디어는 Open Graph 태그에 의존하므로, generateMetadata가 없는 Client Component는 미리보기가 전혀 표시되지 않음.
요청당 서버 작업:
1. 4개 API 호출 (Strapi)
2. React 컴포넌트 렌더링
3. HTML 생성 및 전송
CPU 사용량: 높음 (렌더링)
메모리 사용량: 중간
응답 시간: 150-400ms
장점:
- 클라이언트 부하 최소화
- 캐싱 가능 (ISR, CDN)
단점:
- 서버 CPU 사용량 높음
- 동시 요청 많으면 서버 부하 증가
요청당 서버 작업:
1. 빈 HTML 전송 (초기)
2. 4개 API 프록시 요청
CPU 사용량: 낮음
메모리 사용량: 낮음
응답 시간: 50-100ms (초기) + 4 × API 응답 시간
장점:
- 서버 부하 낮음
- 확장성 좋음
단점:
- 클라이언트 부하 높음
- API 서버 부하는 동일
- 네트워크 비용 증가
-
공개 콘텐츠 페이지
- 아티스트 상세 페이지 (현재 페이지)
- 공연 상세 페이지
- 앨범 상세 페이지
- 공연장 상세 페이지
- 검색 결과 페이지
이유: SEO가 중요하고, 초기 로딩 속도가 사용자 경험에 직결됨
-
목록 페이지
- 아티스트 목록
- 공연 목록
- 앨범 목록
이유: 많은 데이터를 빠르게 표시해야 함
-
정적 콘텐츠
- About 페이지
- 이용약관
- 개인정보처리방침
이유: 변경 빈도 낮고 SEO 중요
-
실시간 인터랙션이 많은 페이지
- 채팅 페이지
- 실시간 대시보드
- 게임, 투표 등
이유: 클라이언트 상태 관리가 중요
-
인증 필요 페이지
- 사용자 프로필 편집
- 관리자 대시보드
- 설정 페이지
이유: SEO 불필요, 사용자별 데이터
-
클라이언트 전용 기능
- 캔버스/차트 렌더링
- 복잡한 애니메이션
- WebSocket 연결
이유: 서버에서 렌더링 불가능
페이지: Server Component 하위 인터랙티브 컴포넌트: Client Component
// page.tsx (Server Component)
export default async function ArtistPage() {
const artist = await fetchArtistBySlug({ slug });
return (
<div>
{/* Client Component: 인터랙션 필요 */}
<ArtistProfileHeader artist={artist} />
{/* Server Component: 데이터 페칭 */}
<Suspense fallback={<GigsSkeleton />}>
<GigsByArtist artistId={artist.id} />
</Suspense>
{/* Client Component: React Query 사용 */}
<AlbumsByArtist artistId={artist.id} />
</div>
);
}장점:
- SEO 완벽 지원
- 초기 로딩 빠름
- 필요한 곳만 인터랙티브
- 번들 크기 최적화
- 둘 다 SSR이 적용되므로 초기 HTML 로딩 속도는 동일
- 사용자가 콘텐츠를 보는 시점: ~300ms (동일)
- 이전 분석의 오류: Client Component도 서버에서 데이터 페칭하고 렌더링함
- RSC: ~400ms (Hydration 최소화)
- Client Component: ~550ms (전체 페이지 Hydration)
- 차이의 원인: Hydration overhead
- RSC는 인터랙티브 컴포넌트만 Hydration
- Client Component는 전체 페이지 트리를 Hydration
- Client Component가 약간 더 크지만, 체감상 큰 차이는 아님
- 대부분의 코드는 이미 Client Component로 구현되어 공유됨
- 저사양 기기나 느린 네트워크에서는 이 차이가 누적됨
- RSC:
generateMetadata로 동적 메타 태그 생성 가능 - Client Component:
generateMetadata사용 불가 - 콘텐츠는 둘 다 크롤링되지만, Open Graph 미리보기는 RSC만 가능
- 소셜 미디어 공유 시 큰 차이
- 둘 다 SSR이므로 서버 부하는 비슷
- RSC가 약간 더 최적화된 렌더링 가능
- CDN 캐싱으로 두 경우 모두 완화 가능
아티스트 상세 페이지는 RSC가 최적입니다.
이유:
- SEO와 소셜 미디어 공유가 매우 중요 (가장 큰 차이점)
- Open Graph 미리보기는 RSC만 가능
- 동적 메타데이터는 필수
- Interactive 시점이 150ms 빠름
- 사용자가 클릭하려고 할 때 이미 준비됨
- "보이는데 안 눌러져요" 현상 방지
- 번들 크기 최적화 (+5% 절감)
- 저사양 기기와 느린 네트워크에 유리
- Hydration overhead 최소화
- 필요한 부분만 Interactive하게
- 초기 콘텐츠 표시는 동일하지만, 전반적인 UX는 우수
현재 RSC 구조에서 더 개선할 수 있는 부분:
-
Suspense 경계 최적화
// 각 섹션을 Suspense로 분리하여 점진적 렌더링 <Suspense fallback={<Skeleton />}> <GigsByArtist /> </Suspense> <Suspense fallback={<Skeleton />}> <AlbumsByArtist /> </Suspense>
-
ISR (Incremental Static Regeneration) 적용
export const revalidate = 60; // 60초마다 재검증
-
CDN 캐싱 활용
- 자주 조회되는 아티스트 페이지는 CDN에 캐싱
- 서버 부하 대폭 감소
-
이미지 최적화
- Next.js Image 컴포넌트 활용 (이미 적용 중)
- WebP 포맷 사용
- Lazy loading
이번 분석에서 수정된 주요 오류:
이전 분석에서는 Client Component가 클라이언트에서 API를 호출한다고 잘못 가정했습니다. 하지만 실제로는:
- ✅ 둘 다 SSR이 적용됨
- ✅ 둘 다 서버에서 데이터 페칭
- ✅ 초기 콘텐츠 표시 속도는 동일
진짜 차이점:
-
Hydration overhead (150ms 차이)
- RSC: 인터랙티브 컴포넌트만
- Client Component: 전체 페이지
-
SEO와 메타데이터 (가장 큰 차이)
- RSC:
generateMetadata가능 - Client Component: 불가능
- RSC:
-
번들 크기 (38KB, 5% 차이)
- 작지만 저사양 기기에서 누적됨
결론:
인디스트릿의 아티스트 상세 페이지는 React Server Component로 구현하는 것이 최선의 선택입니다.
특히 소셜 미디어 공유와 SEO가 중요한 공개 콘텐츠에서는 RSC가 필수입니다.
작성일: 2025-10-30
빌드 버전: Next.js 16.0.1 (Turbopack)
분석 대상: /[locale]/artists/[slug]/page.tsx vs /[locale]/artists-client/[slug]/page.tsx