콘텐츠 컬렉션으로 이미지까지 타입 세이프하게

블로그에서 이미지는 늘 골칫거리다. 원본을 그대로 올리면 무겁고, 손으로 리사이즈하면 관리가 안 된다. Astro의 콘텐츠 컬렉션은 이 문제를 빌드 단계로 넘긴다. 글과 이미지를 같은 폴더에 두고, 스키마에 이미지 필드만 선언하면 된다.
폴더 구조
이 블로그는 글 하나를 폴더 하나로 관리한다. 이미지는 글 옆에 둔다.
src/content/posts/
└── content-collections-images/
├── index.md ← 지금 이 글
└── cover.png ← 옆에 둔 이미지
스키마에 image() 선언하기
컬렉션 설정에서 image() 헬퍼를 쓰면, 프론트매터의 상대 경로가 그냥 문자열이 아니라
최적화 대상 이미지로 처리된다.
// src/content.config.ts
import { defineCollection, z } from 'astro:content';
import { glob } from 'astro/loaders';
const posts = defineCollection({
loader: glob({ pattern: '**/index.{md,mdx}', base: './src/content/posts' }),
schema: ({ image }) =>
z.object({
title: z.string(),
description: z.string(),
pubDate: z.coerce.date(),
heroImage: image().optional(),
heroAlt: z.string().optional(),
}),
});
export const collections = { posts };
경로가 틀리거나 파일이 없으면 빌드가 실패한다. 깨진 이미지 링크를 배포 후에야 발견하는 일이 없어진다.
렌더링
글 페이지에서는 astro:assets의 Image 컴포넌트에 그대로 넘긴다. widths와 sizes만
알려주면 반응형 srcset을 만들어 준다.
---
import { Image } from 'astro:assets';
const { heroImage, heroAlt } = post.data;
---
<Image
src={heroImage}
alt={heroAlt}
widths={[480, 800, 1200]}
sizes="(min-width: 40rem) 40rem, 100vw"
/>
위에 보이는 표지 이미지가 바로 이렇게 렌더된 것이다. 원본은 PNG 한 장이지만, 빌드
결과물은 화면 크기에 맞춰 세 가지 너비의 webp로 나뉜다. width와 height가 자동으로
붙어서 레이아웃이 밀리는 현상(CLS)도 막아 준다.
정리하면, 이미지 관리를 런타임이 아니라 빌드 타임으로 옮기는 게 핵심이다. 글쓴이는 파일을 옆에 두기만 하고, 나머지는 파이프라인이 처리한다.