Astro: 콘텐츠 중심 웹사이트를 위한 현대적 프레임워크
프론트엔드 프레임워크를 고를 때마다 한숨이 나오시지 않나요? Next.js, Nuxt, SvelteKit, Remix… 선택지가 너무 많아서 오히려 결정이 어려워지는 상황이 흔한데요. 그런데 만약 만들려는 게 블로그, 문서 사이트, 포트폴리오처럼 콘텐츠가 중심인 사이트라면 이야기가 좀 달라집니다.
메타 프레임워크 중에서도 Astro는 꽤 독특한 위치를 차지하고 있거든요. 기본적으로 JavaScript를 아예 보내지 않는다는 과감한 철학, 그리고 React든 Vue든 Svelte든 원하는 UI 프레임워크를 골라 쓸 수 있는 유연함까지. 사실 지금 보고 계신 이 블로그도 Astro로 만들었습니다 😄
이 글에서는 Astro를 처음 접하는 분들이 프로젝트 생성부터 배포까지 핵심 개념을 한 번에 훑어볼 수 있도록 정리해봤습니다.
Astro가 특별한 이유
Astro의 가장 큰 특징은 제로(Zero) JavaScript입니다. Astro로 만든 사이트는 기본적으로 JavaScript 없이 순수한 HTML과 CSS만 브라우저로 전송됩니다. React나 Vue 컴포넌트를 쓰더라도 빌드 시점에 전부 HTML로 변환되어 버리죠.
이게 왜 중요할까요? SPA로 만든 웹사이트는 브라우저가 JavaScript를 내려받고 파싱하고 실행해야 비로소 화면이 그려지는데요. 블로그나 문서 사이트처럼 읽기 위주의 콘텐츠에 이런 과정은 사실 낭비나 다름없습니다. Astro는 이 불필요한 과정을 아예 없애버려요. 네트워크 전송량이 줄고 브라우저의 파싱 시간도 사라지니 페이지가 눈에 띄게 빨라집니다. Core Web Vitals 점수가 자연스럽게 좋아지는 건 덤이고요.
그렇다고 인터랙션을 완전히 포기하는 건 아닙니다. 검색 기능이나 다크모드 토글처럼 JavaScript가 꼭 필요한 부분에만 선택적으로 로드할 수 있는데요, Astro는 이걸 아일랜드 아키텍처(Islands Architecture)라고 부릅니다. 정적인 HTML 바다 위에 인터랙티브한 섬을 띄우는 개념이라고 생각하면 됩니다. 이 부분은 뒤에서 자세히 다루겠습니다.
프레임워크에 종속되지 않는다는 점도 빼놓을 수 없습니다. React, Vue, Svelte, Preact, SolidJS 등 원하는 UI 프레임워크를 섞어 쓸 수 있어서 기존에 만들어둔 컴포넌트를 그대로 가져다 쓸 수 있거든요.
설치 및 프로젝트 생성
Astro 프로젝트를 시작하는 가장 쉬운 방법은 공식 CLI를 사용하는 것입니다.
npm create astro@latest
실행하면 프로젝트 이름, 템플릿, TypeScript 설정 등을 물어보는 인터랙티브 프롬프트가 나타납니다.
astro Launch sequence initiated.
dir Where should we create your new project?
./my-astro-site
tmpl How would you like to start your new project?
Include sample files
ts Do you plan to write TypeScript?
Yes
use How strict should TypeScript be?
Strict
deps Install dependencies?
Yes
git Initialize a new git repository?
Yes
옵션을 선택하고 나면 프로젝트가 만들어지는데요, 설치가 끝나면 바로 개발 서버를 띄워볼 수 있습니다.
cd my-astro-site
npm run dev
브라우저에서 http://localhost:4321을 열면 Astro 기본 페이지가 반겨줍니다. 참고로 Astro가 4321 포트를 쓰는 이유는 “ASTRO”의 발음을 숫자로 옮긴 거라는 재미있는 비하인드가 있어요.
프로젝트 구조
처음 생성된 Astro 프로젝트는 아래와 같은 디렉토리 구조를 가지고 있습니다.
my-astro-site/
├── src/
│ ├── components/ # 재사용 가능한 컴포넌트
│ ├── layouts/ # 페이지 레이아웃
│ ├── pages/ # 파일 기반 라우팅의 핵심
│ └── content/ # 마크다운 콘텐츠 (선택사항)
├── public/ # 정적 파일 (이미지, 폰트 등)
├── astro.config.mjs # Astro 설정 파일
├── package.json
└── tsconfig.json
여기서 가장 중요한 디렉토리는 src/pages/입니다. 이곳에 파일을 만들면 자동으로 URL 경로가 만들어지는 파일 기반 라우팅 방식을 쓰거든요. 예를 들어 src/pages/about.astro 파일을 만들면 /about이라는 경로가 바로 생기는 식입니다.
public/ 디렉토리는 Astro가 건드리지 않고 빌드 결과물에 그대로 복사하는 파일을 두는 곳입니다. 파비콘이나 robots.txt, 소셜 미디어 OG 이미지처럼 원본 그대로 서빙되어야 하는 파일이 여기 들어갑니다.
.astro 컴포넌트 문법
Astro만의 고유한 파일 형식인 .astro를 살펴볼 차례입니다. 처음 보면 낯설 수 있지만 금방 익숙해지니 걱정 마세요.
---
// 이 영역은 "컴포넌트 스크립트"입니다 (서버에서만 실행됨)
interface Props {
name: string;
greeting?: string;
}
const { name, greeting = "안녕하세요" } = Astro.props;
const currentYear = new Date().getFullYear();
---
<!-- 이 영역은 HTML 템플릿입니다 -->
<div class="greeting">
<h2>{greeting}, {name}!</h2>
<p>오늘은 {currentYear}년입니다.</p>
</div>
<style>
/* 이 스타일은 이 컴포넌트에만 적용됩니다 (scoped) */
.greeting {
padding: 1rem;
border: 1px solid #ccc;
border-radius: 8px;
}
</style>
파일 맨 위에 ---로 감싼 영역이 컴포넌트 스크립트입니다. 여기에 작성한 JavaScript/TypeScript 코드는 빌드 시점이나 서버에서만 실행되고, 브라우저로는 절대 전달되지 않습니다. 데이터를 가져오거나, props를 처리하거나, 변수를 선언하는 용도로 쓰는데요. 이 덕분에 API 키 같은 민감한 정보도 안심하고 사용할 수 있죠.
그 아래에는 HTML 마크업을 작성하는데, JSX처럼 {}로 JavaScript 표현식을 삽입할 수 있습니다. <style> 블록은 자동으로 이 컴포넌트에만 스코프가 적용되어 다른 컴포넌트의 스타일과 충돌할 걱정이 없습니다.
컴포넌트를 사용할 때는 그냥 import해서 쓰면 됩니다.
---
import Greeting from '../components/Greeting.astro';
---
<html>
<body>
<Greeting name="세상" greeting="안녕하세요" />
</body>
</html>
페이지와 라우팅
Astro는 파일 기반 라우팅을 사용합니다. src/pages/ 디렉토리의 파일 구조가 곧 URL 구조가 되는 거죠.
src/pages/
├── index.astro → /
├── about.astro → /about
├── blog/
│ ├── index.astro → /blog
│ └── first-post.astro → /blog/first-post
└── contact.astro → /contact
.astro 파일뿐 아니라 .md, .mdx, .html 파일도 페이지로 인식하기 때문에 마크다운으로 바로 페이지를 만드는 것도 가능합니다.
URL 파라미터가 필요한 동적 페이지는 파일명에 대괄호를 사용합니다.
---
export async function getStaticPaths() {
return [
{ params: { slug: "hello-world" } },
{ params: { slug: "second-post" } },
];
}
const { slug } = Astro.params;
---
<h1>{slug} 포스트</h1>
getStaticPaths() 함수가 핵심인데요, 빌드 시점에 어떤 URL들을 생성할지 Astro에 알려주는 역할을 합니다. SSG는 빌드할 때 모든 경로를 미리 파악해야 하기 때문에 이런 함수가 필요한 거죠. 물론 데이터베이스나 외부 API에서 데이터를 가져와서 경로를 만들 수도 있습니다.
레이아웃
여러 페이지에 공통으로 적용되는 HTML 구조가 있다면 레이아웃으로 빼는 게 좋습니다. 헤더, 푸터, 내비게이션 같은 요소를 페이지마다 반복하지 않아도 되거든요.
---
interface Props {
title: string;
description?: string;
}
const { title, description = "기본 설명" } = Astro.props;
---
<!doctype html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="description" content={description} />
<title>{title}</title>
<link rel="stylesheet" href="/global.css" />
</head>
<body>
<header>
<nav>
<a href="/">홈</a>
<a href="/about">소개</a>
<a href="/blog">블로그</a>
</nav>
</header>
<main>
<slot />
</main>
<footer>
<p>© 2026 My Blog</p>
</footer>
</body>
</html>
여기서 <slot />이 핵심입니다. 레이아웃을 사용하는 페이지의 콘텐츠가 이 위치에 주입됩니다. Vue나 Svelte의 슬롯과 동일한 개념이고, React의 children에 해당한다고 보면 됩니다.
---
import BaseLayout from '../layouts/BaseLayout.astro';
---
<BaseLayout title="소개 페이지" description="저에 대해 알아보세요">
<h1>안녕하세요!</h1>
<p>저는 웹 개발자입니다.</p>
</BaseLayout>
콘텐츠 컬렉션
블로그처럼 구조가 반복되는 콘텐츠를 다룰 때는 콘텐츠 컬렉션(Content Collections)이 정말 유용합니다. 마크다운 파일의 프런트매터를 Zod 스키마로 정의해두면, 타입이 맞지 않을 때 빌드 시점에서 바로 잡아주거든요.
먼저 src/content/config.ts에 스키마를 정의합니다.
import { defineCollection, z } from "astro:content";
const blog = defineCollection({
type: "content",
schema: z.object({
title: z.string(),
description: z.string(),
date: z.date(),
tags: z.array(z.string()).default([]),
draft: z.boolean().default(false),
}),
});
export const collections = { blog };
그다음 src/content/blog/ 디렉토리에 마크다운 파일을 넣으면 됩니다.
---
title: "첫 번째 포스트"
description: "블로그를 시작했습니다."
date: 2024-01-24
tags:
- 일상
- 개발
---
안녕하세요! 첫 번째 블로그 포스트입니다.
이렇게 등록한 컬렉션 데이터는 페이지에서 가져다 쓸 수 있습니다.
---
import { getCollection } from "astro:content";
const posts = await getCollection("blog", ({ data }) => {
return !data.draft; // draft가 아닌 포스트만
});
// 최신 순 정렬
posts.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("ko-KR")}</time>
</li>
))}
</ul>
스키마가 있으면 프런트매터에서 date 필드에 문자열을 넣었다든가, title을 빠뜨렸다든가 하는 실수를 빌드할 때 바로 잡아줍니다. 포스트가 수백 개로 늘어나면 이 안정성이 정말 빛을 발하죠. 콘텐츠 컬렉션의 스키마 정의, 쿼리, 고급 패턴에 대해 더 자세히 알고 싶다면 Astro Content Collections로 콘텐츠를 타입 안전하게 관리하기를 참고하세요.
아일랜드 아키텍처
앞에서 Astro가 기본적으로 클라이언트에 JavaScript를 보내지 않는다고 했는데요, 그러면 검색 기능이나 다크모드 토글 같은 인터랙션은 어떻게 구현할까요? 이때 등장하는 게 클라이언트 디렉티브(client directive)입니다.
먼저 사용하려는 UI 프레임워크의 통합(integration)을 설치해야 합니다. React를 예로 들면 이렇게 한 줄이면 끝납니다.
npx astro add react
이 명령어가 astro.config.mjs에 React 통합을 자동으로 추가해줍니다.
import { defineConfig } from "astro/config";
import react from "@astrojs/react";
export default defineConfig({
integrations: [react()],
});
이제 평소처럼 React 컴포넌트를 만들고…
import { useState } from "react";
export default function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>현재 카운트: {count}</p>
<button onClick={() => setCount(count + 1)}>+1</button>
<button onClick={() => setCount(count - 1)}>-1</button>
</div>
);
}
Astro 페이지에서 client:load 디렉티브를 붙여주면 됩니다.
---
import Counter from '../components/Counter.tsx';
---
<h1>메인 페이지</h1>
<p>이 텍스트는 정적 HTML입니다.</p>
<!-- 이 컴포넌트만 클라이언트에서 하이드레이션됩니다 -->
<Counter client:load />
client:load를 안 붙이면 어떻게 될까요? 컴포넌트가 빌드 시점에 HTML로 렌더링되고, 클라이언트에서는 그냥 정적인 텍스트로만 보입니다. 버튼을 클릭해도 아무 반응이 없죠. client:load를 붙여야 비로소 React가 하이드레이션되어 인터랙션이 살아나는 겁니다.
클라이언트 디렉티브는 하이드레이션 시점에 따라 여러 종류가 있습니다.
client:load— 페이지 로드 즉시 하이드레이션. 바로 상호작용이 필요한 컴포넌트에 적합client:idle— 브라우저가 유휴 상태일 때 하이드레이션. 우선순위가 낮은 컴포넌트에 적합client:visible— 뷰포트에 들어올 때 하이드레이션. 스크롤해야 보이는 컴포넌트에 적합client:only— 서버 렌더링 없이 클라이언트에서만 렌더링.window객체에 의존하는 컴포넌트용
실전에서는 client:visible을 많이 씁니다. 페이지 하단에 있는 댓글 위젯이나 차트 컴포넌트에 붙여놓으면 사용자가 스크롤해서 도달하기 전까지는 JavaScript를 아예 로드하지 않으니까 초기 성능이 훨씬 좋아집니다.
이미지 최적화
이미지 최적화도 별도 플러그인 없이 바로 쓸 수 있습니다. astro:assets에서 Image 컴포넌트를 import하면 빌드 시 자동으로 WebP 변환, width/height 삽입, lazy loading 적용이 이루어집니다.
---
import { Image } from "astro:assets";
import heroImage from "../assets/hero.png";
---
<Image src={heroImage} alt="히어로 이미지" width={800} height={400} />
한 가지 주의할 점은 이미지를 src/assets/ 디렉토리에 넣어야 최적화 대상이 된다는 겁니다. public/ 디렉토리에 넣으면 그대로 복사만 되고 최적화는 적용되지 않아요.
마크다운 파일에서도 상대 경로로 이미지를 참조하면 동일하게 최적화됩니다.

