[Astro] Content Collections와 마크다운 마이그레이션

2026-03-26 hit count image

Astro의 Content Collections를 이용하여 블로그 콘텐츠를 관리하는 방법과, Jekyll에서 마크다운 파일을 마이그레이션하는 과정을 공유합니다.

astro

개요

이전 글 Astro 설치 및 프로젝트 구성에서 프로젝트의 기본 구성을 완료했습니다. 이번 글에서는 Astro의 핵심 기능 중 하나인 Content Collections를 이용하여 블로그 콘텐츠를 관리하는 방법과, Jekyll에서 마크다운 파일을 마이그레이션하는 과정을 설명합니다.

Content Collections란

Content CollectionsAstro에서 마크다운, MDX 등의 콘텐츠 파일을 구조화하여 관리하는 기능입니다. 주요 특징은 다음과 같습니다.

  • 타입 안전성: Zod 스키마로 frontmatter를 검증하여 빌드 타임에 오류를 감지
  • 자동 타입 생성: TypeScript 타입이 자동으로 생성되어 IDE에서 자동 완성 지원
  • 유연한 로더: glob 패턴으로 파일을 유연하게 로드
  • 빌트인 API: getCollection(), getEntry() 등의 API로 콘텐츠를 쉽게 쿼리

스키마 정의

Content Collections의 스키마는 src/content.config.ts 파일에서 정의합니다. 이 블로그에서 사용하는 스키마는 다음과 같습니다.

// src/content.config.ts
import { defineCollection, z } from 'astro:content';
import { glob } from 'astro/loaders';

const blog = defineCollection({
  loader: glob({ pattern: '**/*.md', base: './src/content' }),
  schema: z.object({
    title: z.string(),
    description: z.string(),
    lang: z.enum(['ja', 'ko', 'en']),
    category: z.string(),
    permalink: z.string(),
    image: z.string().optional(),
    comments: z.boolean().default(true),
    date: z.coerce.date(),
    published: z.boolean().default(true),
  }),
});

export const collections = { blog };

스키마 필드 설명

스키마의 각 필드는 다음과 같은 역할을 합니다.

필드타입필수설명
titlestringO포스트 제목
descriptionstringOSEO용 설명 (160자 이내 권장)
langenumO언어 코드 (ja, ko, en)
categorystringO카테고리 slug (예: astro, react)
permalinkstringOURL 경로 (예: /astro/installation/)
imagestring-대표 이미지 경로
commentsboolean-댓글 표시 여부 (기본값: true)
datedateO작성일
publishedboolean-공개 여부 (기본값: true)

주요 포인트

glob 로더

loader: glob({ pattern: '**/*.md', base: './src/content' }),

src/content/ 아래의 모든 .md 파일을 재귀적으로 로드합니다. 카테고리별 하위 디렉토리 구조를 자유롭게 구성할 수 있습니다.

z.coerce.date()

date: z.coerce.date(),

z.date() 대신 z.coerce.date()를 사용합니다. 이렇게 하면 frontmatter에서 문자열로 작성된 날짜('2026-04-15')를 자동으로 Date 객체로 변환해줍니다. Jekyll에서 마이그레이션할 때 날짜 형식을 변경하지 않아도 되므로 편리합니다.

z.enum으로 언어 제한

lang: z.enum(['ja', 'ko', 'en']),

lang 필드를 enum으로 제한하여 지원하지 않는 언어 코드가 입력되면 빌드 타임에 에러가 발생합니다.

디렉토리 구조와 네이밍 규칙

디렉토리 구조

포스트 파일은 src/content/ 아래에 카테고리별 디렉토리로 구분합니다.

src/content/
├── astro/
│   ├── 2026-04-01-migration-reason/
│   │   ├── index-ja.md
│   │   ├── index-ko.md
│   │   └── index-en.md
│   └── 2026-04-08-installation/
│       ├── index-ja.md
│       ├── index-ko.md
│       └── index-en.md
├── react/
│   └── 2026-03-03-react-19-migration/
│       ├── index-ja.md
│       ├── index-ko.md
│       └── index-en.md
├── jekyll/
│   └── ...
└── ...

