Split View: Astro 5 심층 해부: Server Islands · Content Layer · View Transitions로 다시 그리는 '콘텐츠 사이트의 표준'
Astro 5 심층 해부: Server Islands · Content Layer · View Transitions로 다시 그리는 '콘텐츠 사이트의 표준'
"정적이라는 단어를 다시 정의하자. 캐시할 수 있는 모든 것은 캐시하고, 진짜로 사람마다 달라야 하는 부분만 서버에서 늦게 그린다." — Astro 5 Server Islands의 디자인 한 줄 요약
프롤로그 — '단순한 정적 사이트 생성기'의 시대는 끝났다
2022년의 Astro는 마크다운 블로그용 정적 사이트 생성기였습니다. 2024년 12월 3일에 공개된 Astro 5는, 같은 이름을 달고 있지만 다른 물건입니다. 콘텐츠 중심 사이트의 사실상 표준 이라고 불러도 과하지 않습니다.
이번 글에서 답하고 싶은 질문은 단순합니다.
- Astro 5는 정확히 무엇이 바뀌었는가
- Server Islands와 Content Layer는 어떤 문제를 푸는가
- 같은 카드에서 싸우는 Next.js의 RSC · Server Actions, SvelteKit과 비교하면 무엇이 다른가
- 콘텐츠 사이트, 마케팅 사이트, 문서, 커머스 스토어프런트 — 어디서 Astro가 이기고, 어디서 지는가
- Astro 4에서 5로 어떻게 옮겨가는가
이 글은 광고가 아닙니다. Astro가 잘하는 일과 못하는 일을 같은 무게로 적습니다. 결정은 마지막 표 한 장에서 하시면 됩니다.
1. 아일랜드 아키텍처 복습 — Astro가 처음부터 다르게 했던 한 가지
먼저 짧게 복습합니다. 다른 프레임워크가 SPA에서 SSR로 갔다가 다시 RSC로 우회하는 동안, Astro는 처음부터 다른 길을 갔습니다.
1.1 "모든 페이지는 HTML이다"
기본 출력물은 HTML입니다. JavaScript는 필요한 컴포넌트에만 명시적으로 붙입니다. 이 정책의 이름이 바로 아일랜드 아키텍처(Islands Architecture) 입니다.
페이지 전체가 하나의 React 트리가 아니라, 정적인 HTML 바다 위에 인터랙티브한 "섬"이 듬성듬성 떠 있는 그림입니다. 섬마다 자기 번들과 자기 하이드레이션 시점을 가집니다. 그 결과 평균적인 콘텐츠 페이지에서 클라이언트로 내려가는 JS가 압도적으로 적습니다.
1.2 클라이언트 디렉티브
섬을 만드는 방법은 컴포넌트 옆에 디렉티브 한 줄을 붙이는 것입니다.
---
import Counter from '../components/Counter.tsx'
import LikeButton from '../components/LikeButton.svelte'
import SearchBox from '../components/SearchBox.vue'
---
<h1>섬 없는 텍스트는 정적 HTML</h1>
<Counter client:load />
<LikeButton client:visible />
<SearchBox client:idle />
client:load— 페이지 로드 시 즉시 하이드레이트client:idle— 브라우저가 한가할 때client:visible— 뷰포트에 들어올 때client:media="(max-width: 600px)"— 미디어 쿼리 충족 시client:only="react"— 서버에서 렌더하지 않고 클라이언트에서만
이 한 줄의 차이로 "이 페이지의 4킬로바이트 JS"와 "이 페이지의 300킬로바이트 JS"가 갈립니다.
1.3 프레임워크 어그노스틱
같은 페이지 안에 React, Svelte, Vue, Solid, Preact, Lit을 섞을 수 있습니다. 실무에서 이걸 적극적으로 활용하는 팀은 드물지만, 레거시 위젯을 점진적으로 흡수할 수 있다는 의미는 큽니다. 디자인 시스템은 Svelte, 인터랙티브 데모는 React 같은 분할이 자연스럽습니다.
2. Astro 5가 가져온 큰 그림 — 6가지 헤드라인 변화
Astro 5의 핵심을 한 화면에 담으면 다음과 같습니다.
| 영역 | Astro 4 | Astro 5 |
|---|---|---|
| 콘텐츠 모델 | Content Collections (파일 기반) | Content Layer API (어떤 소스든) |
| 동적 페이지 | 전체 페이지 SSR 또는 정적 | Server Islands (정적 위에 서버 섬) |
| 번들러 | Vite 5 | Vite 6, 이후 6.x 라인에서 Vite 7 합류 |
| 폼/뮤테이션 | 직접 핸들러 | Astro Actions (타입 안전) |
| 라우팅 | 정적 + 부분 SSR | prerender per-route 정교화 |
| i18n | 실험적 | 안정화 + fallback/도메인 라우팅 |
이걸 한 줄로 줄이면 — "정적 우선을 유지한 채로, 풀스택의 일부를 받아들였다" 입니다. Next.js와 반대 방향에서 같은 지점에 도달하는 중입니다. Next.js는 클라이언트 React에서 출발해 RSC로 정적/서버 경계를 다시 그렸고, Astro는 정적 HTML에서 출발해 Server Islands로 서버를 다시 받아들였습니다.
3. Server Islands — '정적 우선'을 깨지 않고 개인화를 끼워 넣는 법
Server Islands는 Astro 5의 얼굴입니다. 한 문장으로:
"페이지를 CDN에서 정적으로 캐시하고, 사람마다 달라야 하는 작은 조각만 서버에서 늦게 그려서 클라이언트가 뒤따라 끼워 넣는다."
3.1 문제 — "캐시할 수 있는 99%와 캐시할 수 없는 1%"
전형적인 콘텐츠 사이트의 페이지를 떠올려 보세요. 헤더, 본문, 사이드바, 푸터 — 거의 모든 픽셀이 모든 방문자에게 동일합니다. 다만 우측 상단의 "로그인됨/안 됨", "장바구니 (3)", "내 추천 글" 같은 작은 조각이 사람마다 다릅니다.
전통적 선택지:
- 전체 페이지 SSR — 1%를 위해 99%를 매번 다시 그린다. 캐시가 무용지물.
- 전체 정적 + 클라이언트 fetch — JS 로드 → fetch → 렌더링 → 레이아웃 시프트. 코어 웹 바이탈 손상.
- ESI / Edge Side Includes — 이론적으론 좋지만 CDN 의존, 디버깅 지옥.
Server Islands는 4번째 길입니다.
3.2 사용법 — server:defer 한 줄
---
// src/pages/index.astro
import Layout from '../layouts/Layout.astro'
import Avatar from '../components/Avatar.astro'
import AvatarFallback from '../components/AvatarFallback.astro'
---
<Layout>
<h1>오늘의 추천 글</h1>
<p>이 본문은 빌드 타임에 만들어져 CDN에 캐시됩니다.</p>
<Avatar server:defer>
<AvatarFallback slot="fallback" />
</Avatar>
</Layout>
---
// src/components/Avatar.astro
import { getUserFromCookie } from '../lib/auth'
const user = await getUserFromCookie(Astro.request)
---
{user ? (
<a href="/me"><img src={user.avatar} alt={user.name} /></a>
) : (
<a href="/login">로그인</a>
)}
server:defer가 붙은 컴포넌트는:
- 빌드 타임에는 렌더되지 않습니다. 대신
slot="fallback"이 자리를 차지합니다. - 페이지는 정적 HTML로 CDN에 캐시됩니다. 0.5kB 안팎의 스크립트와 placeholder만 들어 있습니다.
- 브라우저는 그 자리에 들어갈 HTML을 별도 엔드포인트(
/_server-islands/Avatar)에서 가져옵니다. - 응답이 도착하면 placeholder가 실제 HTML로 교체됩니다.
핵심은 — 본문은 캐시되고, 섬만 사람마다 다르다는 점입니다. CDN을 100% 활용하면서 개인화가 됩니다.
3.3 GA에서 추가된 디테일
베타 동안 다음이 더해지면서 5.0 GA에 올라왔습니다.
- 헤더 설정 — Server Island 응답에
Cache-Control,Set-Cookie같은 헤더를 따로 붙일 수 있음 - 자동 압축 플랫폼 호환 — 기본 압축을 강제하는 호스팅에서도 작동
- Props 자동 암호화 — Server Island에 넘긴 props는 자동으로 암호화돼서, URL이나 placeholder를 통해 클라이언트가 들여다볼 수 없음. 권한 정보 같은 걸 넣어도 안전
3.4 언제 쓰고, 언제 쓰지 말아야 하는가
쓸 때:
- 99/1 페이지 — 본문은 모두에게 같고, 헤더의 사용자 상태/장바구니 같은 작은 조각만 다름
- 위젯 단위 개인화 — "방금 본 글", "추천 상품 3개"
- 로그인 여부에 따른 작은 UI 차이
쓰지 말아야 할 때:
- 페이지 전체가 사용자마다 다른 경우 (대시보드, 메일함, 콘솔) — 그냥 SSR 또는 SPA
- LCP 위의 위 (above-the-fold) 핵심 콘텐츠 — 늦게 채워지면 사용자 인식에 직격타
- 외부 API에 동기적으로 의존하는데 그 API가 느린 경우 — 차라리 클라이언트 페치가 나음
4. Content Layer API — '콘텐츠 컬렉션'을 '어떤 소스든' 으로 일반화
두 번째 큰 변화는 Content Layer입니다. Astro 4의 Content Collections는 파일 시스템의 마크다운/MDX 가 1급 시민이었습니다. Astro 5는 그것을 한 단계 추상화해서, 로더가 곧 컬렉션이 되도록 만들었습니다.
4.1 모델 — Loader + Schema
// src/content.config.ts
import { defineCollection, z } from 'astro:content'
import { glob } from 'astro/loaders'
const blog = defineCollection({
loader: glob({ pattern: '**/*.md', base: './src/content/blog' }),
schema: z.object({
title: z.string(),
pubDate: z.coerce.date(),
tags: z.array(z.string()).default([]),
draft: z.boolean().default(false),
}),
})
export const collections = { blog }
loader— 데이터를 어디서 가져올지schema— Zod로 타입과 검증을 동시에
glob 외에 공식 file 로더가 있고, 그 너머는 자유입니다.
4.2 커스텀 로더 — HTTP, DB, CMS 모두
가장 흥미로운 부분입니다. 임의의 데이터 소스를 컬렉션으로 만들 수 있습니다.
// src/loaders/notion.ts
import type { Loader, LoaderContext } from 'astro/loaders'
export function notionLoader(opts: { databaseId: string }): Loader {
return {
name: 'notion-loader',
async load({ store, parseData, meta, generateDigest }: LoaderContext) {
const lastSynced = meta.get('last-synced')
const pages = await fetchNotionPages(opts.databaseId, { since: lastSynced })
for (const p of pages) {
const data = await parseData({
id: p.id,
data: {
title: p.properties.Title.title[0].plain_text,
slug: p.properties.Slug.rich_text[0].plain_text,
body: p.body,
updatedAt: p.last_edited_time,
},
})
const digest = generateDigest(data)
store.set({ id: p.id, data, digest })
}
meta.set('last-synced', new Date().toISOString())
},
}
}
// src/content.config.ts
import { defineCollection, z } from 'astro:content'
import { notionLoader } from './loaders/notion'
const articles = defineCollection({
loader: notionLoader({ databaseId: process.env.NOTION_DB_ID! }),
schema: z.object({
title: z.string(),
slug: z.string(),
body: z.string(),
updatedAt: z.coerce.date(),
}),
})
export const collections = { articles }
이제 페이지에서는 똑같이 씁니다.
---
import { getCollection } from 'astro:content'
const articles = await getCollection('articles')
---
<ul>
{articles.map(a => <li><a href={`/articles/${a.data.slug}`}>{a.data.title}</a></li>)}
</ul>
데이터가 마크다운 파일이든, Notion이든, Sanity든, 자체 PostgreSQL이든 — 페이지 코드는 모릅니다. 이게 Content Layer의 본질입니다.
4.3 빌드 캐시와 인크리멘털
Content Layer는 SQLite 기반 캐시를 깔고 있어, 변경된 엔트리만 다시 처리합니다. 1만 개 콘텐츠 사이트의 두 번째 빌드가 첫 빌드보다 압도적으로 빠른 이유입니다. 로더는 meta.get/set으로 자기 마지막 동기화 시점을 기억할 수 있고, generateDigest로 콘텐츠 해시를 만들어 변경 여부를 판단합니다.
4.4 외부 로더 생태계
5.0 발표 이후 단기간에 공식·서드파티 로더가 우르르 등장했습니다 — Storyblok, Hygraph, WordPress, Ghost, Strapi, Sanity 등. 즉 "Astro = 헤드리스 CMS의 보편적 프런트엔드" 라는 포지셔닝이 실제로 작동하기 시작했습니다.
4.5 Live Loaders — 런타임 콘텐츠
GA 이후 추가된 흐름이 Live Content Loaders 입니다. 빌드 타임이 아니라 요청 시점에 데이터를 가져오는 로더로, "정적 80% + 라이브 20%" 시나리오(가격, 재고, 환율)에 적합합니다. 같은 getCollection API를 유지하면서 라이브로 갈 수 있다는 점이 인상적입니다.
5. Astro Actions — 폼과 뮤테이션을 타입 안전하게
세 번째 큰 변화는 Astro Actions입니다. 한마디로 "타입 안전한 서버 함수, Zod 입력 스키마와 함께".
5.1 정의
// src/actions/index.ts
import { defineAction } from 'astro:actions'
import { z } from 'astro:schema'
export const server = {
subscribe: defineAction({
accept: 'form',
input: z.object({
email: z.string().email(),
utm: z.string().optional(),
}),
handler: async (input, ctx) => {
const ip = ctx.request.headers.get('x-forwarded-for')
await saveSubscriber({ email: input.email, utm: input.utm, ip })
return { ok: true }
},
}),
}
5.2 호출 — 서버에서 그대로
---
import { actions } from 'astro:actions'
const result = Astro.getActionResult(actions.subscribe)
---
<form method="POST" action={actions.subscribe}>
<input type="email" name="email" required />
<input type="hidden" name="utm" value="blog-banner" />
<button type="submit">구독</button>
{result?.data?.ok && <p>고맙습니다!</p>}
{result?.error && <p class="err">에러: 다시 시도해 주세요.</p>}
</form>
JS가 꺼져 있어도 폼은 작동합니다. JS가 켜져 있으면 점진적으로 강화된 UX가 됩니다. 클라이언트 JS에서도 actions.subscribe(input) 으로 호출 가능합니다.
5.3 무엇이 좋은가
- 입력 검증과 타입이 단일 소스 — Zod 스키마 하나로 런타임 검증, TS 타입, 자동완성이 동시에 됩니다
- 점진적 강화 친화 — 일반 HTML 폼으로도 작동
- Server Islands와 잘 어울림 — 액션이 끝난 뒤 페이지 일부를 무효화/재렌더 가능
Next.js의 Server Actions와 컨셉상 형제지만, Astro는 "정적 페이지 위에 폼 한 개"라는 경량 시나리오에 더 가볍게 맞습니다.
6. Vite 6 / Vite 7 통합 — 빌드의 속살
Astro 5는 Vite 6 위에서 출시됐고, 이후 5.x/6.x 라인에서 Vite 7 합류 작업이 진행됐습니다. 개발자 입장에서 느끼는 변화:
- Environment API — 같은 빌드 그래프 안에서 클라이언트/서버/엣지 같은 다중 환경을 일관되게 다룸. Server Islands를 비롯해 서버 코드 분리가 더 깔끔
- 빠른 콜드 스타트 — 큰 사이트의 첫
astro dev가 체감상 더 가벼움 - CSS 변경 감지 — HMR이 더 정확. CSS 한 글자 바꿔도 풀 리로드 안 함
- 롤업 4.x / 5.x 라인 활용
여기서 중요한 시사점은 — Astro 팀이 Vite 코어 개발자와 거의 한 팀처럼 움직인다는 점입니다. Vite의 변화가 Astro에 빠르게 흘러들어옵니다.
7. View Transitions와 prefetching — SPA처럼 매끄럽게, MPA처럼 가볍게
콘텐츠 사이트의 약점 중 하나는 페이지 이동마다 흰 화면이 깜빡인다는 점이었습니다. View Transitions API와 핵심 prefetching이 이걸 풀어냅니다.
7.1 View Transitions
---
import { ClientRouter } from 'astro:transitions'
---
<head>
<ClientRouter />
</head>
이 한 줄을 레이아웃에 넣으면, 같은 사이트 안 내비게이션이 MPA 그대로 동작하면서 화면 전환에 브라우저 네이티브 View Transitions가 입혀집니다. 헤더는 유지되고 본문만 페이드되거나, 썸네일이 다음 페이지의 히어로 이미지로 자연스럽게 모핑되는 식입니다.
요소별로 이름을 주면 페이지 간 동일 요소가 트래킹됩니다.
<img src={post.cover} transition:name={`cover-${post.slug}`} />
7.2 prefetching — 코어로 흡수
prefetch는 Astro 3.5에서 코어로 들어왔고, 5에서는 기본 ON이 좀 더 합리적인 휴리스틱으로 다듬어졌습니다.
// astro.config.mjs
export default defineConfig({
prefetch: {
prefetchAll: true,
defaultStrategy: 'viewport',
},
})
전략은 tap, hover, viewport, load. 링크별로 data-astro-prefetch="hover" 같이 오버라이드 가능. 결과는 — 사용자가 클릭하기 전에 다음 페이지가 이미 와 있다.
7.3 i18n 개선
i18n은 4.x에서 안정화에 들어갔고 5.x에서 다듬어졌습니다.
- 기본 로캘과 로캘 목록을 한 번에 선언
- URL 전략 — prefix-other-locales(기본 로캘만 prefix 없음), prefix-always, 도메인 기반
getRelativeLocaleUrl,getAbsoluteLocaleUrl같은 헬퍼- 누락된 페이지 fallback — 일본어 페이지가 없으면 영어로 대체
콘텐츠 사이트는 거의 모두 다국어를 결국 만나기 때문에, 이 부분이 1급으로 들어왔다는 것이 큰 의미입니다.
8. Astro vs Next.js vs SvelteKit — 결정 매트릭스
이제 본론입니다. 어디서 무엇을 쓸지.
8.1 철학 한 줄
- Astro 5 — 정적 우선. 필요한 부분만 서버/클라이언트 섬으로.
- Next.js (App Router + RSC) — 서버 우선. 정적은 캐시의 한 형태.
- SvelteKit — 라우터 우선. 컴파일러로 런타임을 줄이는 통합 풀스택.
8.2 항목별 비교
| 항목 | Astro 5 | Next.js (RSC) | SvelteKit |
|---|---|---|---|
| 기본 JS 페이로드 | 가장 적음 (0이 기본) | 중간~높음 | 적음 |
| 풀스택 깊이 | 얕음~중간 | 깊음 | 중간 |
| 콘텐츠 모델 | Content Layer (1급) | App Router + MDX (애드온) | 직접 구성 |
| 폼/뮤테이션 | Actions | Server Actions | form actions |
| 동적 개인화 | Server Islands | RSC + Suspense | streaming + load |
| 다국어 | 코어 i18n | 별도 라이브러리 | 별도 라이브러리 |
| 호스팅 | Static + 어디든 어댑터 | Vercel 최적 | 어댑터 다양 |
| 학습 곡선 | 낮음 | 중간~높음 | 낮음~중간 |
| 컴포넌트 호환성 | React/Svelte/Vue/Solid 등 | React 전용 | Svelte 전용 |
8.3 시나리오별 추천
- 블로그, 문서, 마케팅 사이트, 미디어: Astro 5. Server Islands로 헤더 개인화 정도는 자연스럽게 처리됨. 코어 웹 바이탈에서 거의 이길 곳이 없음
- e커머스 스토어프런트 (헤드리스 CMS + 결제): Astro 5 우세. 상품 페이지 정적, 장바구니/추천만 Server Island. 단, 체크아웃 깊이 들어가면 다른 프레임워크가 나을 수 있음
- 풀스택 SaaS, 대시보드: Next.js. 인증, 권한, 실시간, 복잡한 폼, 깊은 라우팅 트리는 RSC가 더 자연스러움
- 소셜/실시간 앱: Next.js 또는 SvelteKit. Astro의 영역 아님
- 앱스러운 SPA + 약간의 서버: SvelteKit. 코드 양이 가장 적게 나옴
- 다국어 콘텐츠 허브, 매우 큰 사이트: Astro 5. Content Layer + i18n이 이 시나리오를 위해 만들어진 것 같음
한 줄로 — 콘텐츠가 1순위면 Astro, 애플리케이션이 1순위면 Next.js/SvelteKit.
9. 실제 적용 사례 — 콘텐츠 헤비 사이트에서의 모양
가상의 미디어 회사를 가정합니다. 월 1억 PV의 뉴스 사이트. 페이지 구성을 Astro 5 기준으로 어떻게 짤지 살펴봅니다.
9.1 페이지 단위 결정
/홈 — 정적 빌드, Server Island로 "로그인 헤더"와 "추천 섹션"만/articles/[slug]— 정적 빌드 (Content Layer가 CMS에서 가져옴), Server Island로 "헤더 + 댓글 수 + 좋아요 상태"/search— SSR (검색 쿼리가 캐시 키를 망가뜨림)/me,/me/bookmarks— SSR 또는 클라이언트 페치/auth/*— SSR + Actions
CDN 캐시 적중률이 95% 이상이 됩니다. 99/1 페이지에 99/1의 비용 구조가 따라옵니다.
9.2 데이터 흐름
- CMS (예: Sanity, Storyblok) — Content Layer 로더로 빌드 타임 동기화. 변경된 글만 재빌드(인크리멘털).
- 사용자/세션 — Server Island가 쿠키로 직접 읽음. 본문 캐시와 무관.
- 추천 알고리즘 — Server Island가 요청 시점에 추천 API 호출.
- 댓글 — Live Loader 또는 클라이언트 컴포넌트.
9.3 빌드 시간
- 1만 글 콜드 빌드: 수 분
- 1만 글 + 100개 변경 인크리멘털 빌드: 수십 초
- 캐시는 SQLite로 저장돼 CI 캐시에 그대로 올림
같은 규모의 Next.js 정적 빌드보다 빠를 가능성이 높습니다. 단, Next.js의 ISR(Incremental Static Regeneration) + 온디맨드 재검증 모델은 또 다른 매력이 있어, 절대 우위는 아닙니다.
10. Astro 4 → 5 마이그레이션 — 솔직한 이야기
가장 현실적인 질문 — 4에서 5로 옮기는 게 얼마나 아픈가.
10.1 좋은 소식
- 페이지/컴포넌트 문법은 그대로
- 라우팅 그대로
- 통합(Integrations) 대부분 그대로
10.2 손이 가는 부분
-
Content Collections → Content Layer
src/content/config.ts에loader: glob({...})추가getEntry/getCollection의 일부 시그니처 변경- 빌드 타임 캐시가 새로 생기므로
.astro/같은 캐시 디렉터리 CI에 마운트
-
Astro.glob제거- 위 Content Layer로 흡수됨. 마크다운 페이지 수집을 직접 하던 코드가 있다면
getCollection으로 옮김
- 위 Content Layer로 흡수됨. 마크다운 페이지 수집을 직접 하던 코드가 있다면
-
Image 처리 정책 변화
astro:assets의 옵션 일부 정리. 외부 이미지 도메인 화이트리스트는 더 엄격해짐
-
Adapter 업데이트
- Vercel, Netlify, Cloudflare, Node 어댑터 모두 5.x 버전 필요
-
Vite 6 환경 호환
- 커스텀 Vite 플러그인을 끼고 있다면 5/6 호환성 확인
10.3 마이그레이션 절차
- Astro 4 최신 패치로 올린 뒤 deprecation 경고를 0으로
pnpm dlx @astrojs/upgrade로 일괄 메이저 점프astro check통과시키기- 한 페이지에 Server Island 시험 도입
- 한 컬렉션부터 Content Layer로 이주
- 점진 확장
큰 사이트라도 보통 며칠 단위로 끝납니다. Next.js의 Pages → App Router 마이그레이션보다 훨씬 가볍습니다.
11. 솔직한 한계 — Astro가 여전히 지지 않는 곳, 지는 곳
광고를 빼고 적습니다.
11.1 여전히 강한 곳
- 콘텐츠 사이트 / 문서 / 마케팅 페이지에서 코어 웹 바이탈은 거의 무패
- 헤드리스 CMS 통합이 가장 자연스러움 (Content Layer 효과)
- 다국어 사이트의 1급 지원
- 빌드 시간 (인크리멘털 캐시)
- 다중 프레임워크 위젯 공존
11.2 약한 곳
- 깊은 풀스택 — 인증, 권한, 워크플로우, 큐, 백그라운드 작업이 페이지 코드와 강하게 엮인 시나리오. Astro는 의도적으로 얕음.
- 앱스러운 SPA — 모든 페이지가 동적이고 상태 공유가 깊다면, Astro의 정적 우선 모델이 이점을 잃음.
- 에코시스템 깊이 — Next.js 대비 라이브러리/예제/구인 시장이 작음.
- 호스팅 락인 없음의 그림자 — Vercel만큼 매끄럽게 통합된 호스팅이 부재. 어댑터 품질이 호스팅마다 다름.
- 개발 도구 — 강력하지만 Next.js의 RSC 디버깅/캐시 시각화 만큼 무르익지 않음.
- Server Islands의 함정 — 너무 많이 깔면 페이지 위가 placeholder투성이가 되고 사용자 인식이 나빠짐. "본문은 정적, 작은 섬만 동적" 원칙을 지켜야 함.
11.3 결정 한 줄
"당신의 사이트가 책이라면 Astro. 당신의 사이트가 도구라면 Next.js. 그 중간이면 SvelteKit도 같이 평가하라."
에필로그 — 체크리스트와 다음 글 예고
12.1 Astro 5를 도입할 때 체크리스트
- 페이지의 캐시 가능 비율이 80% 이상인가 (Server Islands가 빛나는 임계)
- 콘텐츠 소스가 1개 이상이고, 그중 일부가 외부 CMS/DB인가 (Content Layer 적합)
- 다국어가 필요한가 (코어 i18n 활용)
- 빌드 시간이 늘면 곤란한가 (인크리멘털 캐시 필요)
- 컴포넌트 라이브러리가 React/Svelte/Vue 중 하나로 고정되어 있는가 (호환성 점검)
- 호스팅 어댑터(Vercel/Netlify/Cloudflare/Node)가 안정 버전인지 확인했는가
- 폼은 Actions로, 페이지 일부 개인화는 Server Island로, 진짜 동적 페이지만 SSR로 — 룰을 합의했는가
12.2 안티패턴
- 페이지의 70%를 Server Island로 만든다 (그러면 그냥 SSR이 낫다)
- LCP 영역 위에 Server Island를 둔다 (지연이 사용자에게 직격)
- Content Layer를 우회해 페이지 컴포넌트에서 직접 fetch를 흩뿌린다 (캐시·타입 둘 다 잃음)
- Actions를 거치지 않고 직접 API 라우트만 만든다 (점진적 강화 못함)
client:load로 모든 컴포넌트를 하이드레이트한다 (Astro 쓰는 의미가 사라짐)- View Transitions 도입하면서 페이지마다 다른 레이아웃을 쓴다 (요소 추적이 깨짐)
12.3 다음 글 예고
- "Astro Server Islands로 e커머스 스토어프런트 만들기 — 본문 캐시 95%를 지키는 법"
- "Content Layer 커스텀 로더 패턴 — Notion, Postgres, S3에서 컬렉션 끌어오기"
- "Astro Actions와 점진적 강화 폼 — JS 0kb 폼이 가능한 이유"
- "Astro 6를 향한 로드맵 — Live Loaders 안정화와 그 다음"
이 시리즈의 한 줄 약속 — "콘텐츠 사이트의 표준은 다시 바뀌었고, 그 이름은 Astro 5다."
참고 / References
- Astro 5.0 Release Blog (2024-12-03)
- Astro 5.0 Beta Release
- Server Islands — Astro Docs
- Server Islands Announcement
- Islands Architecture — Astro Docs
- Content Layer — Deep Dive
- Content Collections — Astro Docs
- Content Loader API Reference
- Astro Actions — Docs
- Actions API Reference
- View Transitions — Docs
- View Transitions Router Reference
- Astro 2024 Year in Review
- Live Content Loaders Roadmap Issue #1151
- Storyblok Loader for Content Layer
- Hygraph Astro Content Loader
- Content Layer for Headless WordPress
- Comparing JS Frameworks for Content Sites — DatoCMS
- Astro 4.0 Release
- Astro 3.5 i18n Routing
Astro 5 Deep Dive: Server Islands, Content Layer, and the New Standard for Content Sites
"Let's redefine 'static.' Cache everything that can be cached, and only re-render the bits that genuinely have to differ per person — late, on the server, in place." — A one-line summary of the Astro 5 Server Islands design.
Prologue — The era of "just a static site generator" is over
Astro in 2022 was a static site generator for Markdown blogs. Astro 5, released on December 3, 2024, wears the same name but is a different thing. It is the de facto standard for content-driven sites — and that's not hyperbole.
This post wants to answer a small number of questions clearly:
- What exactly changed in Astro 5?
- What problems do Server Islands and the Content Layer solve?
- How does it compare with Next.js (RSC + Server Actions) and SvelteKit, fighting in the same arena?
- For content sites, marketing sites, docs, and commerce storefronts — where does Astro win, and where does it lose?
- How do you migrate from Astro 4 to 5?
This is not an ad. The things Astro does well and the things it doesn't carry equal weight. Make your decision at the final table.
1. Islands architecture recap — the one thing Astro did differently from day one
A quick refresher. While other frameworks went SPA → SSR → RSC, Astro took a different road from the start.
1.1 "Every page is HTML first"
The default output is HTML. JavaScript is added explicitly, per component that needs it. That policy has a name: Islands Architecture.
A page is not a single React tree. It's a sea of static HTML with interactive "islands" floating in it. Each island has its own bundle and its own hydration timing. The result is that the average content page ships dramatically less JavaScript to the client than competing frameworks.
1.2 Client directives
You make an island by adding one directive next to the component.
---
import Counter from '../components/Counter.tsx'
import LikeButton from '../components/LikeButton.svelte'
import SearchBox from '../components/SearchBox.vue'
---
<h1>Text outside an island is static HTML</h1>
<Counter client:load />
<LikeButton client:visible />
<SearchBox client:idle />
client:load— hydrate immediately on page loadclient:idle— when the browser is idleclient:visible— when it enters the viewportclient:media="(max-width: 600px)"— when a media query matchesclient:only="react"— don't render on the server, only on the client
That one line is the difference between "4 KB of JS on this page" and "300 KB of JS on this page."
1.3 Framework-agnostic
You can mix React, Svelte, Vue, Solid, Preact, and Lit on the same page. Few teams use this aggressively in production, but the meaningful payoff is incremental absorption of legacy widgets. Design system in Svelte, interactive demo in React — that split feels natural.
2. The big picture in Astro 5 — six headline changes
If you had to fit Astro 5 onto one screen, it would look like this.
| Area | Astro 4 | Astro 5 |
|---|---|---|
| Content model | Content Collections (file-based) | Content Layer API (any source) |
| Dynamic pages | Whole-page SSR or static | Server Islands (server islands on static) |
| Bundler | Vite 5 | Vite 6, with Vite 7 joining the 6.x line |
| Forms / mutations | Hand-rolled handlers | Astro Actions (type-safe) |
| Routing | Static + partial SSR | Per-route prerender refined |
| i18n | Experimental | Stable + fallback / domain routing |
One sentence: "It kept static-first and absorbed parts of full-stack." Astro is arriving at the same place as Next.js from the opposite direction. Next.js started in client React and used RSC to redraw the static/server boundary; Astro started in static HTML and used Server Islands to re-admit the server.
3. Server Islands — personalization without breaking static-first
Server Islands are the face of Astro 5. In one sentence:
"Cache the page statically on the CDN, render only the small, per-user bits late on the server, and have the client slot them in afterward."
3.1 The problem — "99% cacheable, 1% not"
Picture a typical content page. Header, body, sidebar, footer — nearly every pixel is the same for every visitor. Only a small corner ("Logged in," "Cart (3)," "Recommended for you") differs per user.
The old options:
- Full-page SSR — re-render 99% for the sake of 1%. Cache is useless.
- Fully static + client fetch — JS load → fetch → render → layout shift. Core Web Vitals suffer.
- ESI / Edge Side Includes — nice in theory, CDN-coupled, hell to debug.
Server Islands is the fourth path.
3.2 Usage — one server:defer
---
// src/pages/index.astro
import Layout from '../layouts/Layout.astro'
import Avatar from '../components/Avatar.astro'
import AvatarFallback from '../components/AvatarFallback.astro'
---
<Layout>
<h1>Today's picks</h1>
<p>This body is built at build time and cached on the CDN.</p>
<Avatar server:defer>
<AvatarFallback slot="fallback" />
</Avatar>
</Layout>
---
// src/components/Avatar.astro
import { getUserFromCookie } from '../lib/auth'
const user = await getUserFromCookie(Astro.request)
---
{user ? (
<a href="/me"><img src={user.avatar} alt={user.name} /></a>
) : (
<a href="/login">Sign in</a>
)}
A component with server:defer:
- Is not rendered at build time. The
slot="fallback"takes its place. - The page is cached as static HTML on the CDN. Roughly 0.5 KB of script plus the placeholder.
- The browser fetches the real HTML from a dedicated endpoint (
/_server-islands/Avatar). - When it arrives, the placeholder is swapped for the real HTML.
The key idea — the body is cached; only the island is personalized. You get 100% CDN utilization with real personalization.
3.3 What GA added
During the beta these landed and rolled into 5.0 GA:
- Per-island headers — set
Cache-Control,Set-Cookie, etc. on the island's response separately. - Auto-compression hosting compatibility — works on platforms that force compression.
- Automatic encryption of props — props passed to a server island are auto-encrypted, so clients can't peek through URLs or placeholders. Safe even for permission-bearing data.
3.4 When to use, and when not
Use when:
- 99/1 pages — body is the same for everyone, only header user state or cart-style badges differ.
- Widget-level personalization — "recently viewed," "3 recommended products."
- Tiny UI variations based on login status.
Do NOT use when:
- The whole page differs per user (dashboards, inboxes, consoles) — just SSR or SPA it.
- The content sits above the LCP fold — late paint hurts perceived performance hard.
- You depend synchronously on a slow external API — a client fetch with a skeleton is often better.
4. Content Layer API — generalizing "collections" to "any source"
The second big change is the Content Layer. In Astro 4, Content Collections treated Markdown / MDX on the filesystem as first-class. Astro 5 abstracts one level higher so that the loader is the collection.
4.1 The model — Loader + Schema
// src/content.config.ts
import { defineCollection, z } from 'astro:content'
import { glob } from 'astro/loaders'
const blog = defineCollection({
loader: glob({ pattern: '**/*.md', base: './src/content/blog' }),
schema: z.object({
title: z.string(),
pubDate: z.coerce.date(),
tags: z.array(z.string()).default([]),
draft: z.boolean().default(false),
}),
})
export const collections = { blog }
loader— where the data comes from.schema— Zod gives types and validation in one stroke.
Besides glob, there's an official file loader; beyond those, the world is yours.
4.2 Custom loaders — HTTP, DB, CMS
This is the interesting part. Any data source becomes a collection.
// src/loaders/notion.ts
import type { Loader, LoaderContext } from 'astro/loaders'
export function notionLoader(opts: { databaseId: string }): Loader {
return {
name: 'notion-loader',
async load({ store, parseData, meta, generateDigest }: LoaderContext) {
const lastSynced = meta.get('last-synced')
const pages = await fetchNotionPages(opts.databaseId, { since: lastSynced })
for (const p of pages) {
const data = await parseData({
id: p.id,
data: {
title: p.properties.Title.title[0].plain_text,
slug: p.properties.Slug.rich_text[0].plain_text,
body: p.body,
updatedAt: p.last_edited_time,
},
})
const digest = generateDigest(data)
store.set({ id: p.id, data, digest })
}
meta.set('last-synced', new Date().toISOString())
},
}
}
// src/content.config.ts
import { defineCollection, z } from 'astro:content'
import { notionLoader } from './loaders/notion'
const articles = defineCollection({
loader: notionLoader({ databaseId: process.env.NOTION_DB_ID! }),
schema: z.object({
title: z.string(),
slug: z.string(),
body: z.string(),
updatedAt: z.coerce.date(),
}),
})
export const collections = { articles }
Pages then consume it the same way as a file-based collection.
---
import { getCollection } from 'astro:content'
const articles = await getCollection('articles')
---
<ul>
{articles.map(a => <li><a href={`/articles/${a.data.slug}`}>{a.data.title}</a></li>)}
</ul>
Whether the data is Markdown, Notion, Sanity, or your own Postgres — pages don't know. That's the point of the Content Layer.
4.3 Build cache and incremental builds
The Content Layer ships an SQLite-backed cache and only reprocesses changed entries. That's why the second build of a 10k-article site is dramatically faster than the first. A loader can stash its last sync timestamp via meta.get/set, and generateDigest produces a content hash to detect changes.
4.4 The external loader ecosystem
After 5.0 shipped, official and third-party loaders arrived quickly — Storyblok, Hygraph, WordPress, Ghost, Strapi, Sanity, and more. The "Astro = universal frontend for any headless CMS" pitch started actually working in production.
4.5 Live Loaders — runtime content
Coming after GA: Live Content Loaders. These fetch data at request time rather than build time, fitting "static 80% + live 20%" scenarios (price, inventory, FX). The most impressive part is that you keep the same getCollection API — going live is a per-collection choice, not an architectural rewrite.
5. Astro Actions — type-safe forms and mutations
The third big change is Actions. In one phrase: "type-safe server functions with Zod input schemas."
5.1 Definition
// src/actions/index.ts
import { defineAction } from 'astro:actions'
import { z } from 'astro:schema'
export const server = {
subscribe: defineAction({
accept: 'form',
input: z.object({
email: z.string().email(),
utm: z.string().optional(),
}),
handler: async (input, ctx) => {
const ip = ctx.request.headers.get('x-forwarded-for')
await saveSubscriber({ email: input.email, utm: input.utm, ip })
return { ok: true }
},
}),
}
5.2 Invocation — server-rendered form, as is
---
import { actions } from 'astro:actions'
const result = Astro.getActionResult(actions.subscribe)
---
<form method="POST" action={actions.subscribe}>
<input type="email" name="email" required />
<input type="hidden" name="utm" value="blog-banner" />
<button type="submit">Subscribe</button>
{result?.data?.ok && <p>Thanks!</p>}
{result?.error && <p class="err">Error — please try again.</p>}
</form>
The form works with JS disabled. With JS on, it becomes progressively enhanced. From client JS you can also call actions.subscribe(input) directly.
5.3 Why it matters
- Single source of truth for validation and types — one Zod schema gives you runtime validation, TypeScript types, and autocomplete.
- Progressive-enhancement friendly — plain HTML form first.
- Plays well with Server Islands — after an action, you can invalidate / re-render an island.
Conceptually it's a sibling of Next.js Server Actions, but Astro fits the "static page with one form on it" scenario more lightly.
6. Vite 6 / Vite 7 integration — the build pipeline
Astro 5 shipped on Vite 6. Subsequent 5.x / 6.x lines pulled in Vite 7. From a developer's seat:
- Environment API — uniform handling of multiple environments (client/server/edge) inside one build graph. The server-code split for Server Islands is much cleaner.
- Faster cold starts — the first
astro devon a big site feels lighter. - Better CSS HMR — change one character of CSS, no full reload.
- Rollup 4.x / 5.x line in use.
The bigger point — the Astro team works almost in lockstep with Vite core. Vite improvements flow into Astro fast.
7. View Transitions and prefetching — SPA smoothness, MPA weight
One historical weakness of content sites was the white flash on every navigation. The View Transitions API and core prefetching close that gap.
7.1 View Transitions
---
import { ClientRouter } from 'astro:transitions'
---
<head>
<ClientRouter />
</head>
That one line in your layout makes intra-site navigation MPA-shaped while overlaying browser-native View Transitions on the page change. The header stays put while the body cross-fades; a thumbnail morphs into the next page's hero image.
Name elements to track them across pages.
<img src={post.cover} transition:name={`cover-${post.slug}`} />
7.2 prefetching — pulled into core
Prefetch became part of Astro core in 3.5. In 5, the default-on heuristics are smarter.
// astro.config.mjs
export default defineConfig({
prefetch: {
prefetchAll: true,
defaultStrategy: 'viewport',
},
})
Strategies are tap, hover, viewport, load. You can override per link with data-astro-prefetch="hover". The result — the next page is already there before the user clicks.
7.3 i18n improvements
i18n stabilized in 4.x and was polished in 5.x.
- Declare default locale and locale list at once.
- URL strategy — prefix-other-locales (only non-defaults get a prefix), prefix-always, or domain-based.
- Helpers —
getRelativeLocaleUrl,getAbsoluteLocaleUrl. - Fallback when pages are missing — Japanese page absent? fall back to English.
Almost every content site eventually meets multilingual; having this first-class matters.
8. Astro vs Next.js vs SvelteKit — decision matrix
Now the real point. What to use, where.
8.1 One-line philosophies
- Astro 5 — static-first. Server / client islands only where needed.
- Next.js (App Router + RSC) — server-first. Static is a form of cache.
- SvelteKit — router-first. Compiler-backed integrated full stack.
8.2 Item-by-item
| Item | Astro 5 | Next.js (RSC) | SvelteKit |
|---|---|---|---|
| Default JS payload | Smallest (zero by default) | Medium-high | Small |
| Full-stack depth | Shallow-medium | Deep | Medium |
| Content model | Content Layer (first-class) | App Router + MDX (add-on) | Roll your own |
| Forms / mutations | Actions | Server Actions | form actions |
| Dynamic personalization | Server Islands | RSC + Suspense | streaming + load |
| i18n | Core i18n | Library | Library |
| Hosting | Static + any adapter | Vercel-optimized | Various adapters |
| Learning curve | Low | Medium-high | Low-medium |
| Component compatibility | React/Svelte/Vue/Solid/etc. | React only | Svelte only |
8.3 Scenarios
- Blogs, docs, marketing, media — Astro 5. Server Islands handle header personalization gracefully. Hard to lose on Core Web Vitals.
- E-commerce storefront (headless CMS + checkout) — Astro 5 ahead. Product pages static; cart / recommendations on Server Islands. Deep checkout flows might prefer another framework.
- Full-stack SaaS, dashboards — Next.js. Auth, permissions, realtime, complex forms, deep route trees feel more natural in RSC.
- Social / realtime apps — Next.js or SvelteKit. Not Astro's lane.
- App-shaped SPA with a bit of server — SvelteKit. Tends to produce the least code.
- Multilingual content hub, very large site — Astro 5. Content Layer + i18n look made for this.
One line — content first, pick Astro; application first, pick Next.js or SvelteKit.
9. A real-world shape — a content-heavy site on Astro 5
A hypothetical media company: 100M PV/month news site. Here's what the architecture looks like on Astro 5.
9.1 Decisions per page
/home — static build; Server Island for "logged-in header" and "recommendations."/articles/[slug]— static build (Content Layer pulls from the CMS); Server Island for "header + comment count + like state."/search— SSR (search queries shred any cache key)./me,/me/bookmarks— SSR or client fetch./auth/*— SSR + Actions.
CDN hit rate clears 95%. A 99/1 page gets a 99/1 cost structure.
9.2 Data flow
- CMS (Sanity, Storyblok, etc.) — Content Layer loader, build-time sync. Only changed entries rebuild (incremental).
- User / session — Server Island reads cookies directly. Independent of body cache.
- Recommendation algorithm — Server Island calls the recommendation API at request time.
- Comments — Live Loader or a client component.
9.3 Build time
- Cold build of 10k articles: a few minutes.
- 10k articles + 100 changes (incremental): tens of seconds.
- The cache is SQLite — mount it from CI cache and inherit the speedup.
This is likely faster than a Next.js static build of the same size. That said, Next.js's ISR + on-demand revalidation model has its own appeal — this isn't an absolute win.
10. Migrating Astro 4 to 5 — the honest version
The most practical question — how much does going from 4 to 5 hurt?
10.1 Good news
- Page / component syntax: unchanged.
- Routing: unchanged.
- Most integrations: unchanged.
10.2 Where you'll actually spend time
-
Content Collections to Content Layer
- Add
loader: glob({...})tosrc/content/config.ts. - Some
getEntry/getCollectionsignatures shifted slightly. - The build introduces a new cache; mount the cache directory in CI.
- Add
-
Astro.globremoved- Subsumed by the Content Layer. Code that hand-rolled Markdown discovery moves to
getCollection.
- Subsumed by the Content Layer. Code that hand-rolled Markdown discovery moves to
-
Image policy changes
- Some
astro:assetsoptions were tidied. External image domain allowlists are stricter.
- Some
-
Adapter upgrades
- Vercel, Netlify, Cloudflare, and Node adapters all need 5.x versions.
-
Vite 6 plugin compatibility
- If you've got custom Vite plugins, verify 5/6 compatibility.
10.3 Migration order
- Bring Astro 4 to the latest patch and clear deprecation warnings.
- Run
pnpm dlx @astrojs/upgradefor the major jump. - Make
astro checkpass. - Try Server Islands on one page.
- Migrate one collection to the Content Layer.
- Expand incrementally.
Even a large site usually wraps up in days. Far lighter than the Pages-to-App-Router migration in Next.js.
11. The honest limits — where Astro still wins, and where it loses
The non-marketing part.
11.1 Where Astro is still strong
- Content sites, docs, marketing pages — Core Web Vitals are nearly unbeatable.
- Headless-CMS integration is the most natural in class (Content Layer effect).
- First-class i18n for multilingual sites.
- Build times (incremental cache).
- Multi-framework widgets coexisting.
11.2 Where it's weaker
- Deep full-stack — auth, authz, workflows, queues, background jobs intertwined with page code. Astro is deliberately shallow.
- App-shaped SPAs — when every page is dynamic and state-shared, the static-first model loses its edge.
- Ecosystem depth — fewer libraries, fewer examples, smaller job market versus Next.js.
- No hosting lock-in (the shadow side) — no host integrates as smoothly as Vercel does with Next.js. Adapter quality varies.
- Dev tooling — strong, but RSC's debugging / cache visualization in Next.js is more mature.
- Server Islands trap — too many of them and the page becomes a sea of placeholders; perceived quality drops. Hold the line at "body static, small islands dynamic."
11.3 One line
"If your site is a book, pick Astro. If your site is a tool, pick Next.js. If it's in between, evaluate SvelteKit too."
Epilogue — checklist and what's next
12.1 Adoption checklist
- Is more than 80% of each page cacheable? (the Server Islands sweet spot)
- Are there one or more content sources, with some living in an external CMS / DB? (Content Layer fit)
- Do you need multilingual? (use core i18n)
- Do build times need to stay short? (lean on the incremental cache)
- Is your component library fixed on React / Svelte / Vue? (sanity-check compatibility)
- Is the hosting adapter (Vercel / Netlify / Cloudflare / Node) at a stable version?
- Have you agreed on the rule — forms via Actions, partial personalization via Server Islands, full SSR only for genuinely dynamic pages?
12.2 Anti-patterns
- 70% of the page is a Server Island. (Just SSR the whole thing.)
- A Server Island lives above the LCP fold. (Late paint hits the user directly.)
- You bypass the Content Layer and scatter
fetchcalls in page components. (You lose both caching and types.) - You skip Actions and hand-roll only API routes. (No progressive enhancement.)
- You
client:loadevery component. (You've erased the reason to use Astro.) - You adopt View Transitions but use different layouts per page. (Element tracking breaks.)
12.3 What's next
- "Building an e-commerce storefront with Server Islands — keeping body cache hit rate at 95%."
- "Custom Content Layer loader patterns — pulling collections from Notion, Postgres, and S3."
- "Astro Actions and progressively enhanced forms — why a zero-JS form is possible."
- "Roadmap toward Astro 6 — Live Loaders stabilizing and beyond."
The series promise in one line — the standard for content sites has shifted again, and its name is Astro 5.
References
- Astro 5.0 Release Blog (2024-12-03)
- Astro 5.0 Beta Release
- Server Islands — Astro Docs
- Server Islands Announcement
- Islands Architecture — Astro Docs
- Content Layer — Deep Dive
- Content Collections — Astro Docs
- Content Loader API Reference
- Astro Actions — Docs
- Actions API Reference
- View Transitions — Docs
- View Transitions Router Reference
- Astro 2024 Year in Review
- Live Content Loaders Roadmap Issue #1151
- Storyblok Loader for Content Layer
- Hygraph Astro Content Loader
- Content Layer for Headless WordPress
- Comparing JS Frameworks for Content Sites — DatoCMS
- Astro 4.0 Release
- Astro 3.5 i18n Routing