Islands 아키텍처와 부분 하이드레이션 — Astro로 JS 90% 줄이면서도 인터랙티브한 웹 만들기
이 글의 전제 조건: Next.js나 React SSR을 한 번이라도 써본 경험이 있다면 바로 따라올 수 있습니다.
Next.js로 SSR을 하고 있는데, 분명 서버에서 HTML을 렌더링하는데도 페이지가 여전히 느리다고 느껴본 적 있으신가요? 저도 처음엔 "SSR 쓰면 빠른 거 아니야?"라고 생각했는데, 알고 보니 SSR 이후에 클라이언트에서 전체 페이지를 다시 하이드레이션하는 비용이 만만치 않았습니다. 블로그 글 하나 보여주는 데 댓글창 컴포넌트, 추천 글 슬라이더, 소셜 공유 버튼 등의 JS까지 전부 다운로드하고 실행해야 하니까요. 낭비죠.
문제를 인식하고 나면, 해결책도 사실 단순합니다. 페이지의 대부분은 그냥 정적 HTML로 두고, 진짜 인터랙션이 필요한 부분에만 JS를 붙이면 됩니다. 이게 바로 Islands 아키텍처의 핵심 발상입니다. 실제로 Wix는 이 방향의 Selective Hydration을 도입해 인터랙션 속도를 40% 개선했습니다. 이 글에서는 Islands 아키텍처의 동작 원리부터 Astro를 활용한 실전 구현, 그리고 실무에서 마주치는 함정까지 한 번에 정리합니다.
2019년 Etsy의 Katie Sylor-Miller가 처음 제안하고 Preact 창시자 Jason Miller가 공식화한 이 패턴은, 2025년 현재 Astro·Qwik·Fresh를 필두로 프론트엔드 주류로 자리 잡았습니다. React Server Components도 서버/클라이언트 경계를 명시적으로 분리한다는 점에서 유사한 방향성을 공유하며, 업계 전반이 이쪽으로 수렴하고 있는 분위기입니다.
핵심 개념
하이드레이션이 뭔데, 왜 문제인가
웹 개발을 하다 보면 "하이드레이션(Hydration)"이라는 단어를 자주 만납니다.
하이드레이션(Hydration): 서버에서 렌더링된 정적 HTML에 JavaScript 이벤트 리스너와 상태를 연결해 인터랙티브하게 만드는 과정. Full Hydration은 페이지 전체를, Partial Hydration(부분 하이드레이션)은 필요한 컴포넌트에만 이 작업을 수행합니다.
문제는 기존 SSR 프레임워크들이 페이지 전체를 하이드레이션한다는 점입니다. 블로그 글 페이지를 예로 들면, 제목·본문·작성일 같은 건 절대 바뀌지 않는 정적 콘텐츠인데도, "이 페이지에 React가 쓰였으니까" 하는 이유로 JS 번들 전체를 다운로드하고 파싱하고 실행합니다. 낭비죠.
Islands 아키텍처의 발상
발상 자체는 직관적입니다. 페이지를 바다(sea of static HTML)라고 보면, 인터랙션이 필요한 컴포넌트들은 그 바다 위에 떠 있는 섬(island)입니다. 섬들은 각자 독립적으로 하이드레이션되고, 나머지 바다는 그냥 HTML입니다.
| 방식 | 하이드레이션 범위 | JS 전송량 |
|---|---|---|
| SPA (Create React App 등) | 페이지 전체 100% | 매우 많음 |
| SSR + Full Hydration (Next.js 기본) | 서버 렌더 후 전체 재수화 | 많음 (RSC 도입 후 개선 중) |
| Islands Architecture | 인터랙티브 컴포넌트만 | 최소화 |
참고로 Next.js 13+ App Router는 RSC를 통해 번들 크기를 상당히 줄였기 때문에, Full Hydration 방식이라도 과거보다 JS 전송량이 많이 개선됐습니다. 그럼에도 콘텐츠 중심 페이지라면, 전체의 8090%가 정적 HTML이 되고 댓글창·장바구니·검색창 같은 나머지 1020%만 island로 처리하는 Islands 아키텍처가 번들 최적화에서 한발 더 앞서 있습니다.
부분 하이드레이션: island를 언제 깨울 것인가
Islands 아키텍처를 구현하는 핵심 메커니즘이 **부분 하이드레이션(Partial Hydration)**입니다. 그리고 이걸 가장 잘 표현한 게 Astro의 client:* 디렉티브입니다. "이 컴포넌트는 언제 JS를 로드해서 활성화할 건지"를 선언적으로 지정할 수 있거든요.
각 island 컴포넌트는 독립적인 JS chunk로 분리되어, 해당 시점이 되면 IntersectionObserver(뷰포트 진입 감지) 또는 requestIdleCallback(유휴 시점 감지) 같은 브라우저 API를 통해 필요한 코드만 로드됩니다. 페이지 전체 번들을 한 번에 내려받는 게 아니라, 섬마다 필요할 때 개별적으로 활성화되는 셈입니다.
| 디렉티브 | 하이드레이션 시점 | 적합한 컴포넌트 |
|---|---|---|
client:load |
페이지 로드 즉시 | 검색창, 네비게이션 |
client:idle |
브라우저 유휴 시 | 소셜 공유 버튼 |
client:visible |
뷰포트 진입 시 | 댓글창, 하단 추천 콘텐츠 |
client:media |
미디어 쿼리 충족 시 | 모바일 전용 메뉴 |
실무에서 이 표를 처음 봤을 때 저는 "그냥 다 client:load 쓰면 안 되나?" 싶었는데, 아래 코드를 직접 짜보고 나서야 디렉티브를 제대로 구분하는 게 얼마나 중요한지 이해됐습니다.
실전 적용
예시 1: Astro로 블로그 포스트 페이지 구성하기
가장 대표적인 활용 사례입니다. 블로그 글 본문은 절대 JS가 필요 없지만, 댓글창과 검색창은 인터랙션이 필요합니다.
---
// src/pages/posts/[slug].astro
import CommentSection from '../../components/CommentSection.tsx';
import SearchBar from '../../components/SearchBar.tsx';
import ShareButton from '../../components/ShareButton.tsx';
const { post } = Astro.props;
---
<html>
<body>
<!-- 정적 영역: JS 전혀 없음, 빠른 FCP 보장 -->
<h1>{post.title}</h1>
<p class="meta">{post.date} · {post.author}</p>
<article set:html={post.content} />
<!-- island: 뷰포트 진입 시 하이드레이션 (스크롤 전까지 JS 불필요) -->
<CommentSection client:visible postId={post.id} />
<!-- island: 페이지 로드 즉시 하이드레이션 (검색은 바로 써야 하니까) -->
<SearchBar client:load />
<!-- island: 브라우저 유휴 시 하이드레이션 (급하지 않음) -->
<ShareButton client:idle url={post.url} />
</body>
</html>각 컴포넌트에 어떤 디렉티브를 붙였는지, 그 이유를 정리하면 이렇습니다.
| 컴포넌트 | 디렉티브 | 이유 |
|---|---|---|
CommentSection |
client:visible |
스크롤해야 보이는 컴포넌트, 미리 로드할 필요 없음 |
SearchBar |
client:load |
사용자가 즉시 사용할 수 있어야 함 |
ShareButton |
client:idle |
중요도 낮음, 메인 스레드 여유 있을 때 처리 |
예시 2: Islands 간 상태 공유 — Nano Stores 활용
솔직히 Islands 아키텍처를 처음 써보면 이 부분에서 막힙니다. Island들은 서로 독립적이라서, 장바구니 island에서 수량을 변경했을 때 헤더 배지 island에 반영하는 게 생각보다 까다롭습니다. 저도 처음에 프롭 드릴링으로 해결하려다 완전히 헛수고를 했던 기억이 있습니다.
이럴 때 경량 상태 관리 라이브러리인 Nano Stores를 사용하면 깔끔하게 해결됩니다. Nano Stores의 atom은 관찰 가능한 값의 최소 단위로, island들이 같은 atom을 구독하면 하나가 값을 바꿀 때 나머지가 자동으로 반응합니다.
// src/stores/cart.ts — island 간 공유 상태
import { atom, computed } from 'nanostores';
export interface CartItem {
id: string;
name: string;
quantity: number;
price: number;
}
export const cartItems = atom<CartItem[]>([]);
// cartItems에서 파생되는 총 수량
export const cartCount = computed(cartItems, (items) =>
items.reduce((sum, item) => sum + item.quantity, 0)
);
export function addToCart(item: CartItem) {
const current = cartItems.get();
const existing = current.find((i) => i.id === item.id);
if (existing) {
// 기존 항목이면 수량만 증가
cartItems.set(
current.map((i) =>
i.id === item.id ? { ...i, quantity: i.quantity + 1 } : i
)
);
} else {
// 새 항목이면 추가
cartItems.set([...current, { ...item, quantity: 1 }]);
}
}// src/components/CartBadge.tsx — 헤더에 붙는 island
import { useStore } from '@nanostores/react';
import { cartCount } from '../stores/cart';
export default function CartBadge() {
const count = useStore(cartCount);
return (
<div className="cart-badge">
🛒 {count > 0 && <span className="badge">{count}</span>}
</div>
);
}// src/components/ProductCard.tsx — 상품 목록의 island
import { addToCart, type CartItem } from '../stores/cart';
interface Props {
product: CartItem;
}
export default function ProductCard({ product }: Props) {
return (
<div className="product-card">
<h3>{product.name}</h3>
<p>{product.price.toLocaleString()}원</p>
{/* 클릭하면 CartBadge가 자동으로 업데이트됨 */}
<button onClick={() => addToCart(product)}>장바구니 담기</button>
</div>
);
}두 island가 같은 store를 구독하고 있어서, ProductCard에서 addToCart를 호출하면 CartBadge가 자동으로 반응합니다. 전역 컨텍스트나 프롭 드릴링 없이 깔끔하게 연결됩니다.
장단점 분석
직접 써보면서 체감한 장단점을 정리하면 이렇습니다.
장점
| 항목 | 내용 | 적합한 시나리오 |
|---|---|---|
| JS 번들 최소화 | 인터랙티브 영역 코드만 전송해 초기 로드 속도 극적 개선 | 콘텐츠 비중이 높은 페이지 |
| Core Web Vitals 향상 | TTI·INP 지표가 개선되어 SEO와 광고 수익에 직접 영향 | 검색 유입이 중요한 서비스 |
| 멀티 프레임워크 | 한 페이지에 React island + Svelte island 혼용 가능 (Astro) | 기술 스택 전환기 |
| 점진적 강화 | JS 비활성화 환경에서도 정적 콘텐츠는 정상 접근 가능 | 접근성 요구사항이 있는 서비스 |
| CDN 캐싱 효율 | 정적 HTML 비율이 높을수록 CDN 캐시 적중률 상승 | 트래픽이 많은 공개 페이지 |
단점 및 주의사항
| 항목 | 내용 | 대응 방안 |
|---|---|---|
| Islands 간 상태 공유 | 독립적인 island 특성상 전역 상태 관리가 복잡해집니다 | Nano Stores 같은 경량 상태 관리 도구를 활용하면 해결됩니다 |
| 적용 범위 제한 | 대시보드, 실시간 협업 툴 같은 복잡한 SPA에는 부적합합니다 | 서비스 성격을 먼저 파악하고 도입 여부를 결정하는 게 낫습니다 |
| 생태계 성숙도 | 기존 React/Next.js 프로젝트에 점진적 적용이 쉽지 않습니다 | Astro·Fresh 같은 전용 프레임워크로 신규 프로젝트부터 시작하는 것이 현실적입니다 |
| 설계 사고 전환 | "무엇이 island인가"를 초기 설계 단계에서 결정해야 합니다 | 컴포넌트별로 "JS 없이 동작 가능한가?"를 먼저 질문하는 게 좋습니다 |
| 실시간 데이터 | 완전 정적화 시 실시간 데이터 갱신에 추가 전략이 필요합니다 | Server Islands나 별도 API 호출 전략을 병행하면 됩니다 |
TTI (Time to Interactive): 페이지가 완전히 인터랙티브해지기까지의 시간. 이 수치가 길면 사용자가 버튼을 클릭해도 반응이 없는 구간이 생깁니다. INP (Interaction to Next Paint) 는 2024년부터 FID를 대체한 Core Web Vitals 지표로, 사용자 인터랙션 이후 다음 화면 갱신까지의 지연을 측정합니다. 두 지표 모두 SEO 순위에 직접 영향을 줍니다.
실무에서 가장 흔한 실수
-
모든 컴포넌트를 island로 만드는 것 — "혹시 나중에 인터랙션 추가할 수도 있으니까"라는 생각으로 island를 남발하면, 결국 Full Hydration과 다를 게 없어집니다. "지금 당장 JS가 필요한가?"를 기준으로 엄격하게 분리하는 게 좋습니다.
-
island 간 직접 통신 시도 — island는 독립적으로 마운트되기 때문에, 부모-자식 프롭 전달 방식으로 island 간 통신을 시도하면 동작하지 않거나 예기치 않은 상태 불일치가 생깁니다. 반드시 공유 store를 통해 소통하는 패턴을 사용하는 게 맞습니다.
-
client:load를 기본값으로 쓰는 것 — 모든 island에client:load를 붙이면 부분 하이드레이션의 이점이 절반 이상 사라집니다. 댓글창처럼 스크롤해야 보이는 컴포넌트는client:visible이 훨씬 적합합니다.
마치며
Islands 아키텍처는 "얼마나 JS를 적게 쓸 수 있는가"라는 질문에서 출발해, 콘텐츠 중심 웹을 훨씬 빠르고 효율적으로 만들어주는 강력한 패턴입니다. 물론 모든 서비스에 맞는 건 아니지만, 블로그·문서·마케팅·이커머스 제품 페이지라면 충분히 검토해볼 만한 가치가 있습니다.
지금 바로 시작해볼 수 있는 3단계:
- Astro로 간단한 블로그 페이지를 만들어보세요 —
pnpm create astro@latest로 프로젝트를 생성한 뒤, 기존에 쓰던 React 컴포넌트를client:visible로 붙여보면 바로 차이를 체감할 수 있습니다. - 현재 서비스의 컴포넌트를 "정적/동적" 두 가지로 분류해보세요 — 화면을 보면서 "이 컴포넌트에서 JS를 제거해도 사용자가 콘텐츠를 볼 수 있는가?"를 기준으로 분류하다 보면, 생각보다 많은 부분이 정적으로 처리 가능하다는 걸 발견하게 됩니다.
- Islands 간 상태가 필요한 지점을 먼저 파악해두세요 — 장바구니 수량, 로그인 상태 같은 공유 상태가 있는 곳에 Nano Stores를 도입하는 연습을 해보면, 실제 프로젝트 적용 시 훨씬 수월하게 진행됩니다.
다음 글: Astro Server Islands 심화 — 개인화 콘텐츠와 인증이 필요한 영역을 메인 렌더 경로 밖으로 분리해 CDN 캐시 적중률을 극대화하는 방법을 다룰 예정입니다.
참고 자료
- Islands Architecture | Jason Miller — 개념의 원저자가 직접 쓴 글, 발상의 배경과 핵심 원리를 가장 명확하게 설명합니다.
- Islands Architecture | Astro 공식 문서 —
client:*디렉티브 동작 방식과 Astro의 구현 철학을 확인하기 좋습니다. - Islands Architecture | Patterns.dev — SPA/SSR과의 차이를 도식으로 비교해주며, 패턴 전반에 대한 중립적 시각을 제공합니다.
- Understanding Astro Islands Architecture | LogRocket — 실제 Astro 프로젝트를 따라가며 배우고 싶을 때 적합합니다.
- Astro Islands Architecture Explained | Strapi — CMS와 Islands를 함께 쓰는 시나리오가 포함되어 콘텐츠 사이트 구축에 참고할 만합니다.
- Modern Front-End Architecture Using Islands and Partial Hydration | NamasteDev — 부분 하이드레이션의 동작 원리를 더 깊게 이해하고 싶을 때 읽어볼 만합니다.
- Partial Hydration: The End of Slow Websites | Feature-Sliced Design — FSD 아키텍처 관점에서 Islands를 바라보는 시각이 흥미롭습니다.
- 40% Faster Interaction: Wix Selective Hydration 사례 | Wix Engineering — 실제 프로덕션에서 수치로 증명된 케이스, 도입 설득 자료로 활용하기 좋습니다.
- Why Islands Architecture Is the Future | DEV Community — 커뮤니티 시각에서 Islands 아키텍처의 미래 전망을 다룹니다.