Use Cases (업데이트: 2026. 6. 2.)

Claude Code로 Astro 콘텐츠 사이트 만들기: Collections, SSG, 검증 흐름

Claude Code로 Astro 콘텐츠 사이트를 만들 때 필요한 설정, 목록, 태그, hydration, 빌드 검증을 정리합니다.

Claude Code로 Astro 콘텐츠 사이트 만들기: Collections, SSG, 검증 흐름

Claude Code와 Astro가 잘 맞는 이유

Astro는 블로그, 문서 사이트, 포트폴리오, 제품 소개 페이지처럼 읽는 콘텐츠가 많은 사이트에 잘 맞는 프레임워크입니다. React나 Vue 컴포넌트를 사용할 수 있지만, 모든 페이지를 거대한 클라이언트 앱으로 만들지는 않습니다. island architecture는 정적인 영역은 그대로 정적으로 두고, 검색창이나 필터처럼 상호작용이 필요한 부분만 브라우저에서 JavaScript를 로드하는 방식입니다.

Claude Code를 Astro 개발에 쓰는 핵심은 파일을 많이 생성하게 하는 것이 아니라, 프로젝트 구조를 먼저 읽게 하는 것입니다. Astro 사이트는 astro.config.mjs, src/content.config.ts, src/pages/, src/components/, 이미지, 라우팅, 빌드 명령이 함께 맞아야 동작합니다. 먼저 계획을 설명하게 하면 불필요한 CMS 도입이나 과한 리팩터링을 줄일 수 있습니다.

이 글은 Astro 5 계열의 현재 Content Collections 방식을 기준으로 합니다. 작업 전에는 Claude Code Quickstart, Astro Content Collections, Astro template directives, Astro routing reference를 확인하세요. 정적 생성과 서버 렌더링의 선택은 SSR/SSG 비교, 검색 유입은 SEO 최적화와 함께 보면 좋습니다.

프로젝트 범위를 먼저 고정하기

처음부터 “Astro 블로그를 만들어줘”라고 하면 범위가 너무 넓습니다. Claude Code가 디자인, 라우팅, 콘텐츠 모델, 의존성을 한꺼번에 건드릴 수 있습니다. 대신 정적 콘텐츠 사이트, MDX, sitemap, 타입이 있는 frontmatter, 글 목록, 태그 페이지, 빌드 검증처럼 범위를 명확히 적습니다.

npm create astro@latest my-astro-site
cd my-astro-site
npx astro add mdx sitemap tailwind
npm run dev

프로젝트를 만든 뒤에는 바로 수정시키지 말고 먼저 읽게 합니다.

claude "이 Astro 프로젝트를 콘텐츠 사이트로 검토해. astro.config.mjs, src/content.config.ts, src/pages, src/components를 읽고, 편집하기 전에 구현 계획과 검증 명령을 설명해."

기본 설정은 작게 시작합니다.

// astro.config.mjs
import { defineConfig } from 'astro/config';
import mdx from '@astrojs/mdx';
import sitemap from '@astrojs/sitemap';
import tailwind from '@astrojs/tailwind';

export default defineConfig({
  site: 'https://example.com',
  output: 'static',
  integrations: [mdx(), sitemap(), tailwind()],
  markdown: {
    shikiConfig: {
      theme: 'github-dark',
    },
  },
});

site 값은 배포 전에 실제 도메인으로 바꿔야 합니다. sitemap과 canonical URL이 이 값에 의존하므로 예시 도메인을 남기면 SEO 검증에서 문제가 됩니다.

현재 방식으로 Content Collections 정의하기

Content Collections는 Markdown과 MDX 글을 타입이 있는 데이터로 다루게 해 줍니다. 제목, 설명, 날짜, 태그 같은 frontmatter를 schema로 검증하므로, 글이 많아질수록 효과가 커집니다. 예전 글에는 src/content/config.ts 예제가 많지만, Astro 5 기준으로는 src/content.config.ts, astro/loaders, astro/zod를 사용하는 방식이 공식 문서의 기본 흐름입니다.