네이밍 규칙

디렉토리와 파일의 네이밍 규칙은 다음과 같습니다.

  • 디렉토리명: YYYY-MM-DD-slug/ 형식으로 날짜와 슬러그를 포함
  • 파일명: index-{lang}.md 형식으로 언어별 파일을 구분
    • index-ja.md — 일본어
    • index-ko.md — 한국어
    • index-en.md — 영어

이 구조의 장점은 하나의 포스트에 대한 다국어 파일이 같은 디렉토리에 모여있어 관리가 편하다는 것입니다.

Jekyll에서 Frontmatter 마이그레이션

Jekyll의 Frontmatter

Jekyll에서 사용하던 frontmatter 형식입니다.

---
layout: post
title: jekyll 설치
description: jekyll 블로그를 시작하기 위해, Mac/Windows에 jekyll을 설치하고 기본 프로젝트를 생성하자.
image: /assets/images/category/jekyll/install.jpg
categories: jekyll
date: 2018-09-08
---

Astro의 Frontmatter

Astro로 마이그레이션한 frontmatter 형식입니다.

---
title: jekyll 설치
description: jekyll 블로그를 시작하기 위해, Mac/Windows에 jekyll을 설치하고 기본 프로젝트를 생성하자.
lang: ko
category: jekyll
permalink: /jekyll/installation/
date: '2018-09-08'
published: false
comments: true
image: /assets/images/category/jekyll/install.jpg
---

주요 변경 사항

Jekyll의 frontmatter와 Astro의 frontmatter를 비교하면 다음과 같은 차이점이 있습니다.

항목JekyllAstro
레이아웃layout: post불필요 (페이지에서 레이아웃 지정)
카테고리categories: jekyllcategory: jekyll (단수형)
언어플러그인으로 관리lang: ko (명시적)
URL자동 생성permalink: /jekyll/installation/ (명시적)
날짜2018-09-08'2018-09-08' (문자열 권장)

Jekyll에서는 layout 필드로 사용할 레이아웃을 지정했지만, Astro에서는 페이지 컴포넌트([...path].astro)에서 레이아웃을 import하여 사용하므로 frontmatter에 layout 필드가 필요 없습니다.

카테고리 시스템

블로그의 카테고리는 src/data/categories.ts에서 중앙 집중적으로 관리합니다.

// src/data/categories.ts
export type CategoryGroup =
  | 'frontend'
  | 'backend'
  | 'development'
  | 'service'
  | 'etc';

export interface Category {
  slug: string;
  group: CategoryGroup;
  image: string;
  isMainCategory: boolean;
  permalink: string;
  singlePage: boolean;
  searchJson: string;
  translations: Record<string, CategoryTranslation>;
}

각 카테고리는 다음과 같은 정보를 포함합니다.

  • slug: URL과 콘텐츠 디렉토리에서 사용되는 식별자
  • group: 프론트엔드, 백엔드, 개발, 서비스 등의 분류
  • translations: 일본어/한국어/영어 3개 언어의 제목, 설명
{
  slug: 'astro',
  group: 'frontend',
  image: '/assets/images/category/astro/background.jpg',
  isMainCategory: true,
  permalink: '/astro/',
  singlePage: false,
  searchJson: 'astro',
  translations: {
    ja: {
      title: 'Astro',
      description: 'JekyllからAstroへのマイグレーション記録です...',
      seeMore: 'もっと見る',
      noSearchResult: '検索結果がありません。',
    },
    ko: {
      title: 'Astro',
      description: 'Jekyll에서 Astro로의 마이그레이션 기록입니다...',
      seeMore: '자세히 보기',
      noSearchResult: '검색 결과가 없습니다.',
    },
    en: {
      title: 'Astro',
      description: 'A record of migrating from Jekyll to Astro...',
      seeMore: 'see more',
      noSearchResult: 'There is no search result.',
    },
  },
},