<Picture /> 컴포넌트를 써서 AVIF와 WebP를 동시에 제공하거나, 반응형 이미지를 설정하는 것도 가능한데요. 이 부분은 Astro 이미지 최적화에서 자세히 다루고 있습니다.
스타일링
Astro 컴포넌트의 <style> 블록은 기본적으로 해당 컴포넌트에만 스코프됩니다. 클래스 이름 충돌을 걱정할 필요가 없다는 뜻이죠. 하지만 슬롯으로 전달된 자식 요소에 스타일을 적용하고 싶을 때는 :global() 선택자를 사용하면 됩니다.
<div class="card">
<slot />
</div>
<style>
.card {
padding: 1rem;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
/* 슬롯 자식 요소에 스타일 적용 */
.card :global(h2) {
font-size: 1.5rem;
margin-top: 0;
}
</style>
Tailwind CSS나 Sass를 쓰고 싶다면 공식 통합으로 간편하게 연동할 수 있습니다.
# Tailwind CSS
npx astro add tailwind
# Sass는 패키지만 설치하면 바로 사용 가능
npm install sass
데이터 가져오기
Astro 컴포넌트 스크립트에서는 fetch()로 외부 API 데이터를 가져올 수 있습니다. 중요한 건 이 코드가 빌드 시점(또는 서버)에서 실행된다는 점입니다. 즉, API 키를 브라우저에 노출하지 않고도 외부 데이터를 사용할 수 있어요.
---
// 빌드 시점에 실행됩니다 (브라우저가 아닌 서버에서)
const response = await fetch("https://jsonplaceholder.typicode.com/posts");
const posts = await response.json();
---
<ul>
{posts.slice(0, 10).map((post) => (
<li>
<h2>{post.title}</h2>
<p>{post.body}</p>
</li>
))}
</ul>
환경 변수는 import.meta.env로 접근합니다. PUBLIC_ 접두사가 있는 변수만 클라이언트에 노출되고, 나머지는 서버 측에서만 접근 가능합니다.
SECRET_API_KEY=my-secret-key # 서버에서만 접근 가능
PUBLIC_API_URL=https://api.example.com # 클라이언트에서도 접근 가능
빌드와 배포
프로덕션 빌드는 간단합니다.
npm run build
dist/ 디렉토리에 정적 파일이 생성되는데, 이걸 아무 정적 호스팅에 올리면 끝입니다.
Astro는 기본적으로 정적 사이트를 생성하지만, 서버 사이드 렌더링이 필요한 경우에는 공식 어댑터를 추가하면 됩니다.
# Vercel 배포용
npx astro add vercel
# Netlify 배포용
npx astro add netlify
# Cloudflare Pages 배포용
npx astro add cloudflare
어댑터를 설치하면 astro.config.mjs에서 출력 모드를 설정할 수 있습니다.
import { defineConfig } from "astro/config";
import vercel from "@astrojs/vercel/serverless";
export default defineConfig({
output: "server",
adapter: vercel(),
});
output 옵션에 따라 렌더링 방식이 달라집니다. "static"은 모든 페이지를 빌드 시점에 HTML로 만들고 "server"는 요청마다 서버에서 렌더링합니다. "hybrid"는 페이지별로 정적과 SSR을 섞어 쓸 수 있게 해주는데요, 블로그나 문서 사이트는 "static" 기본값으로 충분하고 사용자별 대시보드 같은 동적 페이지가 섞여 있다면 "hybrid"를 고려해보세요.
마치며
Astro는 콘텐츠 중심 사이트에 딱 맞는 프레임워크입니다. JavaScript를 기본적으로 안 보낸다는 과감한 선택 덕분에 성능이 좋고 아일랜드 아키텍처로 필요한 곳에만 인터랙션을 넣을 수 있으니 유연하기까지 하죠.
.astro 파일 문법도 HTML을 알면 거의 바로 쓸 수 있을 정도로 진입 장벽이 낮습니다. Props 타입 시스템, 슬롯, CSS 스코핑 같은 컴포넌트의 고급 기능이 궁금하다면 Astro 컴포넌트 깊이 파헤치기를 참고해보세요. 기존에 React나 Vue로 만든 컴포넌트가 있다면 그대로 끌어다 쓸 수 있어서 전환 비용도 부담되지 않고요.
Lighthouse로 성능을 측정해보면 놀라는 분이 많더라고요. 처음에는 “JavaScript 없이 괜찮을까?” 싶다가도 직접 써보면 대부분의 콘텐츠 사이트에는 이 방식이 훨씬 합리적이라는 걸 느끼게 됩니다.
정적 사이트 생성기로 블로그를 시작해보고 싶은 분이라면 Astro를 한번 시도해보시길 추천합니다.
더 깊이 공부하고 싶다면 Astro 공식 문서를 살펴보세요. 튜토리얼부터 레시피까지 잘 정리되어 있어서 이 글에서 다루지 못한 View Transitions, 미들웨어, Server Islands 같은 고급 기능도 배울 수 있습니다.
This work is licensed under
CC BY 4.0