English
Youngkwang Yang

기록을 위한 개인 기술 블로그

목차

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

Astro

원본 cover.png가 astro:assets(Sharp)를 거쳐 480·800·1200 너비 webp로 출력되는 파이프라인 다이어그램

블로그에서 이미지는 늘 골칫거리다. 원본을 그대로 올리면 무겁고, 손으로 리사이즈하면 관리가 안 된다. 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:assetsImage 컴포넌트에 그대로 넘긴다. widthssizes만 알려주면 반응형 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로 나뉜다. widthheight가 자동으로 붙어서 레이아웃이 밀리는 현상(CLS)도 막아 준다.

정리하면, 이미지 관리를 런타임이 아니라 빌드 타임으로 옮기는 게 핵심이다. 글쓴이는 파일을 옆에 두기만 하고, 나머지는 파이프라인이 처리한다.