Astro 5 완벽 가이드 — Content Layer·Server Islands·Sessions 새 기능과 실전 활용
이 글의 핵심
Astro 5는 2024년 말 GA된 메이저 릴리스로 Content Layer·Server Islands·Sessions·새 Actions API를 핵심 축으로 가집니다. 정적 사이트에서 동적 개인화까지 하나의 프레임워크로 커버하면서 여전히 JS 번들은 0에 가까운 수준으로 유지합니다. 이 글은 Astro 5의 주요 새 기능과 실전 활용·마이그레이션 전략을 다룹니다.
Astro 5의 핵심 변화
| 영역 | Astro 4 | Astro 5 |
|---|---|---|
| 콘텐츠 소스 | content/ 디렉터리 중심 | Content Layer(어떤 소스든) |
| 동적 영역 + 캐시 | 어려움 | Server Islands |
| Form/Mutations | 직접 구현 | Actions API |
| 세션 | 외부 라이브러리 | 기본 제공 (experimental) |
| i18n | 기본 동작 | 라우팅·번역 미들웨어 내장 |
| 이미지 | Sharp | Sharp + AVIF/WebP 진화 |
Content Layer API
기존 방식 (Astro 4)
// src/content/config.ts
import { defineCollection, z } from "astro:content"
const blog = defineCollection({
type: "content",
schema: z.object({ title: z.string(), date: z.date() }),
})
export const collections = { blog }
Markdown 파일만 대상. 외부 CMS를 쓰려면 별도 스크립트·빌드 스텝이 필요했습니다.
Astro 5: Loader 기반
// src/content.config.ts
import { defineCollection, z } from "astro:content"
import { glob, file } from "astro/loaders"
const blog = defineCollection({
loader: glob({ pattern: "**/*.{md,mdx}", base: "./src/content/blog" }),
schema: z.object({
title: z.string(),
pubDate: z.coerce.date(),
tags: z.array(z.string()).optional(),
}),
})
// 외부 JSON API
const notionPosts = defineCollection({
loader: async () => {
const res = await fetch("https://api.notion.com/v1/databases/...",
{ headers: { Authorization: `Bearer ${import.meta.env.NOTION_TOKEN}` } })
const data = await res.json()
return data.results.map((item: any) => ({
id: item.id,
title: item.properties.Name.title[0].text.content,
slug: item.properties.Slug.rich_text[0].text.content,
body: item.properties.Body.rich_text[0].text.content,
}))
},
schema: z.object({ title: z.string(), slug: z.string(), body: z.string() }),
})
// 로컬 JSON 파일
const products = defineCollection({
loader: file("./data/products.json"),
schema: z.object({ id: z.string(), name: z.string(), price: z.number() }),
})
export const collections = { blog, notionPosts, products }
사용
---
import { getCollection, getEntry } from "astro:content"
const posts = await getCollection("blog", ({ data }) => !data.draft)
const featured = await getEntry("notionPosts", "featured-slug")
---
어떤 소스든 동일 API로 접근하고 Zod 스키마·빌드 캐시가 모두 적용됩니다.
커스텀 Loader
import type { Loader } from "astro/loaders"
function stripeProducts(): Loader {
return {
name: "stripe-products",
load: async ({ store, logger }) => {
const res = await fetch("https://api.stripe.com/v1/products", {
headers: { Authorization: `Bearer ${process.env.STRIPE_SECRET}` },
})
const { data } = await res.json()
store.clear()
for (const p of data) {
store.set({ id: p.id, data: p })
}
logger.info(`Loaded ${data.length} products`)
},
}
}
CI/빌드마다 외부 API에서 가져와 저장소에 캐시. Incremental 빌드와 통합됩니다.
Server Islands: 캐시 + 개인화
정적 페이지를 CDN에 두고 “일부 영역만 서버 실시간 렌더링”으로 바꿉니다.
---
// src/pages/index.astro
import Layout from "../layouts/Base.astro"
import UserBadge from "../components/UserBadge.astro"
import LatestPosts from "../components/LatestPosts.astro"
---
<Layout>
<h1>Welcome</h1>
<!-- 서버 아일랜드: 요청 시점에 서버가 렌더링 -->
<UserBadge server:defer>
<p slot="fallback">Loading user…</p>
</UserBadge>
<!-- 정적: 빌드 시 결정, CDN에 캐시됨 -->
<LatestPosts />
</Layout>
---
// src/components/UserBadge.astro
const session = Astro.cookies.get("session")
const user = session ? await getUser(session.value) : null
---
{user ? <span>Hi, {user.name}</span> : <a href="/login">Log in</a>}
결과:
- 전체 페이지는
Cache-Control: s-maxage=31536000, immutable로 영구 캐시 <UserBadge>영역만 별도 요청으로 서버에서 렌더 → 사용자별 동적
CDN hit율 99%+를 유지하면서 “로그인 상태·장바구니·개인화”를 달성합니다.
Actions API
폼 제출·mutation을 타입 안전하게 처리합니다.
// src/actions/index.ts
import { defineAction } from "astro:actions"
import { z } from "astro:schema"
import { db } from "../lib/db"
export const server = {
createComment: defineAction({
accept: "form",
input: z.object({
postId: z.string(),
body: z.string().min(1).max(500),
}),
handler: async ({ postId, body }, context) => {
const session = context.cookies.get("session")
if (!session) throw new Error("unauthorized")
const [row] = await db.insert(comments).values({ postId, body, userId: session.value }).returning()
return row
},
}),
}
---
// src/pages/posts/[id].astro
import { actions } from "astro:actions"
const result = Astro.getActionResult(actions.createComment)
---
<form method="POST" action={actions.createComment}>
<input type="hidden" name="postId" value={post.id} />
<textarea name="body" required></textarea>
<button>Submit</button>
</form>
{result?.data && <p>Saved!</p>}
{result?.error && <p class="error">{result.error.message}</p>}
- 서버에만 존재하는 함수지만 클라이언트에서 타입 안전하게 호출
- Zod 검증 자동
- progressive enhancement: JS 없이도 form 동작
- React Server Actions와 유사한 DX
클라이언트 호출
<script>
import { actions } from "astro:actions"
document.getElementById("btn")?.addEventListener("click", async () => {
const { data, error } = await actions.createComment({ postId: "1", body: "Hello" })
if (error) console.error(error)
else console.log("created", data)
})
</script>
Sessions (experimental)
// astro.config.mjs
import { defineConfig } from "astro/config"
import node from "@astrojs/node"
export default defineConfig({
output: "server",
adapter: node({ mode: "standalone" }),
experimental: {
session: {
driver: "fs", // fs | redis | cloudflare-kv | vercel-kv | upstash
options: { base: "./.sessions" },
},
},
})
---
const sess = Astro.session
const count = (await sess.get<number>("count")) ?? 0
await sess.set("count", count + 1)
---
<p>Visits: {count + 1}</p>
별도 라이브러리 없이 세션이 내장됩니다. 드라이버 교체만으로 파일시스템 → Redis → KV로 확장.
i18n 내장
// astro.config.mjs
export default defineConfig({
i18n: {
defaultLocale: "ko",
locales: ["ko", "en"],
routing: { prefixDefaultLocale: false },
fallback: { en: "ko" },
},
})
src/pages/
index.astro # ko (기본)
about.astro
en/
index.astro # en
about.astro
---
import { getRelativeLocaleUrl, getLocaleByPath } from "astro:i18n"
const locale = getLocaleByPath(Astro.url.pathname)
---
<a href={getRelativeLocaleUrl("en", "/about")}>English</a>
middleware로 Accept-Language 자동 리다이렉트도 설정 가능.
이미지 최적화 고도화
---
import { Image, Picture } from "astro:assets"
import hero from "../assets/hero.jpg"
---
<Image src={hero} alt="hero" width={1200} height={630} format="avif" quality={80} loading="eager" />
<Picture
src={hero}
alt="hero"
widths={[400, 800, 1200]}
sizes="(min-width: 1024px) 1200px, 100vw"
formats={["avif", "webp", "jpg"]}
/>
Astro 5는 Sharp 최신 + AVIF 기본 + 반응형 이미지 생성을 최적화해 LCP 점수 개선에 직접 기여.
성능 / DX 개선
- Vite 6: 더 빠른 HMR·빌드
- esbuild 기반 MDX: 대형 MDX 빌드 속도 개선
- 출력 HTML 최소화: 불필요 공백 제거
- CSS Bundling 재작성: 중복 제거
- Astro DB 통합 개선 (experimental): 원격 Turso DB를 빌드에 활용
전통 콘텐츠 사이트 템플릿
---
// src/pages/blog/[...slug].astro
import { getCollection } from "astro:content"
import Layout from "../../layouts/Post.astro"
export async function getStaticPaths() {
const posts = await getCollection("blog", ({ data }) => !data.draft)
return posts.map((p) => ({ params: { slug: p.slug }, props: { post: p } }))
}
const { post } = Astro.props
const { Content } = await post.render()
---
<Layout title={post.data.title} description={post.data.description}>
<article>
<h1>{post.data.title}</h1>
<time>{post.data.pubDate.toLocaleDateString()}</time>
<Content />
</article>
</Layout>
Cloudflare Pages + Server Islands
// astro.config.mjs
import cloudflare from "@astrojs/cloudflare"
export default defineConfig({
output: "server",
adapter: cloudflare({
platformProxy: { enabled: true },
}),
server: { port: 4321 },
})
- 정적 빌드 파일은 CDN에 배포
server:defer영역은 Workers가 처리- KV/D1/R2 바인딩도
Astro.locals.runtime.env.DB로 접근
마이그레이션: 4 → 5
pnpm create astro@latest -- --template minimal로 새 프로젝트 생성해 구조 비교astro upgrade실행src/content/config.ts→src/content.config.ts이관 + loader 기반으로- 폼 핸들러를 Actions로 점진 전환
- 캐시 문제 있던 동적 컴포넌트를 Server Islands로 변환
experimental플래그 검토 (Session 등)
트러블슈팅
Content Layer 빌드 속도 저하
Loader가 큰 데이터를 매번 fetch → load에서 캐시 우선 로직 추가, store.clear() 호출을 변경 감지 시에만.
Server Islands가 캐시되지 않음
- 응답 헤더에
Cache-Control확인 - Edge에서 쿠키·쿼리 기반 캐시 키 분리
server:defer아일랜드 내부가 “사용자별 응답”인지 “페이지 공용 응답”인지 구분
Actions에서 타입이 풀림
astro:actions import 확인. tsconfig moduleResolution: "bundler" 권장.
i18n 리다이렉트 루프
prefixDefaultLocale 설정과 middleware의 조합 점검. URL 패턴 통일.
이미지가 너무 큼
format="avif"quality·widths지정- CDN에서 이미지 원본 요청 차단
체크리스트
- Astro 5 + 필요한 어댑터 설치
- Content Layer로 외부 데이터 소스 통합
- Server Islands로 캐시·개인화 분리
- Actions로 폼·mutation 타입 안전
- 이미지 AVIF +
Picture반응형 - i18n 라우팅 확정
- Session 드라이버 결정 (Redis/KV 등)
- Lighthouse LCP/INP 목표치 달성
마무리
Astro 5는 “정적은 빠르지만 동적이 어렵다”는 오랜 딜레마를 Server Islands로, “Markdown 외 콘텐츠는 불편하다”는 문제를 Content Layer로 해결했습니다. Actions·Session·i18n 내장까지 포함해 콘텐츠·마케팅·커머스 스토어프론트·블로그 같은 분야에서 Next.js 대비 훨씬 가볍고 빠른 스택으로 자리잡았습니다. 2026년 현재 공식 어댑터가 Cloudflare·Vercel·Netlify·Node를 모두 커버해 어디든 배포 가능하며, React/Vue/Svelte 컴포넌트도 “필요한 곳에만” 섬처럼 삽입할 수 있어 팀의 기존 투자를 유지할 수 있습니다. pkglog.com도 Astro로 운영되고 있으며, 이 글의 패턴 대부분이 실제로 적용되어 있습니다.
관련 글
- Astro 완벽 가이드
- Next.js 완벽 가이드
- 콘텐츠 관리 전략
- JAMstack 완벽 가이드