Astro 웹 프레임워크
개요
Astro는 블로그, 문서, 포트폴리오, 마케팅 사이트, 전자상거래 등 콘텐츠가 풍부한 웹사이트를 위해 설계된 웹 프레임워크입니다. 핵심 혁신은 **아일랜드 아키텍처(Islands Architecture)**입니다. 기본적으로 Astro는 브라우저에 JavaScript를 전혀 전송하지 않습니다. 대화형 컴포넌트는 분리된 "아일랜드"로 선택적으로 하이드레이션됩니다. Astro는 React, Vue, Svelte, Solid 및 기타 UI 프레임워크를 동일한 프로젝트에서 동시에 지원하므로 컴포넌트별로 적합한 도구를 선택할 수 있습니다.
이 기술을 사용해야 할 때
- 블로그, 문서 사이트, 마케팅 페이지 또는 포트폴리오를 구축할 때
- 성능과 Core Web Vitals가 최우선일 때
- 프로젝트에 Markdown 또는 MDX 파일이 많을 때
- SSG(정적) 출력을 원하고 동적 라우트를 위한 선택적 SSR을 사용하려 할 때
- 사용자가
.astro파일,Astro.props, 콘텐츠 컬렉션 또는client:디렉티브에 대해 질문할 때
작동 방식
1단계: 프로젝트 설정
npm create astro@latest my-site
cd my-site
npm install
npm run dev
필요에 따라 통합 기능을 추가합니다:
npx astro add tailwind # Tailwind CSS
npx astro add react # React 컴포넌트 지원
npx astro add mdx # MDX 지원
npx astro add sitemap # 자동 sitemap.xml
npx astro add vercel # Vercel SSR 어댑터
프로젝트 구조:
src/
pages/ ← 파일 기반 라우팅 (.astro, .md, .mdx)
layouts/ ← 재사용 가능한 페이지 셸
components/ ← UI 컴포넌트 (.astro, .tsx, .vue 등)
content/ ← 타입-세이프 콘텐츠 컬렉션 (Markdown/MDX)
styles/ ← 글로벌 CSS
public/ ← 정적 에셋 (그대로 복사됨)
astro.config.mjs ← 프레임워크 설정
2단계: Astro 컴포넌트 구문
.astro 파일은 상단에 코드 펜스(서버 전용)가 있고 아래에 템플릿이 있습니다:
---
// src/components/Card.astro
// 이 블록은 서버에서만 실행됩니다 — 브라우저에서는 절대 실행되지 않음
interface Props {
title: string;
href: string;
description: string;
}
const { title, href, description } = Astro.props;
---
<article class="card">
<h2><a href={href}>{title}</a></h2>
<p>{description}</p>
</article>
<style>
/* 이 컴포넌트에 자동으로 스코프가 지정됨 */
.card { border: 1px solid #eee; padding: 1rem; }
</style>
3단계: 파일 기반 페이지 및 라우팅
src/pages/index.astro → /
src/pages/about.astro → /about
src/pages/blog/[slug].astro → /blog/:slug (동적)
src/pages/blog/[...path].astro → /blog/* (포괄)
getStaticPaths를 사용한 동적 라우트:
---
// src/pages/blog/[slug].astro
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();
---
<h1>{post.data.title}</h1>
<Content />
4단계: 콘텐츠 컬렉션
콘텐츠 컬렉션은 Markdown 및 MDX 파일에 대한 타입-세이프한 접근을 제공합니다:
// src/content/config.ts
import { z, defineCollection } from 'astro:content';
const blog = defineCollection({
type: 'content',
schema: z.object({
title: z.string(),
date: z.coerce.date(),
tags: z.array(z.string()).default([]),
draft: z.boolean().default(false),
}),
});
export const collections = { blog };
---
// src/pages/blog/index.astro
import { getCollection } from 'astro:content';
const posts = (await getCollection('blog'))
.filter(p => !p.data.draft)
.sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf());
---
<ul>
{posts.map(post => (
<li>
<a href={`/blog/${post.slug}`}>{post.data.title}</a>
<time>{post.data.date.toLocaleDateString()}</time>
</li>
))}
</ul>
5단계: 아일랜드 — 선택적 하이드레이션
기본적으로 UI 프레임워크 컴포넌트는 JS 없이 정적 HTML로 렌더링됩니다. client: 디렉티브를 사용하여 하이드레이션합니다:
---
import Counter from '../components/Counter.tsx'; // React 컴포넌트
import VideoPlayer from '../components/VideoPlayer.svelte';
---
<!-- 정적 HTML — 브라우저로 JavaScript 전송 없음 -->
<Counter initialCount={0} />
<!-- 페이지 로드 시 즉시 하이드레이션 -->
<Counter initialCount={0} client:load />
<!-- 컴포넌트가 화면에 보일 때 하이드레이션 -->
<VideoPlayer src="/demo.mp4" client:visible />
<!-- 브라우저가 유휴 상태일 때만 하이드레이션 -->
<Analytics client:idle />
<!-- 특정 미디어 쿼리에서만 하이드레이션 -->
<MobileMenu client:media="(max-width: 768px)" />
6단계: 레이아웃
---
// src/layouts/BaseLayout.astro
interface Props {
title: string;
description?: string;
}
const { title, description = 'My Astro Site' } = Astro.props;
---
<html lang="en">
<head>
<meta charset="utf-8" />
<title>{title}</title>
<meta name="description" content={description} />
</head>
<body>
<nav>...</nav>
<main>
<slot /> <!-- 페이지 콘텐츠가 여기 렌더링됨 -->
</main>
<footer>...</footer>
</body>
</html>
---
// src/pages/about.astro
import BaseLayout from '../layouts/BaseLayout.astro';
---
<BaseLayout title="About Us">
<h1>About Us</h1>
<p>Welcome to our company...</p>
</BaseLayout>
7단계: SSR 모드 (온디맨드 렌더링)
어댑터를 설정하여 동적 페이지에 대해 SSR을 활성화합니다:
// astro.config.mjs
import { defineConfig } from 'astro/config';
import vercel from '@astrojs/vercel/serverless';
export default defineConfig({
output: 'hybrid', // 'static' | 'server' | 'hybrid'
adapter: vercel(),
});
export const prerender = false를 사용하여 개별 페이지를 SSR에 선택합니다.
예제
예제 1: RSS 피드가 있는 블로그
// src/pages/rss.xml.ts
import rss from '@astrojs/rss';
import { getCollection } from 'astro:content';
export async function GET(context) {
const posts = await getCollection('blog');
return rss({
title: 'My Blog',
description: 'Latest posts',
site: context.site,
items: posts.map(post => ({
title: post.data.title,
pubDate: post.data.date,
link: `/blog/${post.slug}/`,
})),
});
}
예제 2: API 엔드포인트 (SSR)
// src/pages/api/subscribe.ts
import type { APIRoute } from 'astro';
export const POST: APIRoute = async ({ request }) => {
const { email } = await request.json();
if (!email) {
return new Response(JSON.stringify({ error: 'Email required' }), {
status: 400,
headers: { 'Content-Type': 'application/json' },
});
}
await addToNewsletter(email);
return new Response(JSON.stringify({ success: true }), { status: 200 });
};
예제 3: 아일랜드로 React 컴포넌트 사용
// src/components/SearchBox.tsx
import { useState } from 'react';
export default function SearchBox() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
async function search(e: React.FormEvent) {
e.preventDefault();
const data = await fetch(`/api/search?q=${query}`).then(r => r.json());
setResults(data);
}
return (
<form onSubmit={search}>
<input value={query} onChange={e => setQuery(e.target.value)} />
<button type="submit">Search</button>
<ul>{results.map(r => <li key={r.id}>{r.title}</li>)}</ul>
</form>
);
}
---
import SearchBox from '../components/SearchBox.tsx';
---
<!-- 즉시 하이드레이션됨 — 이 아일랜드는 대화형입니다 -->
<SearchBox client:load />
모범 사례
- ✅ 대부분의 컴포넌트는 정적
.astro파일로 유지하세요 — 대화형이어야 하는 것만 하이드레이션하세요 - ✅ 모든 Markdown/MDX 콘텐츠에 콘텐츠 컬렉션을 사용하세요 — 타입 안전성과 자동 유효성 검사가 제공됩니다
- ✅ 아래쪽 컴포넌트의 경우
client:load보다client:visible을 선호하세요 — 초기 JS를 줄입니다 - ✅ 환경 변수에는
import.meta.env를 사용하세요 — 공용 변수는PUBLIC_으로 접두사를 붙이세요 - ✅ 전체 SPA 없이 부드러운 페이지 탐색을 위해
astro:transitions의<ViewTransitions />를 추가하세요 - ❌ 모든 컴포넌트에
client:load를 사용하지 마세요 — 이는 Astro의 성능 이점을 무효화합니다 - ❌ 클라이언트 직면 템플릿에서 사용되는
.astro프론트매터에 비밀 정보를 넣지 마세요 - ❌ 정적 모드에서 동적 라우트에
getStaticPaths를 생략하지 마세요 — 빌드가 실패합니다
보안 및 안전 참고 사항
.astro파일의 프론트매터 코드는 서버 측에서만 실행되며 브라우저에 절대 노출되지 않습니다.- 민감하지 않은 값에만
import.meta.env.PUBLIC_*을 사용하세요. 프라이빗 환경 변수(PUBLIC_접두사 없음)는 클라이언트로 전송되지 않습니다. - SSR 모드를 사용하는 경우, 데이터베이스 쿼리나 API 호출 전에 모든
Astro.request입력을 검증하세요. set:html로 렌더링하기 전에 사용자 제공 콘텐츠를 삭제하세요 — 이는 자동 이스케이프를 우회합니다.
일반적인 함정
-
문제: React/Vue 컴포넌트의 JavaScript가 브라우저에서 실행되지 않습니다 해결책:
client:디렉티브(client:load,client:visible등)를 추가하세요 — 없으면 컴포넌트가 정적 HTML로만 렌더링됩니다. -
문제: 개발 중 콘텐츠 업데이트 후
getStaticPaths데이터가 오래되었습니다 해결책: Astro의 개발 서버는 콘텐츠 파일을 감시합니다.content/config.ts변경 사항이 반영되지 않으면 서버를 다시 시작하세요. -
문제:
Astro.props타입이any입니다 — 자동 완성이 되지 않습니다 해결책: 프론트매터에Props인터페이스 또는 타입을 정의하면 Astro가 자동으로 추론합니다. -
문제:
.astro컴포넌트의 CSS가 다른 컴포넌트로 누출됩니다 해결책:.astro<style>태그의 스타일은 자동으로 스코프가 지정됩니다. 의도적으로 하위 요소를 대상으로 할 때만:global()을 사용하세요.
관련 기술
@sveltekit— 반응형 UI가 있는 풀스택 프레임워크가 필요할 때 (Astro의 콘텐츠 중심과 대조)@nextjs-app-router-patterns— React 우선 풀스택 프레임워크가 필요할 때@tailwind-patterns— Tailwind CSS로 Astro 사이트 스타일링@progressive-web-app— Astro 사이트에 PWA 기능 추가
제한 사항
- 이 기술은 작업이 위에 설명된 범위와 명확하게 일치하는 경우에만 사용하세요.
- 출력물을 환경별 검증, 테스트 또는 전문가 검토의 대체물로 취급하지 마세요.
- 필요한 입력, 권한, 안전 경계 또는 성공 기준이 누락된 경우 중단하고 명확한 설명을 요청하세요.