Astro Islands vs React Server Components: 콘텐츠 사이트냐 앱이냐, 프로젝트 유형별 선택 기준
새 프로젝트를 시작할 때마다 반복되는 질문이 있다. "Next.js App Router로 갈까, 이번엔 Astro를 써볼까?" 솔직히 저도 처음엔 둘 다 "서버에서 렌더링하고 클라이언트 JS를 줄여준다"는 말만 듣고 비슷한 물건인 줄 알았다. 그런데 실제로 프로젝트에 적용해보니 이 두 접근법의 철학은 놀랍도록 달랐다.
2026년 1월 Cloudflare가 Astro를 인수하면서 흥미로운 변화가 생겼다. Astro 6부터는 Cloudflare의 workerd 런타임 위에서 개발 서버가 동작하고, Server Islands가 Cloudflare CDN 엣지와 자연스럽게 통합된다. 쉽게 말해 "정적 셸은 전 세계 CDN 캐시에서 즉시, 동적 개인화 콘텐츠는 엣지 서버에서 지연 주입"이 이제 한 프레임워크 안에서 가능해진다. 한편 React 19가 RSC를 안정화시키면서 양쪽 모두 성숙기에 들어섰다. 이 글을 읽고 나면 두 아키텍처의 실질적 차이를 파악하고, 자신의 프로젝트 성격에 어떤 것이 맞는지 판단할 수 있게 된다.
이 글은 React를 어느 정도 써본 프론트엔드 개발자를 주 독자로 쓰였다. useState, Context, Suspense boundary 같은 React 기본 개념은 알고 있다고 가정한다. React가 완전히 처음이라면 공식 튜토리얼을 먼저 돌고 오면 이 글이 훨씬 잘 읽힌다.
핵심 개념
Astro Islands: 기본값이 제로 JavaScript인 세계
Astro의 아일랜드 아키텍처는 Jason Miller가 제안한 패턴을 구현한 것이다. 발상은 단순하다. 페이지 전체를 정적 HTML로 렌더링하고, 상호작용이 꼭 필요한 컴포넌트만 "섬(Island)"으로 독립 하이드레이션한다.
.astro 파일은 기본적으로 서버에서만 실행된다. 브라우저에 JS를 보내고 싶다면 명시적으로 client:* 디렉티브를 붙여야 한다.
---
// src/pages/index.astro
import Header from '../components/Header.astro'; // 서버 전용, JS 없음
import Counter from '../components/Counter.tsx'; // React 컴포넌트
import Map from '../components/Map.vue'; // Vue 컴포넌트도 섞을 수 있음
---
<html>
<body>
<Header />
<!-- client:visible이 없으면 Counter는 정적 HTML로만 렌더링됨 — React 런타임 미포함 -->
<!-- client:visible: 뷰포트에 들어올 때 하이드레이션 -->
<Counter client:visible />
<!-- client:idle: 브라우저가 한가할 때 하이드레이션 -->
<Map client:idle />
</body>
</html>로딩 전략이 세분화돼 있다는 게 실무에서 꽤 유용하다. 뷰포트 밖에 있는 컴포넌트는 client:visible로 스크롤될 때까지 기다리면 되고, 덜 중요한 위젯은 client:idle로 메인 스레드가 한가할 때 처리하면 된다.
아일랜드 아키텍처 핵심: 각 섬은 독립된 하이드레이션 루트다. React, Vue, Svelte 컴포넌트를 한 페이지에 섞어도 각자가 격리돼 있어 서로 충돌하지 않는다.
Server Islands: 정적 셸과 동적 구멍
Astro 5에서 추가된 server:defer는 한 발 더 나간 개념이다. 기존 Client Island가 "정적 + 클라이언트 JS"였다면, Server Island는 "정적 셸 + 서버에서 동적으로 채워지는 구멍"이다.
---
// src/pages/shop.astro
import ProductGrid from '../components/ProductGrid.astro';
import PersonalizedBanner from '../components/PersonalizedBanner.astro';
---
<html>
<body>
<!-- 캐시 가능한 정적 콘텐츠 -->
<ProductGrid />
<!-- 개인화 콘텐츠: 정적 셸이 먼저 전달되고, 이 부분만 서버에서 지연 로드 -->
<PersonalizedBanner server:defer>
<span slot="fallback">로딩 중...</span>
</PersonalizedBanner>
</body>
</html><span slot="fallback">이 왜 필요한지 궁금할 수 있다. Server Island는 두 단계로 동작한다. 첫 응답에는 정적 셸과 fallback 슬롯이 함께 전달되고, 서버가 개인화 콘텐츠를 생성하면 이 슬롯을 교체한다. RSC의 Suspense boundary와 역할이 비슷하지만 구조가 다르다. RSC의 Suspense는 단일 React 트리 안에서 스트리밍으로 채워지는 반면, Server Island의 fallback은 React와 완전히 무관한 정적 HTML 조각이다 — 하이드레이션 비용이 없다.
JS 없이 동적 콘텐츠를 개인화할 수 있다는 게 Server Islands의 묘미다.
RSC: 단일 React 트리 안의 서버-클라이언트 경계
React Server Components는 같은 React 컴포넌트 문법 안에서 'use client' 디렉티브 하나로 경계를 선언한다. 서버 컴포넌트는 HTML만 스트리밍하고 JS를 전혀 전달하지 않는다.
// app/page.tsx — Server Component (기본값)
import CartButton from './CartButton';
export default async function ProductPage() {
// 서버에서 직접 DB 쿼리. 이 로직은 클라이언트에 노출되지 않음
const products = await db.query('SELECT * FROM products');
return (
<div>
{products.map(p => (
<article key={p.id}>
<h2>{p.name}</h2>
<p>{p.description}</p>
{/* 이 컴포넌트만 클라이언트 JS가 필요 */}
<CartButton productId={p.id} />
</article>
))}
</div>
);
}// app/CartButton.tsx — Client Component
'use client';
import { useState } from 'react';
export default function CartButton({ productId }: { productId: string }) {
const [added, setAdded] = useState(false);
return (
<button onClick={() => setAdded(true)}>
{added ? '장바구니에 담겼습니다' : '장바구니에 담기'}
</button>
);
}Dan Abramov는 overreacted.io의 "RSC for Astro Developers" 글에서 RSC를 "프랙탈 아일랜드" 개념으로 설명했다. 단일 React 트리이기 때문에 클라이언트 컨텍스트 프로바이더가 서버 서브트리 위에 위치할 수 있고, 그 아래 어디서든 컨텍스트를 읽을 수 있다는 의미다.
// app/layout.tsx — 인증 컨텍스트가 전체 트리를 감쌈
import { AuthProvider } from './providers';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html>
<body>
<AuthProvider> {/* Client Component 프로바이더 */}
{children} {/* 하위 Server Component들도 컨텍스트 혜택 */}
</AuthProvider>
</body>
</html>
);
}Astro에서는 섬이 서로 격리돼 있어 이런 전역 상태 공유가 구조적으로 불가능하다. 이게 두 아키텍처의 가장 결정적인 차이다.
'use client'경계 주의사항: 이 경계를 넘는 props는 직렬화 가능해야 한다. 함수나 클래스 인스턴스를 그대로 넘기면 런타임 에러가 난다.Date객체는.toISOString()으로 변환 후 넘기는 식으로 처리하면 된다.
실전 적용
예시 1: 콘텐츠 중심 블로그·문서 사이트 — Astro Islands
콘텐츠가 80%이고 상호작용이 20%인 사이트라면 Astro가 구조적으로 유리하다. 대형 미디어 사이트들이 Astro를 선택하는 이유가 초기 번들 크기와 빌드 성능 차이에 있다는 걸, 아래 코드를 보면서 직접 확인할 수 있다.
---
// src/pages/blog/[slug].astro
import { getCollection } from 'astro:content';
import ShareButton from '../../components/ShareButton.tsx';
import Newsletter from '../../components/Newsletter.tsx';
export async function getStaticPaths() {
const posts = await getCollection('blog');
return posts.map(post => ({ params: { slug: post.slug }, props: { post } }));
}
const { post } = Astro.props;
const { Content } = await post.render();
---
<article>
<h1>{post.data.title}</h1>
<!-- 글 본문: 순수 HTML, JS 없음 -->
<Content />
<!-- 공유 버튼만 하이드레이션 -->
<ShareButton client:visible url={Astro.url.href} />
<!-- 뉴스레터: 메인 스레드 여유 시 로드 -->
<Newsletter client:idle />
</article>| 코드 포인트 | 설명 |
|---|---|
getCollection('blog') |
Content Layer API로 타입 안전하게 콘텐츠 접근 |
client:visible |
뷰포트 진입 시에만 JS 로드, 초기 번들에 미포함 |
client:idle |
메인 스레드 여유 시 로드, 성능 영향 최소화 |
<Content /> 렌더링 |
마크다운 본문은 순수 HTML, React 런타임 없음 |
정적 콘텐츠 비율이 높을수록 Next.js 대비 빌드 시간 차이가 커진다. 벤치마크 리포트들이 일관되게 보고하는 부분이고, 페이지 수가 늘어날수록 체감이 더 분명해진다.
예시 2: SaaS 대시보드 — RSC (Next.js App Router)
잦은 네비게이션, 인증 상태 공유, 실시간 업데이트가 필요한 앱에서는 RSC의 단일 트리가 진가를 발휘한다. 실무에서 자주 맞닥뜨리는 상황인데, 인증 처리와 데이터 페칭을 서버에서 끝내고 브라우저엔 렌더링 결과물만 넘기는 패턴이 얼마나 자연스러운지 직접 보면 이해가 된다.
// app/dashboard/page.tsx — Server Component
import { auth } from '@/lib/auth';
import { RevenueChart } from './RevenueChart';
import { ActivityFeed } from './ActivityFeed';
export default async function DashboardPage() {
// 서버에서 인증 확인, DB 쿼리
const session = await auth();
const metrics = await fetchMetrics(session.userId);
return (
<main>
<h1>{session.user.name}님의 대시보드</h1>
{/* metrics 데이터는 서버에서 계산, 차트 렌더링과 인터랙션만 클라이언트 */}
<RevenueChart data={metrics.revenue} />
<ActivityFeed userId={session.userId} />
</main>
);
}// app/dashboard/RevenueChart.tsx — Client Component
'use client';
import { LineChart, Line, XAxis, YAxis, Tooltip } from 'recharts';
export function RevenueChart({ data }: { data: RevenueData[] }) {
return (
<LineChart data={data} width={600} height={300}>
<XAxis dataKey="date" />
<YAxis />
<Tooltip />
<Line type="monotone" dataKey="amount" stroke="#6366f1" />
</LineChart>
);
}auth() 호출과 DB 쿼리가 서버에서만 일어나고, 브라우저에는 렌더링된 HTML과 차트 인터랙션 코드만 전달된다. AuthProvider가 최상위 레이아웃에 있으니 대시보드 어느 클라이언트 컴포넌트에서든 세션 정보를 컨텍스트로 읽을 수 있다. Astro로 이 패턴을 구현하려면 섬 간 컨텍스트 공유가 안 되기 때문에 완전히 다른 설계가 필요하다.
어떤 프로젝트에 무엇을 쓸까
| 프로젝트 특성 | 추천 선택 | 이유 |
|---|---|---|
| 콘텐츠 비율이 높고 JS 최소화가 최우선 | Astro Islands | 기본값 0KB JS, 빌드 성능 우위 |
| 잦은 페이지 이동 + 전역 상태 (인증, 장바구니) | RSC (Next.js) | 단일 트리에서 Context 자유롭게 공유 |
| 기존 React 팀 + 풀스택 앱 전환 | RSC (Next.js) | 학습 곡선 최소화, 기존 생태계 활용 |
| 다중 프레임워크 혼용 (React + Vue + Svelte) | Astro Islands | 프레임워크 불가지론적 구조 |
| 블로그, 문서 사이트, 마케팅 랜딩 | Astro Islands | 정적 콘텐츠 중심, SEO 최적화 |
| SaaS 대시보드, 어드민, 이커머스 앱 | RSC (Next.js) | 복잡한 상태·네비게이션 관리 |
단순하게 정리하면 이렇다.
- "페이지를 읽는" 사이트 → Astro Islands
- "페이지 안에서 뭔가를 하는" 앱 → RSC
페이지 전환보다 페이지 안 인터랙션이 많고 로그인 상태가 사이트 전체를 흐르는 서비스라면 RSC가 자연스러운 선택이다. 반대로 글 읽기, 문서 검색, 제품 소개가 주 목적이라면 Astro의 제로 JS 기본값이 크게 빛을 발한다.
장단점 분석
장점
Astro Islands
| 항목 | 내용 |
|---|---|
| 제로 JS 기본값 | 정적 페이지는 브라우저에 JS를 전혀 전달하지 않음. Next.js "Hello World"가 ~85-100KB(gzip)인 것과 대조적 |
| 프레임워크 불가지론 | 단일 프로젝트에서 React·Vue·Svelte·Solid 컴포넌트를 혼용 가능 |
| 빌드 성능 | 정적 콘텐츠 비율이 높을수록 Next.js 대비 빌드 시간에서 유리 |
| Server Islands | server:defer로 정적 셸과 동적 콘텐츠를 하이드레이션 없이 혼합 |
| 단순한 멘탈 모델 | .astro(서버)와 Client Island(클라이언트)의 경계가 파일 수준에서 명확 |
RSC (Next.js App Router)
| 항목 | 내용 |
|---|---|
| 단일 React 트리 | 전역 Context가 서버-클라이언트 경계를 가로질러 동작, 인증·테마 공유 용이 |
| SPA급 네비게이션 | 클라이언트 라우팅으로 반복 방문 시 페이지 전환이 부드러움 |
| Server Actions | 별도 API 라우트 없이 폼 처리·데이터 뮤테이션 처리 |
| 성숙한 생태계 | React 19 안정화, 광범위한 엔터프라이즈 채택과 방대한 커뮤니티 |
단점 및 주의사항
Astro Islands
| 항목 | 내용 | 대응 방안 |
|---|---|---|
| 섬 간 상태 공유 불가 | React Context·Zustand 등이 독립 하이드레이션 루트를 넘을 수 없음 | nanostores(프레임워크 불가지론적 경량 상태 관리 라이브러리) 또는 URL 파라미터·LocalStorage 활용 |
| MPA 네비게이션 | 복잡한 SPA 경험 구현이 구조적으로 어려움 | View Transitions API로 완화 가능 (Astro 5 내장) |
| RSC 전용 기능 미지원 | Server Actions 완전 통합 등 React 전용 기능을 그대로 쓸 수 없음 | Astro의 자체 폼 처리 또는 API 엔드포인트 활용 |
RSC (Next.js App Router)
| 항목 | 내용 | 대응 방안 |
|---|---|---|
| 초기 번들 비용 | React 런타임이 항상 포함돼 최소 ~85-100KB(gzip) | 콘텐츠 사이트에 사용 시 Astro 대비 불리함을 감수해야 함 |
| 경계 관리 복잡도 | 'use client'/'use server' 오류는 런타임에서야 발견됨 |
TypeScript strict 모드 + lint 규칙으로 조기 감지 |
| Next.js 종속성 | 완전한 RSC 경험은 사실상 Next.js App Router에 의존 | Waku, TanStack Start 등 대안 고려 |
| Hydration 미스매치 | 서버-클라이언트 불일치 에러 디버깅 난이도 높음 | Suspense boundary를 작게 유지, suppressHydrationWarning 남용 자제 |
Serializable Props 주의: RSC에서 서버 컴포넌트가 클라이언트 컴포넌트에 넘기는 props는 JSON으로 직렬화 가능해야 한다.
Date객체, 함수, 클래스 인스턴스는 경계를 넘을 수 없다.
실무에서 가장 흔한 실수
-
Astro에서 섬 간 상태를 React Context로 공유하려는 시도 — 각 섬은 독립된 하이드레이션 루트이므로 한 섬의 Context는 다른 섬에서 읽힐 수 없다. Astro가 공식 권장하는 nanostores(프레임워크 불가지론적 경량 상태 관리 라이브러리)를 쓰거나, URL 파라미터·LocalStorage를 경유하는 방식으로 해결할 수 있다.
-
RSC에서
'use client'경계를 너무 상위에 선언하는 실수 — 최상위 레이아웃에'use client'를 붙이면 사실상 전통적인 CSR 앱과 다를 바 없어진다. 상태가 필요한 리프 컴포넌트에만'use client'를 붙이고, 서버 컴포넌트 트리를 최대한 깊게 유지하는 것이 RSC를 제대로 활용하는 방법이다. -
콘텐츠 사이트에 Next.js App Router를 고집하는 과스펙 — 블로그나 문서 사이트에 Next.js를 쓰면 전역 상태 공유나 SPA 네비게이션 같은 RSC의 장점을 쓸 일이 거의 없다. 반면 React 런타임 번들 ~85-100KB는 고스란히 부담으로 남는다. 팀이 이미 Next.js 기반이라면 유지가 합리적이지만, 새로 시작하는 콘텐츠 사이트라면 Astro를 진지하게 검토해볼 만하다.
마치며
두 기술 모두 "클라이언트 JS 최소화"라는 같은 방향을 향하고 있지만, 도달하는 방법의 철학적 차이가 크다는 사실이, 선택 기준을 단순한 기능 체크리스트보다 훨씬 중요하게 만든다. Astro는 "섬만 띄운다"는 물리적 분리를, RSC는 "경계를 선언한다"는 논리적 분리를 택했다. 콘텐츠 비율과 전역 상태의 필요도, 이 두 가지가 선택의 핵심이다.
지금 바로 시작해볼 수 있는 3단계:
- 프로젝트 성격 진단 — 기획서나 와이어프레임을 보면서 "읽는 페이지"가 많은지, "하는 페이지"가 많은지 세어보면 방향이 잡힌다. 정적 콘텐츠 + 군데군데 인터랙션이면 Astro, 잦은 페이지 이동 + 공유 상태면 RSC가 자연스러운 출발점이다.
- Astro 빠른 체험 —
pnpm create astro@latest로 공식 블로그 템플릿을 생성하면 Content Layer, Markdown 렌더링,client:visible적용 예시가 바로 보인다. 브라우저 DevTools에서 번들 사이즈를 확인해보면 제로 JS 기본값의 의미가 실감 난다. - RSC 경계 실습 — Next.js App Router 프로젝트에서
'use client'를 리프 컴포넌트에만 붙여보고, React DevTools의 "Server Components" 패널로 어떤 컴포넌트가 서버에서 렌더링되는지 시각적으로 확인해보면 경계 감각이 빠르게 잡힌다.
참고 자료
- RSC for Astro Developers | overreacted (Dan Abramov, 2025.05)
- Server Components vs. Islands Architecture: The performance showdown | LogRocket
- Islands architecture | Astro 공식 문서
- Server Islands | Astro 공식 문서
- Astro Framework 2026: Astro 6, Cloudflare & What Changed | alexbobes.com
- Cloudflare acquires Astro | Cloudflare 공식 블로그 (2026.01)
- What's New With Astro 5? | Peerlist
- React Server Components in Production: Benefits, Pitfalls and Best Practices for 2026 | Growin
- Beyond React: How Astro and Its Server Islands Compare to React Frameworks | The New Stack
- Islands Architecture | patterns.dev
- Astro in 2026: Why It's Beating Next.js for Content Sites | DEV Community
- Next.js vs Remix vs Astro: Which Framework Should You Use in 2026? | AdminLTE.IO