콘텐츠 쿼리

Content Collections에 저장된 콘텐츠는 getCollection() API로 쿼리할 수 있습니다.

기본 사용법

getCollection() API는 다음과 같이 사용할 수 있습니다.

import { getCollection } from 'astro:content';

// 모든 블로그 포스트 가져오기
const allPosts = await getCollection('blog');

// 한국어 + 공개된 포스트만 필터링
const koPosts = await getCollection('blog', ({ data }) => {
  return data.lang === 'ko' && data.published !== false;
});

카테고리별 필터링

getCollection()을 사용하여 다음과 같이 특정 카테고리의 포스트만 가져올 수도 있습니다.

// 특정 카테고리의 포스트 가져오기
const astroPosts = await getCollection('blog', ({ data }) => {
  return (
    data.lang === 'ko' && data.category === 'astro' && data.published !== false
  );
});

// 날짜 내림차순 정렬
const sortedPosts = astroPosts.sort(
  (a, b) => new Date(b.data.date).getTime() - new Date(a.data.date).getTime()
);

페이지에서 사용

getCollection()은 다음과 같이 동적 라우팅 페이지([...path].astro)에서 getStaticPaths와 함께 사용합니다.

// src/pages/ko/[...path].astro
export async function getStaticPaths() {
  const posts = await getCollection('blog', ({ data }) => {
    return data.lang === 'ko' && data.published !== false;
  });

  return posts.map((post) => ({
    params: { path: post.data.permalink.slice(1, -1) },
    props: { post },
  }));
}

permalink 필드의 앞뒤 /를 제거하여 params.path로 사용하는 것이 포인트입니다. 이렇게 하면 /ko/astro/installation/ 같은 URL로 접근할 수 있습니다.

완료

이번 글에서는 AstroContent Collections를 이용하여 블로그 콘텐츠를 관리하는 방법을 살펴보았습니다.

  • src/content.config.ts에서 Zod 스키마로 frontmatter를 타입 안전하게 관리
  • YYYY-MM-DD-slug/index-{lang}.md 네이밍으로 다국어 포스트 구조화
  • Jekyll frontmatter에서 Astro frontmatter로의 변환 포인트
  • categories.ts에서 카테고리를 중앙 집중 관리
  • getCollection() API로 콘텐츠 쿼리 및 필터링

다음 글 다국어(i18n) 구현에서는 일본어/한국어/영어 3개 언어를 지원하는 다국어 시스템 구현 방법을 다룹니다.

시리즈 안내

이 포스트는 Jekyll에서 Astro로 마이그레이션 시리즈의 일부입니다.

  1. Jekyll에서 Astro로 마이그레이션한 이유
  2. Astro 설치 및 프로젝트 구성
  3. Content Collections와 마크다운 마이그레이션
  4. 다국어(i18n) 구현
  5. SEO 구현
  6. 이미지 최적화 — 커스텀 rehype 플러그인
  7. 댓글 시스템 (Utterances)
  8. 광고 연동 (Google AdSense)
  9. Pagefind를 이용한 검색 구현
  10. 레이아웃과 컴포넌트 아키텍처
  11. GitHub Pages 배포
  12. 소셜 공유 자동화 스크립트
  13. 트러블슈팅과 팁

제 블로그가 도움이 되셨나요? 하단의 댓글을 달아주시면 저에게 큰 힘이 됩니다!

앱 홍보

책 홍보

블로그를 운영하면서 좋은 기회가 생겨 책을 출판하게 되었습니다.

아래 링크를 통해 제가 쓴 책을 구매하실 수 있습니다.
많은 분들에게 도움이 되면 좋겠네요.



SHARE
Twitter Facebook RSS