// src/content.config.ts
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().max(80),
    description: z.string().max(120),
    pubDate: z.coerce.date(),
    updatedDate: z.coerce.date().optional(),
    tags: z.array(z.string()).default([]),
    draft: z.boolean().default(false),
    heroImage: z.string().optional(),
  }),
});

export const collections = { blog };

Claude Code에는 이렇게 리뷰를 맡깁니다.

claude "src/content.config.ts가 Astro 5의 Content Collections 방식에 맞는지 검토해. description 길이, draft 제외, updatedDate 선택 필드, 오래된 API 혼용 여부를 확인해."

이 schema는 게시 계약처럼 동작합니다. description이 너무 길거나, 날짜가 잘못됐거나, tags가 배열이 아니면 배포 전에 잡을 수 있습니다. 다국어 사이트에서는 번역자가 본문을 바꾸다가 frontmatter를 망가뜨리는 실수가 흔하므로 더 중요합니다.

예시 1: 글 목록과 페이지네이션

첫 번째 실전 예시는 글 목록입니다. getCollection()의 반환 순서를 그대로 믿지 말고 직접 정렬해야 합니다. 초안은 제외하고, 게시일 기준으로 내림차순 정렬한 뒤 카드 컴포넌트로 넘깁니다.

---
// src/pages/blog/[...page].astro
import { getCollection } from 'astro:content';
import PostCard from '../../components/PostCard.astro';

