핵심 요약

  • Astro v5+부터 Content Collections는 glob() 로더 기반으로 재설계됐습니다.
  • Zod 스키마로 마크다운 프런트매터를 빌드 시점에 타입 검증합니다.
  • 잘못된 프런트매터가 있으면 astro build가 실패하므로 런타임 오류를 사전에 방지합니다.

Astro v5+ glob 로더 API

Astro v4까지는 src/content/config.ts에서 컬렉션을 정의했습니다. v5부터는 파일 이름이 src/content.config.ts로 바뀌었고, 로더(loader) 방식이 도입됐습니다.

// src/content.config.ts (v5+ 방식)
import { defineCollection } from 'astro:content';
import { glob } from 'astro/loaders';
import { z } from 'astro/zod';

const blog = defineCollection({
  loader: glob({ base: './src/content/blog', pattern: '**/*.{md,mdx}' }),
  schema: z.object({
    title: z.string(),
    description: z.string(),
    pubDate: z.coerce.date(),
  }),
});

export const collections = { blog };

v4 이하와의 차이점:

  • type: 'content' 대신 loader: glob(...) 사용
  • 파일 경로가 src/content/config.ts에서 src/content.config.ts로 변경
  • 로더를 커스텀 구현하거나 외부 API에서 데이터를 가져오는 것도 가능해짐

Zod 스키마로 프런트매터 검증

Zod는 TypeScript-first 스키마 검증 라이브러리입니다. Astro는 astro/zod에서 Zod를 re-export하므로 별도 설치 없이 사용할 수 있습니다.

기본 타입 검증

const blog = defineCollection({
  loader: glob({ base: './src/content/blog', pattern: '**/*.{md,mdx}' }),
  schema: z.object({
    title: z.string(),                              // 필수 문자열
    description: z.string(),                        // 필수 문자열
    pubDate: z.coerce.date(),                       // 문자열을 Date로 변환
    updatedDate: z.coerce.date().optional(),        // 선택 필드
    category: z.enum(['프로젝트', '에세이', 'TIL']),  // 열거형
    tags: z.array(z.string()).default([]),           // 기본값 있는 배열
    draft: z.boolean().default(false),              // 기본값 있는 불리언
    schemaType: z.enum(['article', 'faq', 'howto']).default('article'),
  }),
});

z.coerce.date()는 마크다운 프런트매터의 2026-04-04 같은 문자열을 자동으로 JavaScript Date 객체로 변환합니다. z.coerce 없이 z.date()를 쓰면 YAML 파싱 결과에 따라 변환이 안 될 수 있습니다.

이미지 필드 처리

coverImage 같은 이미지 경로는 Astro의 이미지 최적화 파이프라인과 통합하기 위해 특별한 처리가 필요합니다:

const blog = defineCollection({
  loader: glob({ base: './src/content/blog', pattern: '**/*.{md,mdx}' }),
  schema: ({ image }) =>
    z.object({
      title: z.string(),
      coverImage: z.optional(image()),  // image() 헬퍼 사용
    }),
});

schema가 함수 형태일 때 { image } 헬퍼를 받아 사용합니다. image()는 상대 경로를 검증하고 <Image /> 컴포넌트와 통합됩니다.


Content Collections 사용 방법

컬렉션을 정의한 후 페이지에서 데이터를 가져오는 방법입니다.

모든 포스트 목록

// src/pages/blog/index.astro
---
import { getCollection } from 'astro:content';

const posts = await getCollection('blog', ({ data }) => {
  return data.draft !== true;  // 드래프트 제외
});

// 날짜 내림차순 정렬
const sortedPosts = posts.sort(
  (a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf()
);
---

개별 포스트 렌더링

// src/pages/blog/[slug].astro
---
import { getCollection, render } from 'astro:content';

export async function getStaticPaths() {
  const posts = await getCollection('blog');
  return posts.map((post) => ({
    params: { slug: post.id },
    props: post,
  }));
}

const post = Astro.props;
const { Content } = await render(post);
---

<article>
  <h1>{post.data.title}</h1>
  <Content />
</article>

v5+에서는 post.slug 대신 post.id를 사용합니다. 또한 마크다운을 HTML로 변환하려면 entry.render() 대신 render(entry)를 import해서 사용합니다.


빌드 시점 타입 검증의 이점

Zod 스키마 덕분에 마크다운 파일의 프런트매터가 잘못됐을 때 astro build 또는 astro dev 실행 시 즉시 오류가 발생합니다:

[ERROR] Invalid content entry frontmatter.
  src/content/blog/my-post.md
  > "category" must be one of '프로젝트' | '에세이' | 'TIL'

런타임에 undefined로 조용히 실패하는 대신 빌드가 실패하므로 배포 전에 문제를 발견할 수 있습니다. TypeScript와 조합하면 post.data.title의 타입이 자동으로 string으로 추론됩니다.