Next.js RSC 프리페치 동작 차이 분석: 정적 빌드 vs 서버 인스턴스 배포
Next.js RSC는 정적 빌드에서 .txt 파일로 페이로드를 생성하고, 서버 환경에서는 동적으로 처리하는데, 이러한 근본적 차이가 성능과 캐싱 전략에 미치는 영향을 심층 분석합니다.
Next.js RSC 프리페치 동작 차이 분석: 정적 빌드 vs 서버 인스턴스 배포
Next.js 애플리케이션을 운영하다 보면 흥미로운 문제에 직면하게 됩니다. 동일한 코드베이스임에도 불구하고 정적 빌드로 배포한 사이트와 서버에서 실행하는 사이트의 성능이 극명하게 다르다는 것입니다.
특히 고트래픽 환경에서 이 차이는 더욱 두드러집니다. 정적 사이트는 CDN의 도움으로 빠르게 로딩되지만, 서버 사이트는 예상보다 느리고 비용도 많이 발생합니다. 그 원인은 무엇일까요?
답은 React Server Components(RSC)의 프리페치 메커니즘에 있습니다. Next.js는 정적 빌드와 서버 인스턴스 환경에서 완전히 다른 방식으로 RSC를 처리하며, 이는 성능 특성과 비용 구조에 근본적인 차이를 만들어냅니다.
목차
- 정적 빌드에서의 RSC 페이로드 처리
- 서버 인스턴스에서의 동적 RSC 처리
- Next.js 내부 아키텍처의 근본적 차이
- 빌드 시스템과 파일 생성 프로세스
- 성능과 네트워크 특성 비교
- 실제 구현 예제와 최적화 전략
- 결론과 권장사항
정적 빌드에서의 RSC 페이로드 처리
.txt 파일 생성 메커니즘과 설계 철학
Next.js의 정적 빌드 환경에서 RSC 페이로드가 .txt 파일로 생성되는 것은 의도적인 설계 결정입니다. GitHub Discussion #59394에 따르면, 원래 .rsc 확장자를 사용했으나 Next.js 커밋 af8963d에서 .txt로 변경되었습니다.
이 변경의 핵심 이유는 호환성과 안정성입니다. 대부분의 웹 서버가 .txt 확장자를 text/plain MIME 타입으로 기본 제공하여 정적 호스팅 환경에서 별도 설정 없이 안정적으로 서빙될 수 있기 때문입니다.
Next.js 소스코드 /packages/next/src/export/routes/app-page.ts에서 확인할 수 있는 파일 생성 프로세스는 다음과 같습니다:
// RSC payload 생성 과정
if (flightData) {
await fileWriter(
ExportedAppPageFiles.FLIGHT,
htmlFilepath.replace(/\.html$/, '.txt'),
flightData
)
}
이 메커니즘은 빌드 시점에 모든 RSC 페이로드를 사전 생성하여 런타임 계산 부담을 제거합니다. https://leeduhan.github.io/posts/react/2025-08-19-html2canvas_cors_guide.txt?_rsc=1ld0r 형태의 URL은 경로 기반 파일 시스템 매핑을 통해 정적 파일로 직접 연결됩니다.
빌드 프로세스와 파일 시스템 구조
next build 실행 시 RSC 파일 생성은 HTML 파일과 병렬로 처리됩니다. GitHub Issue #73427에서 확인된 파일 명명 규칙에 따르면, 경로가 /로 끝나는 경우 .txt를 추가하고, 그 외의 경우 index.txt를 추가합니다.
빌드 후 생성되는 out 디렉토리 구조는 다음과 같습니다:
out/
├── index.html
├── index.txt # 루트 경로의 RSC payload
├── about/
│ ├── index.html
│ └── index.txt # /about 경로의 RSC payload
└── posts/
├── [id].html
└── [id].txt # 동적 라우트의 RSC payload
이러한 구조는 GitHub Pages나 Vercel Static Hosting 같은 정적 호스팅 서비스에서 추가 설정 없이 즉시 배포 가능하며, CDN을 통한 aggressive 캐싱이 가능합니다. Vercel 보안 문서 CVE-2025-49005에 따르면, RSC 페이로드는 Vary: RSC, Next-Router-State-Tree, Next-Router-Prefetch 헤더를 통해 캐시 키 분리를 보장합니다.
서버 인스턴스에서의 동적 RSC 처리
React Flight 프로토콜과 실시간 페이로드 생성
서버 환경에서 https://example-store.com/product/4564?_rsc=1ni0o 형태의 동적 경로는 React Flight 프로토콜을 통해 처리됩니다. Tony Alicea의 RSC 심층 분석에 따르면, 서버 컴포넌트 실행 → Flight 직렬화 → 스트리밍 전송의 3단계로 진행됩니다.
**React Working Group RSC 구현 가이드 (Discussion #5)**에서 설명하는 Flight 프로토콜의 직렬화 과정은 다음과 같습니다:
// 서버에서 JSX.stringify 시 Symbol 치환
JSON.stringify(jsx, (key, value) => {
if (value === Symbol.for('react.element')) {
return '$RE'; // 특수 문자열로 치환
}
return value;
});
// 클라이언트에서 JSON.parse 시 Symbol 복원
JSON.parse(response, (key, value) => {
if (value === '$RE') {
return Symbol.for('react.element');
}
return value;
});
동적 라우팅과 RSC 파라미터 처리
GitHub Issue #65335에서 확인된 바에 따르면, App Router는 RSC: 1 헤더와 _rsc 파라미터 존재 여부를 검사하여 RSC 요청을 구분합니다. Next-Router-State-Tree 헤더를 통해 라우터 상태를 추적하며, 각 요청마다 고유한 RSC 해시를 생성합니다.
⚠️ 치명적인 문제점은 GitHub Discussion #59167에서 보고된 CDN 캐싱 실패입니다. 동일한 제품 페이지임에도 접근 경로에 따라 다른 RSC 해시가 생성되어:
product/299336/?_rsc=1vl30
product/299336/?_rsc=qe3go
product/299336/?_rsc=1vg99
product/299336/?_rsc=1stsw
각각 별도의 서버 요청이 발생하며, CDN을 완전히 우회하여 직접 서버로 전달됩니다. 이는 고트래픽 환경에서 심각한 성능 저하와 비용 증가를 초래합니다.
Next.js 내부 아키텍처의 근본적 차이
Router 구현과 네비게이션 처리
정적 빌드와 서버 환경의 Router 동작 방식은 근본적으로 다릅니다. Next.js 공식 문서에 따르면:
Static Export 환경에서는 모든 라우팅이 클라이언트 사이드에서 처리되며, SPA와 유사한 동작을 보입니다. Next.js 클라이언트 코드 /packages/next/src/client/components/router-reducer/fetch-server-response.ts에서:
if (process.env.NODE_ENV === 'production') {
if (process.env.__NEXT_CONFIG_OUTPUT === 'export') {
if (fetchUrl.pathname === basePath) {
fetchUrl.pathname += '/index.txt'
} else {
fetchUrl.pathname += '.txt'
}
}
}
Server 환경에서는 Server-centric routing을 사용하여 클라이언트가 route map을 다운로드할 필요가 없습니다. Stack Overflow의 Next 13 Server-centric routing 분석에 따르면, 서버에서 라우트 정보를 관리하고 클라이언트는 Link 컴포넌트로 SPA 스타일 네비게이션을 수행합니다.
프리페치 전략의 구조적 차이
Web.dev의 Route Prefetching in Next.js 분석에 따르면, 프리페치 전략에서도 명확한 차이가 있습니다:
| 구분 | Static Export | Server Instance |
|---|---|---|
| 메커니즘 | 파일 기반 (표준 브라우저 prefetch) | API 기반 (?_rsc= 쿼리) |
| 대상 | 정적 HTML/JS/CSS 파일 | 동적 RSC payload |
| 캐싱 | 브라우저/CDN 표준 캐시 | Router Cache (인메모리) |
| 무효화 | 배포 시점에만 | router.refresh(), revalidatePath() |
Next.js 공식 문서에 따르면, Link 컴포넌트의 prefetch 속성은 정적 라우트에서 전체 라우트를 프리페치하지만, 동적 라우트에서는 가장 가까운 loading.js boundary까지만 부분 프리페치합니다.
빌드 시스템과 파일 생성 프로세스
Static Export의 빌드 최적화
next build && next export 과정에서 RSC 페이로드 생성은 app-page.ts에서 다음과 같이 처리됩니다:
await fileWriter(
ExportedAppPageFiles.HTML,
htmlFilepath,
html ?? '',
'utf8'
)
await fileWriter(
ExportedAppPageFiles.FLIGHT,
htmlFilepath.replace(/\.html$/, '.txt'),
flightData
)
GitHub Issue #74445에서 확인된 skipTrailingSlashRedirect 설정 시 문제와 GitHub Issue #73427의 basePath 설정 충돌 문제는 정적 export의 경로 매핑 복잡성을 보여줍니다.
Server 환경의 동적 생성 프로세스
Smashing Magazine의 RSC Forensics 분석에 따르면, 서버 환경에서는 Out-of-Order 스트리밍을 통해 컴포넌트 완료 순서와 관계없이 올바른 위치에 삽입됩니다. Suspense 경계를 활용하여 데이터 로딩 중인 컴포넌트는 fallback으로 대체되고, Promise 해결 시 해당 부분만 업데이트됩니다.
성능과 네트워크 특성 비교
CDN 캐싱 효율성과 서버 부하
YLD.io의 Ledger 프로젝트 사례에서 정적 빌드의 캐시 응답 시간이 서버 응답보다 현저히 짧았습니다. 반면 GitHub Discussion #59167의 고트래픽 프로젝트에서는 동적 RSC의 치명적 문제가 보고되었습니다:
- 💰 100만 요청과 100,000개 이상 제품 처리를 위해 매우 비싼 서버 구성 다수 필요
- 🚫 RSC 요청이 CDN을 완전히 우회하여 직접 서버로 전달
- 📈 Vercel이 AWS나 AWS 래퍼(flightcontrol) 대비 느리고 비용 효율성 낮음
JavaScript 번들 크기와 네트워크 효율성
GeekyAnts 사례에서 RSC 도입으로 JavaScript/TypeScript 코드를 82,926줄에서 43,294줄로 52% 감소 달성했습니다. Lighthouse 점수는 50점에서 90점 이상으로, LCP 2500ms 미만 사용자 비율은 66.89%에서 81.65%로 개선되었습니다.
트레이드오프와 적용 시나리오
| 측면 | Static Export | Server Instance |
|---|---|---|
| 💰 비용 | CDN 호스팅만으로 충분 (저비용) | 서버 인프라 필요 (고비용) |
| ⚡ 성능 | 즉각적 CDN 응답 | TTFB 지연, 서버 부하 |
| 📈 확장성 | 무제한 (CDN 기반) | 서버 리소스에 제한 |
| 🔄 유연성 | 빌드 타임 데이터만 | 실시간 개인화 가능 |
| 🎯 적합 사례 | 마케팅 사이트, 블로그, 문서 | 대시보드, 장바구니, 실시간 앱 |
실제 구현 예제와 최적화 전략
정적 빌드 최적화 설정
// next.config.js
module.exports = {
output: 'export',
images: {
loader: 'custom',
loaderFile: './my-loader.ts',
},
experimental: {
staleTimes: {
dynamic: 0,
static: 300,
}
}
}
하이브리드 컴포넌트 구조
YLD.io 사례의 하이브리드 전략을 적용한 구현:
// Server Component (정적 부분)
export default async function ProductPage({ params }) {
const product = await getStaticProduct(params.id);
return (
<div>
<ProductInfo product={product} />
{/* 민감한 데이터는 클라이언트에서 처리 */}
<UserSpecificContent />
</div>
);
}
// Client Component (동적 부분)
'use client'
function UserSpecificContent() {
// 사용자별 데이터는 클라이언트에서 처리
return <PersonalizedRecommendations />;
}
캐시 헤더 최적화
Vercel 환경에서의 캐시 제어 우선순위:
const headers = {
'Cache-Control': 'public, max-age=31536000, immutable',
'CDN-Cache-Control': 'public, max-age=86400',
'Vercel-CDN-Cache-Control': 'public, max-age=3600'
};
결론과 권장사항
Next.js의 RSC 구현은 정적 빌드와 서버 인스턴스 환경에서 근본적으로 다른 아키텍처를 보여줍니다. 정적 빌드의 .txt 파일 기반 접근은 CDN 친화적이고 확장성이 뛰어나지만 실시간 데이터 처리에 제한이 있습니다. 서버 환경의 동적 RSC 처리는 유연성과 개인화를 제공하지만, GitHub Issue #65335에서 확인된 CDN 캐싱 문제로 인해 고트래픽 환경에서 심각한 성능 저하와 비용 증가를 초래할 수 있습니다.
🎯 고트래픽 환경을 위한 핵심 권장사항
- 예산 제약이 있다면 Pages Router 사용 고려
- 정적 콘텐츠 중심이라면 Static Export 우선 적용
- 하이브리드가 필요하다면 민감하지 않은 데이터만 서버 사이드 캐싱
- 대규모 제품 카탈로그의 경우 RSC 해시 문제로 인한 CDN 비효율성 신중히 검토
RSC의 CDN 캐싱 문제는 Next.js 팀이 추적 중인 확인된 이슈이며, 현재로서는 각 프로젝트의 특성에 맞는 신중한 아키텍처 선택이 필요합니다.
결국 기술의 선택은 트레이드오프의 연속입니다. 정적 빌드의 단순함과 효율성을 취할 것인가, 아니면 서버 사이드의 유연성과 실시간성을 선택할 것인가? 이 글이 여러분의 프로젝트에 맞는 최적의 선택을 하는 데 도움이 되기를 바랍니다.