export async function getStaticPaths({ paginate }) {
  const posts = (await getCollection('blog', ({ data }) => data.draft !== true))
    .sort((a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf());

  return paginate(posts, { pageSize: 12 });
}

const { page } = Astro.props;
---

<section class="mx-auto max-w-5xl px-4 py-10">
  <h1 class="text-3xl font-bold">Blog</h1>
  <div class="mt-8 grid gap-6 md:grid-cols-2">
    {page.data.map((post) => <PostCard post={post} />)}
  </div>
  <nav class="mt-10 flex justify-between">
    {page.url.prev ? <a href={page.url.prev}>Previous</a> : <span />}
    {page.url.next ? <a href={page.url.next}>Next</a> : <span />}
  </nav>
</section>

카드 컴포넌트는 날짜, 제목, 설명, 태그만 안정적으로 보여주면 됩니다.

---
// src/components/PostCard.astro
import type { CollectionEntry } from 'astro:content';

type Props = {
  post: CollectionEntry<'blog'>;
  lang?: string;
};

const { post, lang = 'ko' } = Astro.props;
const href = lang === 'ja' ? `/blog/${post.id}/` : `/${lang}/blog/${post.id}/`;
const date = post.data.updatedDate ?? post.data.pubDate;
---

<article class="rounded border border-slate-200 p-5">
  <p class="text-sm text-slate-500">
    <time datetime={date.toISOString()}>{date.toLocaleDateString('ko-KR')}</time>
  </p>
  <h2 class="mt-2 text-xl font-semibold">
    <a href={href}>{post.data.title}</a>
  </h2>
  <p class="mt-3 text-slate-700">{post.data.description}</p>
  <ul class="mt-4 flex flex-wrap gap-2">
    {post.data.tags.map((tag) => <li class="rounded bg-slate-100 px-2 py-1 text-xs">{tag}</li>)}
  </ul>
</article>

이 구조는 개인 기술 블로그, 사내 지식 베이스, 제품 변경 로그에 모두 사용할 수 있습니다. Claude Code에는 스타일을 바꾸기 전에 날짜, 제목, 설명, 태그를 보존하라고 명시하는 편이 안전합니다.

예시 2: 태그 페이지와 필요한 부분만 hydration

두 번째 예시는 태그별 목록입니다. 태그 이름을 URL에 그대로 넣으면 공백, 대문자, 한글 태그에서 문제가 생길 수 있습니다. URL용 slug와 표시용 label을 분리합니다.

---
// src/pages/tags/[tag]/[...page].astro
import { getCollection } from 'astro:content';
import PostCard from '../../../components/PostCard.astro';

const tagSlug = (tag) =>
  encodeURIComponent(tag.toLowerCase().trim().replace(/\s+/g, '-'));

export async function getStaticPaths({ paginate }) {
  const posts = await getCollection('blog', ({ data }) => data.draft !== true);
  const groups = new Map();

  for (const post of posts) {
    for (const label of post.data.tags) {
      const slug = tagSlug(label);
      const group = groups.get(slug) ?? { slug, label, posts: [] };
      group.posts.push(post);
      groups.set(slug, group);
    }
  }

  return [...groups.values()].flatMap((group) =>
    paginate(group.posts, {
      params: { tag: group.slug },
      props: { tag: group.label },
      pageSize: 10,
    }),
  );
}

const { page, tag } = Astro.props;
---

<section class="mx-auto max-w-5xl px-4 py-10">
  <h1 class="text-3xl font-bold">Tag: {tag}</h1>
  <div class="mt-8 grid gap-6 md:grid-cols-2">
    {page.data.map((post) => <PostCard post={post} />)}
  </div>
</section>

세 번째 예시는 검색 UI입니다. 검색창 하나 때문에 전체 페이지를 클라이언트 앱으로 만들 필요는 없습니다.

---
// src/pages/index.astro
import Hero from '../components/Hero.astro';
import SearchBox from '../components/SearchBox.tsx';
import LatestPosts from '../components/LatestPosts.astro';
---

<Hero />
<SearchBox client:visible />
<LatestPosts />

client:load는 첫 화면에서 즉시 조작해야 하는 컴포넌트에, client:visible은 화면에 들어온 뒤 로드해도 되는 컴포넌트에 어울립니다. Claude Code가 directive를 추가할 때는 이유를 먼저 설명하게 하세요.

흔한 실패와 검증

첫 번째 실패는 오래된 튜토리얼을 그대로 복사하는 것입니다. 두 번째 실패는 “전체를 개선해”처럼 범위를 크게 주는 것입니다. 세 번째 실패는 heroImage, description, OGP를 나중으로 미루는 것입니다. 네 번째 실패는 빌드 성공만 보고 공개하는 것입니다. 페이지네이션, 내부 링크, 외부 링크, 모바일 표시, CTA까지 확인해야 합니다.

검증은 최소한 다음 순서로 실행합니다.

npm run build
npx astro check
npm run preview

빌드가 실패하면 Claude Code에 원인 분류부터 요청합니다.

claude "npm run build 실패 로그를 읽어. Astro 문법, Content Collections schema, MDX code fence, route 생성, 링크 문제로 분류하고 가장 작은 수정안을 제안해."

CTA와 실제로 확인한 결과

처음 시작하는 독자는 무료 치트시트로 기본 명령을 익히고, 템플릿이 필요한 독자는 상품 목록을 볼 수 있습니다. 팀에서 Astro 콘텐츠 운영과 Claude Code 리뷰 흐름을 같이 만들고 싶다면 교육 및 도입 상담이 자연스러운 다음 단계입니다.

실제로 적용해 보니 Claude Code는 .astro 파일 생성과 연결 작업은 빠르게 처리했습니다. 다만 현재 Astro API인지, 태그 URL이 안전하게 인코딩되는지, client:load를 과하게 붙이지 않았는지, 빌드 검증을 실행했는지는 사람이 봐야 했습니다. 안정적인 순서는 프로젝트를 읽게 하기, 범위를 좁히기, 공식 문서 기준을 확인하기, build와 preview로 확인하기였습니다.

#Claude Code #Astro #framework #SSG #frontend
무료

무료 PDF: Claude Code 치트시트

이메일을 입력하면 명령, 리뷰 습관, 안전한 워크플로를 정리한 PDF를 받을 수 있습니다.

개인정보를 안전하게 관리하며 스팸을 보내지 않습니다.

Masa

작성자 소개

Masa

Claude Code 실무 워크플로와 팀 도입을 검증하는 엔지니어입니다.