Astro Content Collections 완벽 가이드: 개념부터 Schema 검증 실전까지

블로그 홈이 멈추면서 어떤 글의 publishDate 필드 형식이 잘못됐다는 오류가 떴습니다. 파일을 뒤지는 데만 30분이 걸렸고, 문제는 날짜를 2024-12-01이 아니라 2024/12/01로 작성한 것이었습니다. 글 30개짜리 작은 블로그에서도 이 정도인데, 글이 수백 개라면 필드를 추가할 때마다 모든 파일을 일일이 확인해야 합니다.
Content Collections는 바로 이 문제를 해결합니다. TypeScript가 코드 오류를 검사하듯 Astro가 콘텐츠 오류를 자동으로 검사하게 해 줍니다. Schema 구성을 마치면 편집기에서 자동 완성도 제공되므로 필드 이름을 확인하려고 문서를 뒤질 필요가 없습니다. 이 글에서는 Content Collections가 무엇인지, 설정 파일을 어떻게 작성하는지, Schema 검증을 어떻게 사용하는지 설명합니다.
Content Collections란 무엇이며 왜 필요한가요?
Content Collections도 결국 Markdown 폴더를 관리하는 기능 아닌가요? src/pages/ 아래에 blog/ 폴더를 직접 만들어도 블로그 기능을 구현할 수 있지 않을까요?
맞습니다. 기능 자체는 구현할 수 있습니다. 하지만 이 방식에는 타입 안전성 보호가 없습니다.
기존 방식에서 Markdown frontmatter는 다음과 같습니다.
---
title: "내 블로그 제목"
date: "2024-12-01"
tags: ["Astro", "튜토리얼"]
---
글 내용...
보기에는 아무 문제가 없어 보입니다. 하지만 다음과 같은 상황을 생각해 보세요.
- 어떤 글에서
tags를tag로 잘못 작성했습니다(s 하나가 빠짐). - 날짜를
2024-12-01이 아니라12/01/2024로 작성했습니다. author필드를 새로 추가했지만 일부 예전 글에는 넣지 않았습니다.
Astro는 이런 오류를 미리 알려 주지 않습니다. 런타임에 페이지 렌더링이 실패하고 나서야 어디가 잘못됐는지 알게 됩니다.
Content Collections는 바로 이 문제를 해결합니다. 본질적으로 타입 안전성을 갖춘 콘텐츠 관리 시스템입니다. 쉽게 말해 Markdown 파일에 TypeScript 타입 검사를 추가하는 것입니다.
구체적으로 Content Collections는 다음 기능을 제공합니다.
- Schema 검증: frontmatter 필드의 타입과 구조를 정의하고 일치하지 않으면 바로 오류를 표시합니다.
- 자동 타입 생성: Schema를 바탕으로 TypeScript 타입을 자동 생성해 편집기에서 자동 완성을 제공합니다.
- 통합 조회 API:
getCollection()같은 메서드로 콘텐츠를 조회하고 타입 안전한 데이터를 반환합니다. - 성능 최적화: Astro 5.0에서 도입된 Content Layer API로 조회 속도가 더 빨라졌습니다.
간단히 말하면 기존 방식은 ‘자유롭지만 안전하지 않고’, Content Collections는 ‘제약은 있지만 신뢰할 수 있습니다’. Schema 구성에 시간을 조금 더 들이면 초보적인 오류의 99%를 피할 수 있습니다.
솔직히 지금은 제 모든 Astro 프로젝트에서 Content Collections를 사용합니다. 한 번 구성하면 프로젝트 전체에서 계속 이점을 누릴 수 있습니다.
Content Collections 구성 실전
이론은 여기까지 하고 바로 구성해 보겠습니다. 전체 과정은 디렉터리 만들기, 설정 작성하기, 콘텐츠 만들기의 세 단계입니다.
1단계: 디렉터리 만들기
Content Collections에서는 콘텐츠를 src/content/ 디렉터리 아래에 둬야 합니다. Astro v2.0부터 콘텐츠 컬렉션을 저장하기 위해 마련된 예약 디렉터리입니다.
디렉터리 구조는 대략 다음과 같습니다.
src/
├── content/
│ ├── blog/ # 블로그 컬렉션
│ │ ├── post-1.md
│ │ └── post-2.md
│ └── docs/ # 문서 컬렉션
│ ├── guide-1.md
│ └── guide-2.md
├── content.config.ts # 설정 파일(위치에 주의)
└── pages/
└── ...
주의: 설정 파일은 content/ 디렉터리 안이 아니라 src/content.config.ts(또는 .js, .mjs)에 둡니다. 저도 처음에는 파일 위치를 잘못 지정해 한참 동안 문제를 찾았습니다.
각 하위 디렉터리가 하나의 컬렉션(Collection)입니다. 예를 들어 src/content/blog/는 blog 컬렉션이고, src/content/docs/는 docs 컬렉션입니다.
2단계: 설정 파일 작성하기
Content Collections의 핵심인 src/content.config.ts를 만듭니다.
// src/content.config.ts
import { defineCollection, z } from 'astro:content';
// blog 컬렉션 정의
const blogCollection = defineCollection({
type: 'content', // 타입: content는 Markdown/MDX 파일을 의미
schema: z.object({
title: z.string(), // 제목(필수)
description: z.string(), // 설명(필수)
pubDate: z.coerce.date(), // 게시일(Date 객체로 자동 변환)
tags: z.array(z.string()).optional(), // 태그 배열(선택 사항)
draft: z.boolean().default(false), // 초안 상태(기본값 false)
}),
});
// collections 객체 내보내기
export const collections = {
'blog': blogCollection, // 키 이름은 디렉터리 이름에 대응
};
코드가 조금 복잡해 보일 수 있으니 나눠서 살펴보겠습니다.
defineCollection(): 컬렉션 설정을 정의합니다.type: 'content': Markdown/MDX 파일 형식의 컬렉션이라는 뜻입니다.schema: 검증 라이브러리인 Zod로 frontmatter 구조를 정의합니다.collections객체: 컬렉션 설정을 내보내며, 키 이름은 디렉터리 이름과 같아야 합니다.
핵심은 schema 부분입니다. 각 필드는 z.xxx()로 타입을 정의합니다.
z.string(): 문자열 타입z.coerce.date(): 문자열을 Date 객체로 자동 변환z.array(z.string()): 문자열 배열.optional(): 선택적 필드.default(false): 기본값 설정
3단계: 콘텐츠 파일 만들기
구성을 마치면 src/content/blog/ 아래에 Markdown 파일을 만들 수 있습니다.
---
title: "Astro Content Collections 입문"
description: "Content Collections를 구성하고 사용하는 방법 알아보기"
pubDate: "2024-12-01"
tags: ["Astro", "튜토리얼"]
---
글 내용...
frontmatter가 Schema 정의를 충족하면 Astro가 정상적으로 파싱합니다. 필드가 일치하지 않으면(예: pubDate 형식 오류) Astro가 빌드 시점에 바로 오류를 표시합니다.
페이지에서 데이터 조회하기
구성을 마치면 어떤 Astro 파일에서든 콘텐츠를 조회할 수 있습니다.
---
// src/pages/blog/index.astro
import { getCollection } from 'astro:content';
// 모든 블로그 글 가져오기
const allPosts = await getCollection('blog');
// 초안 제외(draft: true)
const publishedPosts = allPosts.filter(post => !post.data.draft);
---
<ul>
{publishedPosts.map(post => (
<li>
<a href={`/blog/${post.slug}`}>
{post.data.title}
</a>
<p>{post.data.description}</p>
</li>
))}
</ul>
post.data는 frontmatter 데이터이며 완전한 TypeScript 타입 힌트가 제공됩니다. VS Code에서 post.data.를 입력하면 편집기가 title, description, pubDate 등의 필드를 자동으로 제안합니다.
바로 이것이 Content Collections의 매력입니다. 타입 안전성에 편집기 자동 완성까지 더해져 코딩 경험이 한 단계 좋아집니다.
Schema 검증 깊이 알아보기
앞 절에서는 z.string(), z.coerce.date() 같은 기본 타입을 사용했습니다. 하지만 Schema 검증 기능은 여기서 끝나지 않습니다. 이 절에서는 Zod의 여러 사용법을 더 깊이 살펴보겠습니다.
기본 타입 빠르게 확인하기
먼저 자주 사용하는 타입을 살펴보겠습니다.
import { z } from 'astro:content';
z.string() // 문자열
z.number() // 숫자
z.boolean() // 불리언
z.date() // Date 객체
z.coerce.date() // 문자열을 Date로 자동 변환
z.array(z.string()) // 문자열 배열
z.enum(['draft', 'published']) // 열거형(지정된 값만 허용)
이 중 z.coerce.date()는 특히 유용합니다. 사실 Markdown frontmatter에서 날짜는 문자열 형식("2024-12-01")으로 작성합니다. z.date()는 Date 객체를 요구하기 때문에 오류가 나지만, z.coerce.date()는 자동으로 변환해 주므로 번거로움을 크게 줄일 수 있습니다.
선택적 필드와 기본값
모든 필드가 필수인 것은 아닙니다. 예를 들어 tags가 필요 없는 글도 있을 수 있습니다. 이때 .optional()을 사용합니다.
schema: z.object({
title: z.string(), // 필수
tags: z.array(z.string()).optional(), // 선택 사항
draft: z.boolean().default(false), // 기본값 있음
})
.default()도 편리합니다. frontmatter에 해당 필드가 없으면 Astro가 기본값을 자동으로 채웁니다.
고급 사용법: 이미지 검증
Astro는 이미지 경로 검증 전용 image() 타입도 제공합니다.
import { defineCollection, z } from 'astro:content';
const blogCollection = defineCollection({
schema: ({ image }) => z.object({ // 주의: 여기서는 함수 형식을 사용
title: z.string(),
cover: image(), // 이미지 경로 검증
}),
});
image()는 경로가 유효한 이미지 파일을 가리키는지 확인합니다(상대 경로 지원). 블로그 홈에 커버 이미지를 표시할 때 특히 유용한 기능입니다.
다른 컬렉션 참조하기: z.reference()
콘텐츠끼리 연결해야 할 때가 있습니다. 예를 들어 블로그 글이 특정 카테고리에 속하고, 카테고리 자체도 컬렉션일 수 있습니다. 이때 z.reference()를 사용합니다.
// 카테고리 컬렉션 정의
const categoryCollection = defineCollection({
schema: z.object({
name: z.string(),
slug: z.string(),
}),
});
// 블로그 컬렉션에서 카테고리 참조
const blogCollection = defineCollection({
schema: z.object({
title: z.string(),
category: z.reference('category'), // category 컬렉션 참조
}),
});
export const collections = {
'category': categoryCollection,
'blog': blogCollection,
};
이제 블로그 글의 frontmatter에서 category 필드에 카테고리 파일 이름(확장자 제외)만 작성하면 됩니다.
---
title: "내 블로그"
category: "tech" # src/content/category/tech.md 참조
---
Astro가 해당 카테고리의 존재 여부를 자동으로 검증하며 타입 안전성도 보장합니다.
복잡한 객체 중첩
frontmatter 구조가 복잡하다면 객체를 중첩할 수 있습니다.
schema: z.object({
title: z.string(),
author: z.object({
name: z.string(),
email: z.string().email(), // 이메일 형식 검증
avatar: z.string().url(), // URL 형식 검증
}),
seo: z.object({
keywords: z.array(z.string()),
description: z.string().max(160), // 최대 길이 제한
}).optional(),
})
이에 대응하는 frontmatter는 다음과 같습니다.
---
title: "글 제목"
author:
name: "홍길동"
email: "[email protected]"
avatar: "https://example.com/avatar.jpg"
seo:
keywords: ["Astro", "튜토리얼"]
description: "Astro에 관한 튜토리얼입니다"
---
타입 안전성의 마법: TypeScript 자동 추론
Schema 구성을 마치면 Astro가 TypeScript 타입을 자동으로 생성합니다. 코드에서 데이터를 조회할 때 편집기가 완전한 타입 힌트를 제공합니다.
import { getCollection } from 'astro:content';
const posts = await getCollection('blog');
posts.forEach(post => {
// 편집기가 post.data 아래의 모든 필드를 제안
console.log(post.data.title); // ✅ 타입: string
console.log(post.data.pubDate); // ✅ 타입: Date
console.log(post.data.tags); // ✅ 타입: string[] | undefined
console.log(post.data.notExist); // ❌ 컴파일 오류: 존재하지 않는 필드
});
이것이 Content Collections의 가장 편리한 점입니다. 타입 정의를 직접 작성할 필요 없이 Astro가 Schema를 바탕으로 완전히 정확한 타입을 자동 생성합니다.
getEntry()와 getCollection() 비교
마지막으로 조회 API의 차이를 알아보겠습니다.
getCollection('blog'): 전체 컬렉션의 모든 콘텐츠를 가져옵니다.getEntry('blog', 'my-post'): 지정한 slug의 단일 콘텐츠를 가져옵니다.
단일 콘텐츠 조회가 더 효율적이므로 상세 페이지에 적합합니다.
---
// 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>
<Content />
</article>
솔직히 처음 Zod 문법을 봤을 때는 저도 조금 막막했습니다. 하지만 몇 번 사용하면 금방 익숙해지고, Zod의 오류 메시지도 명확해서 문제가 생겨도 쉽게 해결할 수 있습니다.
자주 발생하는 문제와 해결 방법
Content Collections를 구성하다 보면 여러 오류를 마주칠 수 있습니다. 이 절에서는 제가 직접 겪었던 문제를 중심으로 가장 흔한 오류와 해결 방법을 정리했습니다.
오류 1: MarkdownContentSchemaValidationError
가장 흔한 오류로, frontmatter가 Schema 정의와 일치하지 않는다는 뜻입니다. 오류 메시지는 대략 다음과 같습니다.
blog → my-post.md frontmatter does not match collection schema.
- "title" is required
- "pubDate" must be a valid date
이 오류는 어떻게 해석하나요?
Astro는 문제가 발생한 파일(my-post.md)과 필드(title, pubDate)를 명확히 알려 줍니다.
주요 원인과 해결 방법:
-
필드 누락: Schema에는 필수로 정의했지만 frontmatter에 필드가 없습니다.
- 해결: 누락된 필드를 추가하거나 Schema에서
.optional()을 지정합니다.
- 해결: 누락된 필드를 추가하거나 Schema에서
-
필드 이름 오타: 예를 들어
pubDate를publishDate로 작성했습니다.- 해결: 필드 이름을 통일하고 편집기의 자동 완성 기능을 활용합니다.
-
타입 불일치: 예를 들어 Schema는
z.number()를 요구하지만 frontmatter에는 문자열을 작성했습니다.- 해결: 필드 값의 형식을 확인합니다.
오류 2: InvalidContentEntryFrontmatterError
이 오류는 frontmatter 형식 자체에 문제가 있어(YAML 문법 오류) 파싱조차 할 수 없다는 뜻입니다.
흔한 원인은 다음과 같습니다.
---
title: "내 제목
description: "닫는 따옴표를 빠뜨렸습니다"
---
해결 방법: YAML 문법, 특히 따옴표, 콜론, 들여쓰기를 확인합니다. YAML 문법 검사를 지원하는 편집기 플러그인을 사용하는 것이 좋습니다.
오류 3: 날짜 형식 문제
저도 여러 번 겪었던 문제입니다. z.coerce.date() 대신 z.date()를 사용하면 Astro는 frontmatter의 날짜가 문자열이 아니라 Date 객체이기를 요구합니다. 하지만 YAML에는 문자열만 작성할 수 있습니다.
해결 방법: Schema에서 z.coerce.date()를 사용하면 문자열을 Date 객체로 자동 변환합니다.
// ❌ 잘못된 방법: Date 객체를 요구하지만 frontmatter에는 문자열이 있음
pubDate: z.date()
// ✅ 올바른 방법: 문자열을 Date로 자동 변환
pubDate: z.coerce.date()
기존 데이터 처리: .passthrough()
블로그에 예전 글이 많다면 frontmatter 필드가 통일되어 있지 않을 수 있습니다. 이때 .passthrough()를 사용해 검증을 일시적으로 완화할 수 있습니다.
schema: z.object({
title: z.string(),
// ... 기타 필드
}).passthrough() // 정의되지 않은 추가 필드 허용
하지만 이는 임시방편일 뿐입니다. 장기적으로는 frontmatter 구조를 통일하는 것이 좋습니다.
여러 컬렉션 구성 방법
사이트에 블로그, 문서, 사례 등 여러 종류의 콘텐츠가 있다면 여러 컬렉션을 만들 수 있습니다.
src/content/
├── blog/
├── docs/
└── case-studies/
그런 다음 content.config.ts에서 각각 정의합니다.
const blogCollection = defineCollection({ /* ... */ });
const docsCollection = defineCollection({ /* ... */ });
const caseStudiesCollection = defineCollection({ /* ... */ });
export const collections = {
'blog': blogCollection,
'docs': docsCollection,
'case-studies': caseStudiesCollection,
};
각 컬렉션은 서로 영향을 주지 않는 별도의 Schema를 사용할 수 있습니다.
Schema 설계 모범 사례
제 경험을 정리하면 다음과 같습니다.
- 필수 필드를 최소화합니다: 정말 필요한 필드만 필수로 두고 나머지는
.optional()또는.default()를 사용합니다. - 날짜에는
z.coerce.date()를 사용합니다: 수동 변환의 번거로움을 줄일 수 있습니다. - 필드 이름은 카멜 케이스를 사용합니다:
pub_date보다pubDate가 JavaScript 관례에 더 잘 맞습니다. - 복잡한 객체는 분리합니다: frontmatter가 너무 복잡하다면 여러 컬렉션으로 나누고
z.reference()로 연결하는 방법을 고려합니다. - 명확한 주석을 작성합니다: Schema에 각 필드의 용도를 설명하는 주석을 추가해 팀원이 이해할 수 있게 합니다.
점검 목록(문제 해결용)
오류가 발생하면 다음 순서로 확인합니다.
-
src/content/디렉터리가 있나요? -
src/content.config.ts파일의 위치가 올바른가요?(content/안이 아님) - Schema에서 내보낸
collections객체의 키 이름과 디렉터리 이름이 같은가요? - Frontmatter의 YAML 문법이 올바른가요?(따옴표, 콜론, 들여쓰기)
- 필수 필드를 모두 입력했나요?
- 필드 타입이 Schema 정의와 일치하나요?
솔직히 이런 문제는 보기에는 복잡하지만 Astro의 오류 메시지가 이미 꽤 친절합니다. 오류 정보를 꼼꼼히 읽으면 대부분 빠르게 원인을 찾을 수 있습니다.
결론
지금까지 살펴본 내용을 처음의 세 가지 문제와 연결해 보겠습니다.
Content Collections가 무엇인지 모르겠나요? 이제 Markdown 콘텐츠에 TypeScript 타입 검사를 추가하는 기능이라는 점을 이해했을 것입니다. 페이지가 깨진 뒤가 아니라 빌드 시점에 Astro가 오류를 발견하게 해 줍니다.
설정 파일은 어떻게 작성하나요? 세 단계를 기억하세요. src/content/ 디렉터리를 만들고, src/content.config.ts 파일을 만든 다음, defineCollection()과 Zod로 Schema를 정의합니다. 키 이름은 디렉터리 이름과 같아야 합니다.
Schema 검증은 어떻게 사용하나요? 기본 타입(z.string(), z.coerce.date(), z.array())을 익히고 .optional()과 .default() 사용법을 알아 두세요. 오류가 발생하면 Astro의 오류 메시지를 확인하면 됩니다.
솔직히 Content Collections는 Astro에서 가장 쓸 만한 기능 중 하나라고 생각합니다. 초기에 구성 시간을 조금 더 들이면 나중에 버그를 찾느라 쓰는 시간을 크게 줄일 수 있습니다. 편집기의 자동 완성도 정말 편리해서 코딩 경험이 몇 단계 좋아집니다.
다음 단계
지금 바로 Content Collections를 사용해 보고 싶다면 다음과 같이 진행해 보세요.
- 새 프로젝트에는 처음부터 적용하기: Astro 프로젝트를 만들 때 바로 Content Collections를 구성해 처음부터 규칙을 세웁니다.
- 기존 프로젝트는 점진적으로 마이그레이션하기: 먼저
.passthrough()로 기존 콘텐츠가 작동하게 만든 뒤 frontmatter 구조를 차근차근 통일합니다. - 공식 문서 참고하기: 문제가 생기면 전체 API 레퍼런스가 있는 Astro 공식 문서를 확인합니다.
Content Collections는 어렵지 않지만 직접 연습해야 익숙해집니다. 튜토리얼을 많이 읽는 것보다 설정 파일을 직접 한 번 작성하는 편이 낫습니다. 한번 시도해 보세요. 타입 안전성이 주는 편안함을 좋아하게 될 것입니다.
Astro Content Collections 전체 구성 절차
Content Collections를 처음부터 구성하고 Schema 검증까지 적용해 타입 안전한 콘텐츠 관리 시스템을 구현하는 전체 절차
⏱️ Estimated time: 30 min
- 1
Step 1: 디렉터리 구조 만들기
프로젝트 루트에 src/content/ 디렉터리를 만듭니다.
• Astro 예약 디렉터리(v2.0부터)
• content/ 아래에 하위 디렉터리를 만들어 컬렉션으로 사용합니다(예: blog/, docs/).
• 각 하위 디렉터리가 하나의 컬렉션입니다.
주의: 설정 파일 src/content.config.ts는 content/ 디렉터리 안이 아니라 src/ 디렉터리 아래에 둡니다. - 2
Step 2: 설정 파일 만들기
src/content.config.ts 파일을 만듭니다.
1. 의존성을 가져옵니다.
import { defineCollection, z } from 'astro:content'
2. 컬렉션 설정을 정의합니다.
const blogCollection = defineCollection({
type: 'content',
schema: z.object({
title: z.string(),
description: z.string(),
pubDate: z.coerce.date(),
tags: z.array(z.string()).optional(),
draft: z.boolean().default(false)
})
})
3. collections 객체를 내보냅니다.
export const collections = { 'blog': blogCollection }
주의: 키 이름은 디렉터리 이름과 같아야 합니다. - 3
Step 3: 콘텐츠 파일 만들기
src/content/blog/ 아래에 Markdown 파일을 만듭니다.
frontmatter는 반드시 Schema 정의를 충족해야 합니다.
• title(필수 문자열)
• description(필수 문자열)
• pubDate(예: "2024-12-01" 형식의 날짜이며 z.coerce.date()가 자동 변환)
• tags(선택적 문자열 배열)
• draft(선택적 불리언, 기본값 false)
필드가 Schema와 일치하지 않으면 Astro가 빌드 시점에 바로 오류를 표시합니다. - 4
Step 4: 페이지에서 데이터 조회하기
Astro 파일에서 getCollection을 가져옵니다.
import { getCollection } from 'astro:content'
모든 블로그 글을 가져옵니다.
const allPosts = await getCollection('blog')
초안을 제외합니다.
const publishedPosts = allPosts.filter(post => !post.data.draft)
데이터를 사용할 때:
• post.data에는 완전한 TypeScript 타입 힌트가 제공됩니다.
• 편집기가 title, description, pubDate 등의 필드를 자동으로 제안합니다.
• 단일 글 조회에는 getEntry('blog', slug)가 더 효율적입니다. - 5
Step 5: 고급 Schema 구성
이미지 검증:
schema: ({ image }) => z.object({
cover: image()
})
다른 컬렉션 참조:
z.reference('category')
복잡한 객체 중첩:
z.object({
author: z.object({
name: z.string(),
email: z.string().email(),
avatar: z.string().url()
})
})
기존 데이터 처리:
.passthrough()를 사용하면 정의되지 않은 추가 필드를 허용할 수 있습니다.
여러 컬렉션을 사용하는 경우:
collections 객체에 blog, docs, case-studies 등의 컬렉션을 각각 정의합니다. - 6
Step 6: 자주 발생하는 오류 해결하기
MarkdownContentSchemaValidationError: 누락된 필드(필드를 추가하거나 .optional() 지정), 필드 이름 오타(이름 통일), 타입 불일치(필드 값 형식 확인)를 점검합니다. InvalidContentEntryFrontmatterError: YAML 문법(따옴표, 콜론, 들여쓰기)을 확인합니다. 날짜 형식 문제: z.date() 대신 z.coerce.date()를 사용합니다. 점검 목록: content/ 디렉터리 존재 여부, content.config.ts 위치, collections 키와 디렉터리 이름의 일치 여부, YAML 문법, 필수 필드 입력 여부, 필드 타입과 Schema의 일치 여부를 확인합니다.
FAQ
Content Collections란 무엇이며 왜 필요한가요?
기존 방식(src/pages/ 아래에 blog/ 폴더를 직접 생성)은 타입 안전성 보호가 없어 다음과 같은 문제가 생기기 쉽습니다.
• 필드 이름 오타(tags를 tag로 작성)
• 잘못된 날짜 형식(2024-12-01 대신 12/01/2024)
• 새 필드를 추가한 뒤 일부 글에 넣는 것을 잊는 문제
• Astro가 미리 알려 주지 않아 런타임에 페이지가 깨진 뒤에야 알게 되는 문제
Content Collections가 제공하는 기능:
1) Schema 검증(frontmatter 필드의 타입과 구조를 정의하고 불일치 시 바로 오류 표시)
2) 자동 타입 생성(Schema를 바탕으로 TypeScript 타입을 생성해 편집기 자동 완성 제공)
3) 통합 조회 API(getCollection() 등의 메서드가 타입 안전한 데이터 반환)
4) 성능 최적화(Astro 5.0의 Content Layer API로 더 빠른 조회)
기존 방식이 '자유롭지만 안전하지 않다'면 Content Collections는 '제약은 있지만 신뢰할 수 있다'고 할 수 있습니다. 한 번 구성하면 프로젝트 전체에서 이점을 누릴 수 있습니다.
Content Collections는 어떻게 구성하나요? 전체 절차는 무엇인가요?
1) 디렉터리 만들기:
• 프로젝트 루트에 src/content/ 디렉터리를 만듭니다(Astro v2.0부터 예약 디렉터리).
• content/ 아래에 blog/, docs/ 같은 하위 디렉터리를 컬렉션으로 만듭니다. 각 하위 디렉터리가 하나의 컬렉션입니다.
2) 설정 파일 작성:
• src/content.config.ts 파일을 만듭니다(content/ 디렉터리 안이 아님에 주의).
• defineCollection과 z를 가져옵니다.
• 컬렉션 설정을 정의합니다(type: 'content'는 Markdown/MDX 파일, schema는 Zod로 frontmatter 구조 정의).
• collections 객체를 내보냅니다(예: 'blog': blogCollection처럼 키 이름이 디렉터리 이름과 같아야 함).
3) 콘텐츠 파일 만들기:
• src/content/blog/ 아래에 Markdown 파일을 만듭니다.
• frontmatter는 Schema 정의를 충족해야 하며, 일치하지 않으면 Astro가 빌드 시점에 바로 오류를 표시합니다.
Schema 검증은 어떻게 사용하며 자주 쓰는 타입은 무엇인가요?
• z.string() 문자열
• z.number() 숫자
• z.boolean() 불리언
• z.date() Date 객체
• z.coerce.date() 문자열을 Date로 자동 변환(YAML에는 문자열만 쓸 수 있어 특히 유용)
• z.array(z.string()) 문자열 배열
• z.enum(['draft', 'published']) 열거형
선택적 필드와 기본값:
• .optional()은 필드를 선택 사항으로 만듭니다.
• .default(false)는 기본값을 설정합니다.
고급 사용법:
• image() 이미지 경로 검증: schema: ({ image }) => z.object({ cover: image() })
• z.reference('category')로 다른 컬렉션 참조
• 복잡한 객체 중첩: z.object({ author: z.object({ name: z.string(), email: z.string().email() }) })
Schema 구성을 마치면 Astro가 TypeScript 타입을 자동으로 생성합니다. 편집기에서 완전한 타입 힌트를 제공하며 post.data 아래의 모든 필드가 타입 안전성을 갖습니다.
Content Collections 데이터는 어떻게 조회하나요? getCollection과 getEntry의 차이는 무엇인가요?
• getCollection('blog')은 전체 컬렉션의 모든 콘텐츠를 배열로 반환합니다.
• getEntry('blog', 'my-post')는 지정한 slug의 단일 콘텐츠를 객체로 반환합니다(상세 페이지에 더 효율적).
Astro 파일에서 가져오기:
import { getCollection, getEntry } from 'astro:content'
데이터 사용:
• post.data는 frontmatter 데이터이며 완전한 TypeScript 타입 힌트가 있습니다.
• 편집기가 title, description, pubDate 등의 필드를 자동으로 제안합니다.
초안 제외:
const publishedPosts = allPosts.filter(post => !post.data.draft)
단일 글 조회 예제:
const post = await getEntry('blog', slug)
if (!post) return Astro.redirect('/404')
const { Content } = await post.render()
Content Collections에서 자주 발생하는 오류와 해결 방법은 무엇인가요?
1) MarkdownContentSchemaValidationError(frontmatter가 Schema와 일치하지 않음):
• 필드 누락 여부를 확인하고 누락 필드를 추가하거나 .optional()을 지정합니다.
• 필드 이름 오타를 확인하고 편집기 자동 완성을 이용해 이름을 통일합니다.
• 타입이 일치하지 않으면 필드 값 형식을 확인합니다.
2) InvalidContentEntryFrontmatterError(YAML 문법 오류):
• 따옴표, 콜론, 들여쓰기를 확인합니다.
• YAML 문법 검사를 지원하는 편집기 플러그인을 권장합니다.
3) 날짜 형식 문제:
• z.date() 대신 z.coerce.date()를 사용합니다(YAML에는 문자열만 쓸 수 있으며 z.coerce.date()가 자동 변환).
4) 기존 데이터 처리:
• .passthrough()로 검증을 일시적으로 완화해 정의되지 않은 추가 필드를 허용할 수 있지만 임시방편일 뿐입니다.
• 장기적으로는 frontmatter 구조를 통일하는 것이 좋습니다.
점검 목록:
• content/ 디렉터리가 존재하는지
• content.config.ts 위치가 올바른지(content/ 안이 아님)
• collections 키와 디렉터리 이름이 같은지
• YAML 문법이 올바른지
• 필수 필드를 모두 입력했는지
• 필드 타입이 Schema와 일치하는지
여러 컬렉션은 어떻게 구성하며 Schema 설계 모범 사례는 무엇인가요?
• 사이트에 블로그, 문서, 사례 등 여러 종류의 콘텐츠가 있다면 여러 컬렉션(src/content/blog/, docs/, case-studies/)을 만듭니다.
• content.config.ts에서 각각 정의합니다.
const blogCollection = defineCollection({...})
const docsCollection = defineCollection({...})
• collections 객체를 내보냅니다.
'blog': blogCollection,
'docs': docsCollection,
'case-studies': caseStudiesCollection
• 컬렉션마다 서로 영향을 주지 않는 별도의 Schema를 사용할 수 있습니다.
Schema 설계 모범 사례:
1) 필수 필드는 최소화합니다(정말 필요한 것만 필수로 두고 나머지는 .optional() 또는 .default() 사용).
2) 날짜에는 z.coerce.date()를 사용합니다(수동 변환 불필요).
3) 필드 이름은 카멜 케이스로 작성합니다(pub_date보다 pubDate가 JavaScript 관례에 적합).
4) 복잡한 객체는 분리합니다(frontmatter가 너무 복잡하면 여러 컬렉션으로 나누고 z.reference()로 연결).
5) 명확한 주석을 작성합니다(Schema에 각 필드의 용도를 설명하는 주석 추가).
4분 읽기 · 게시일: 2025년 11월 24일 · 수정일: 2026년 9월 4일
Astro 가이드
검색으로 들어왔다면 같은 시리즈의 이전 글이나 다음 글로 이동하는 것이 가장 빠릅니다.
이전
Astro 블로그 처음부터 만들기: 1시간 만에 홈페이지부터 배포까지 완성하는 가이드
Astro로 개인 블로그를 만드는 전 과정을 단계별로 설명합니다. 환경 준비부터 홈페이지, 글 목록, 태그 분류, RSS 구독, SEO 최적화, 배포까지 초보자도 1시간 안에 프로덕션 수준의 블로그를 공개할 수 있습니다.
15편 중 2편
다음
Astro Markdown 고급 가이드: 블로그를 10배 더 전문적으로 만드는 7가지 실전 팁
Astro Markdown/MDX의 고급 기능을 완전히 익히는 가이드입니다. 코드 하이라이트 테마 설정, 사용자 정의 컴포넌트 삽입, 수학 공식과 순서도 표시 방법을 자세한 설정 단계와 실전 코드 예제, 자주 발생하는 문제의 해결 방법과 함께 설명합니다.
15편 중 4편



댓글
GitHub로 로그인하여 댓글을 남기세요