[2026] Astro Content Collections 완벽 가이드 | 타입 안전·스키마·MDX·블로그 구축
이 글의 핵심
Astro Content Collections로 타입 안전한 콘텐츠 관리 시스템을 구축하는 완벽 가이드입니다. 스키마 정의, MDX, 블로그, 다국어, SEO까지 실전 예제로 정리했습니다.
실무 경험 공유: 기술 블로그 플랫폼을 Astro Content Collections로 구축하면서, 타입 안전성을 100% 확보하고 콘텐츠 관리 오류를 완전히 제거한 경험을 공유합니다.
들어가며: “마크다운 관리가 복잡해요”
실무 문제 시나리오
시나리오 1: 프론트매터 오타
YAML 프론트매터에 오타가 있어도 런타임에만 발견됩니다. Content Collections는 빌드 타임에 검증합니다.
시나리오 2: 타입 안전성 부족
마크다운 데이터를 사용할 때 타입이 없습니다. Content Collections는 자동 타입 생성합니다.
시나리오 3: 복잡한 쿼리
여러 조건으로 콘텐츠를 필터링하기 어렵습니다. Content Collections는 강력한 쿼리 API를 제공합니다.
1. Content Collections란?
핵심 개념
Content Collections는 Astro의 타입 안전 콘텐츠 관리 시스템입니다. 주요 장점:
- 타입 안전: Zod 스키마로 프론트매터 검증
- 자동 타입 생성: TypeScript 타입 자동 생성
- 강력한 쿼리: 필터링, 정렬, 페이지네이션
- MDX 지원: React 컴포넌트 사용 가능
- 빌드 타임 검증: 오류를 미리 발견
2. 기본 설정
디렉터리 구조
아래 코드는 code를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
src/
└── content/
├── config.ts
├── blog/
│ ├── post-1.md
│ └── post-2.mdx
└── docs/
├── intro.md
└── guide.md
스키마 정의
다음은 typescript를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// src/content/config.ts
import { defineCollection, z } from 'astro:content';
const blogCollection = defineCollection({
type: 'content',
schema: z.object({
title: z.string(),
description: z.string(),
pubDate: z.date(),
updatedDate: z.date().optional(),
author: z.string().default('Anonymous'),
tags: z.array(z.string()),
draft: z.boolean().default(false),
featured: z.boolean().default(false),
}),
});
const docsCollection = defineCollection({
type: 'content',
schema: z.object({
title: z.string(),
description: z.string(),
order: z.number(),
category: z.enum(['guide', 'api', 'tutorial']),
}),
});
export const collections = {
blog: blogCollection,
docs: docsCollection,
};
3. 콘텐츠 작성
마크다운
아래 코드는 markdown를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
title: 'Getting Started with Astro
description: 'Learn how to build fast websites with Astro'
pubDate: 2026-04-11
author: 'JB'
tags: ['astro', 'tutorial']
draft: false
---
featured: true
# Getting Started
Astro is a modern static site generator...
MDX (컴포넌트 사용)
다음은 mdx를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
title: 'Interactive Guide
description: 'Learn with interactive examples'
pubDate: 2026-04-11
---
tags: ['interactive']
import Button from '@/components/Button.astro';
import Counter from '@/components/Counter.tsx';
# Interactive Guide
Click the button below:
<Button>Click me</Button>
Try the counter:
<Counter client:load />
4. 콘텐츠 조회
전체 조회
다음은 astro를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 비동기 처리를 통해 효율적으로 작업을 수행합니다, 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// src/pages/blog/index.astro
import { getCollection } from 'astro:content';
const allPosts = await getCollection('blog');
---
const publishedPosts = allPosts.filter(post => !post.data.draft);
<h1>Blog Posts</h1>
<ul>
{publishedPosts.map(post => (
<li>
<a href={`/blog/${post.slug}`}>{post.data.title}</a>
</li>
))}
</ul>
필터링 및 정렬
다음은 astro를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 비동기 처리를 통해 효율적으로 작업을 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
import { getCollection } from 'astro:content';
// 필터링
const featuredPosts = await getCollection('blog', ({ data }) => {
return data.featured && !data.draft;
});
// 정렬
const sortedPosts = featuredPosts.sort((a, b) =>
b.data.pubDate.valueOf() - a.data.pubDate.valueOf()
);
// 태그별 필터링
const tag = Astro.params.tag;
const tagPosts = await getCollection('blog', ({ data }) => {
return data.tags.includes(tag);
---
});
단일 조회
다음은 astro를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// src/pages/blog/[...slug].astro
import { getEntry } from 'astro:content';
const { slug } = Astro.params;
const post = await getEntry('blog', slug);
if (!post) {
return Astro.redirect('/404');
}
---
const { Content } = await post.render();
<article>
<h1>{post.data.title}</h1>
<p>By {post.data.author} on {post.data.pubDate.toLocaleDateString()}</p>
<Content />
</article>
5. 동적 라우팅
getStaticPaths
다음은 astro를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 비동기 처리를 통해 효율적으로 작업을 수행합니다, 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// src/pages/blog/[...slug].astro
import { getCollection } from 'astro:content';
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();
<article>
<h1>{post.data.title}</h1>
<Content />
</article>
태그 페이지
다음은 astro를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 비동기 처리를 통해 효율적으로 작업을 수행합니다, 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// src/pages/blog/tag/[tag].astro
import { getCollection } from 'astro:content';
export async function getStaticPaths() {
const posts = await getCollection('blog');
const allTags = [...new Set(posts.flatMap(post => post.data.tags))];
return allTags.map(tag => ({
params: { tag },
props: {
posts: posts.filter(post => post.data.tags.includes(tag)),
},
}));
}
const { tag } = Astro.params;
---
const { posts } = Astro.props;
<h1>Posts tagged with "{tag}"</h1>
<ul>
{posts.map(post => (
<li>
<a href={`/blog/${post.slug}`}>{post.data.title}</a>
</li>
))}
</ul>
6. 페이지네이션
다음은 astro를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 비동기 처리를 통해 효율적으로 작업을 수행합니다, 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// src/pages/blog/[...page].astro
import { getCollection } from 'astro:content';
import type { GetStaticPaths } from 'astro';
export const getStaticPaths: GetStaticPaths = async ({ paginate }) => {
const posts = await getCollection('blog', ({ data }) => !data.draft);
const sortedPosts = posts.sort((a, b) =>
b.data.pubDate.valueOf() - a.data.pubDate.valueOf()
);
return paginate(sortedPosts, { pageSize: 10 });
};
---
const { page } = Astro.props;
<h1>Blog Posts</h1>
<ul>
{page.data.map(post => (
<li>
<a href={`/blog/${post.slug}`}>{post.data.title}</a>
</li>
))}
</ul>
<nav>
{page.url.prev && <a href={page.url.prev}>Previous</a>}
<span>Page {page.currentPage} of {page.lastPage}</span>
{page.url.next && <a href={page.url.next}>Next</a>}
</nav>
7. 관련 글 추천
다음은 astro를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
import { getCollection } from 'astro:content';
const { slug } = Astro.params;
const post = await getEntry('blog', slug);
const allPosts = await getCollection('blog', ({ data }) => !data.draft);
// 같은 태그를 가진 글 찾기
const relatedPosts = allPosts
.filter(p =>
p.slug !== slug &&
p.data.tags.some(tag => post.data.tags.includes(tag))
)
---
.slice(0, 3);
<article>
<h1>{post.data.title}</h1>
<Content />
</article>
<aside>
<h2>Related Posts</h2>
<ul>
{relatedPosts.map(related => (
<li>
<a href={`/blog/${related.slug}`}>{related.data.title}</a>
</li>
))}
</ul>
</aside>
8. RSS 피드 생성
다음은 typescript를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 비동기 처리를 통해 효율적으로 작업을 수행합니다, 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// 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', ({ data }) => !data.draft);
return rss({
title: 'My Blog',
description: 'A blog about web development',
site: context.site,
items: posts.map(post => ({
title: post.data.title,
description: post.data.description,
pubDate: post.data.pubDate,
link: `/blog/${post.slug}/`,
})),
});
}
9. 다국어 지원
아래 코드는 typescript를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// src/content/config.ts
const blogCollection = defineCollection({
type: 'content',
schema: z.object({
title: z.string(),
description: z.string(),
lang: z.enum(['en', 'ko', 'ja']).default('en'),
translationKey: z.string().optional(),
}),
});
다음은 astro를 활용한 상세한 구현 코드입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// 같은 글의 다른 언어 버전 찾기
const currentPost = await getEntry('blog', slug);
const allPosts = await getCollection('blog');
const translations = allPosts.filter(post =>
post.data.translationKey === currentPost.data.translationKey &&
post.slug !== slug
---
);
<nav>
{translations.map(translation => (
<a href={`/blog/${translation.slug}`}>
{translation.data.lang.toUpperCase()}
</a>
))}
</nav>
10. 실전 예제: 기술 블로그
다음은 typescript를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// src/content/config.ts
import { defineCollection, z } from 'astro:content';
const blogCollection = defineCollection({
type: 'content',
schema: z.object({
title: z.string(),
description: z.string(),
pubDate: z.date(),
updatedDate: z.date().optional(),
author: z.string(),
category: z.enum(['frontend', 'backend', 'devops', 'ai']),
tags: z.array(z.string()),
draft: z.boolean().default(false),
featured: z.boolean().default(false),
coverImage: z.string().optional(),
readingTime: z.number(),
relatedPosts: z.array(z.string()).optional(),
}),
});
export const collections = { blog: blogCollection };
다음은 astro를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 비동기 처리를 통해 효율적으로 작업을 수행합니다, 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// src/pages/blog/index.astro
import { getCollection } from 'astro:content';
import Layout from '@/layouts/Layout.astro';
const allPosts = await getCollection('blog', ({ data }) => !data.draft);
const sortedPosts = allPosts.sort((a, b) =>
b.data.pubDate.valueOf() - a.data.pubDate.valueOf()
);
const featuredPosts = sortedPosts.filter(post => post.data.featured).slice(0, 3);
const recentPosts = sortedPosts.slice(0, 10);
---
const categories = [...new Set(allPosts.map(post => post.data.category))];
<Layout title="Blog">
<section>
<h2>Featured Posts</h2>
<div class="grid">
{featuredPosts.map(post => (
<article class="card">
{post.data.coverImage && (
<img src={post.data.coverImage} alt={post.data.title} />
)}
<h3>
<a href={`/blog/${post.slug}`}>{post.data.title}</a>
</h3>
<p>{post.data.description}</p>
<div class="meta">
<span>{post.data.category}</span>
<span>{post.data.readingTime}분</span>
</div>
</article>
))}
</div>
</section>
<section>
<h2>Recent Posts</h2>
<ul>
{recentPosts.map(post => (
<li>
<a href={`/blog/${post.slug}`}>{post.data.title}</a>
<time>{post.data.pubDate.toLocaleDateString()}</time>
</li>
))}
</ul>
</section>
<aside>
<h2>Categories</h2>
<ul>
{categories.map(category => (
<li>
<a href={`/blog/category/${category}`}>{category}</a>
</li>
))}
</ul>
</aside>
</Layout>
정리 및 체크리스트
핵심 요약
- Content Collections: Astro의 타입 안전 콘텐츠 관리
- Zod 스키마: 프론트매터 검증
- 자동 타입 생성: TypeScript 타입 자동 생성
- 강력한 쿼리: 필터링, 정렬, 페이지네이션
- MDX 지원: React 컴포넌트 사용 가능
구현 체크리스트
- Content Collections 설정
- 스키마 정의
- 콘텐츠 작성
- 동적 라우팅 구현
- 페이지네이션 구현
- RSS 피드 생성
- SEO 최적화
같이 보면 좋은 글
- Astro 블로그 완벽 가이드
- Next.js 15 완벽 가이드
- MDX 완벽 가이드
이 글에서 다루는 키워드
Astro, Content Collections, MDX, TypeScript, Blog, CMS, Static Site
자주 묻는 질문 (FAQ)
Q. Content Collections vs 일반 마크다운, 어떤 게 나은가요?
A. Content Collections는 타입 안전성, 스키마 검증, 강력한 쿼리 API를 제공합니다. 소규모 프로젝트는 일반 마크다운도 충분하지만, 중대형 프로젝트는 Content Collections를 권장합니다.
Q. MDX를 사용해야 하나요?
A. 인터랙티브 컴포넌트가 필요하면 MDX를 사용하세요. 단순 텍스트만 있다면 일반 마크다운이 더 빠릅니다.
Q. CMS와 통합할 수 있나요?
A. 네, Contentful, Sanity 등 Headless CMS와 통합 가능합니다. 다만 Content Collections의 타입 안전성 이점은 줄어듭니다.
Q. 성능은 어떤가요?
A. 빌드 타임에 모든 콘텐츠를 처리하므로 런타임 성능은 매우 빠릅니다. 콘텐츠가 수천 개여도 문제없습니다.