Communitygithub.com

0xDarkMatter/claude-mods - Astro 운영 모드

Astro 프레임워크 패턴, 아일랜드 아키텍처, 콘텐츠 컬렉션, 렌더링 전략 및 배포에 대한 종합 가이드입니다. Astro, 아일랜드 아키텍처, 콘텐츠 컬렉션, Cloudflare, 뷰 트랜지션, 부분 수화, SSG, SSR, 하이브리드 렌더링, Astro 어댑터 등을 다룹니다.

claude-mods - Astro 운영 모드란 무엇인가요?

이 스킬은 Astro 프레임워크 개발을 위한 포괄적인 패턴과 모범 사례를 제공합니다. 아일랜드 아키텍처, 콘텐츠 컬렉션, 다양한 렌더링 전략(SSG, SSR, 하이브리드), 뷰 트랜지션, 그리고 다중 플랫폼 배포(Cloudflare, Vercel, Netlify, Node.js)를 다룹니다. 사용자는 의사 결정 트리를 통해 정적 사이트, 서버 렌더링, 또는 하이브리드 접근 방식 중 가장 적합한 렌더링 전략을 선택할 수 있습니다. 또한 `client:*` 지시문을 사용한 점진적 수화, 콘텐츠 컬렉션의 Zod 스키마 정의 및 쿼리, 뷰 트랜지션을 통한 부드러운 페이지 전환, 그리고 각 배포 플랫폼에 맞는 어댑터 설치 및 구성 방법을 상세히 설명합니다. 일반적인 문제점과 해결책을 함께 제공하여 초보자도 쉽게 따라할 수 있습니다.

지원 대상✓Claude Code~Codex CLI~Cursor
npx skills add https://github.com/0xDarkMatter/claude-mods/tree/HEAD/skills/astro-ops

즐겨 사용하는 AI에게 물어보기

이 에이전트 스킬이 미리 로드된 새 채팅을 엽니다.

문서

Astro 운영 모드 (Astro Operations)

2026년 7월 기준 확인된 사실.

Astro 프레임워크 개발을 위한 종합 패턴: 아일랜드 아키텍처, 콘텐츠 컬렉션, 렌더링 전략, 뷰 트랜지션, 다중 플랫폼 배포.

렌더링 전략 결정 트리

어떤 렌더링 전략을 사용할까요?
│
├─ 콘텐츠가 대부분 정적인가요? (블로그, 문서, 마케팅)
│  ├─ 예 → 하루에 한 번 미만으로 변경되나요?
│  │  ├─ 예 → SSG (output: 'static')
│  │  │        가장 빠른 TTFB, CDN 캐시 가능, 런타임 비용 없음
│  │  └─ 아니요 → 하이브리드 (output: 'hybrid')
│  │           기본 정적 + 경로별 선택적 SSR
│  └─ 아니요 → 모든 페이지에 개인화가 필요한가요?
│     ├─ 예 → SSR (output: 'server')
│     │        요청별 동적, 인증 인식, 실시간 데이터
│     └─ 아니요 → 하이브리드 (output: 'hybrid')
│              정적 셸 + 동적 부분을 위한 서버 아일랜드
│
├─ 앱에 실시간 상호작용이 필요한가요? (대시보드, SPA)
│  ├─ 예 → 클라이언트 측 라우팅이 있는 완전한 SPA인가요?
│  │  ├─ 예 → React/Vue SPA 또는 Astro + client:only 고려
│  │  └─ 아니요 → 하이브리드 + 아일랜드 아키텍처
│  │           정적 페이지의 대화형 아일랜드
│  └─ 아니요 → SSG (output: 'static')
│
├─ 빌드 시간이 우려되나요? (1만 페이지 이상)
│  ├─ 예 → 주문형 렌더링이 있는 하이브리드
│  │        인기 페이지는 사전 렌더링, 긴 꼬리는 SSR
│  └─ 아니요 → SSG로 충분히 처리 가능
│
└─ 엣지 컴퓨팅이 필요한가요? (전역적으로 낮은 지연 시간)
   ├─ 예 → SSR + Cloudflare/Vercel Edge 어댑터
   └─ 아니요 → SSR + Node 어댑터 또는 SSG

설정

// astro.config.mjs
import { defineConfig } from 'astro/config';

// SSG (기본값) - 모든 페이지가 빌드 시 사전 렌더링됨
export default defineConfig({
  output: 'static',
});

// SSR - 모든 페이지가 요청 시 렌더링됨
export default defineConfig({
  output: 'server',
  adapter: cloudflare(), // 또는 vercel(), netlify(), node()
});

// 하이브리드 - 기본 정적, 페이지별 선택적 SSR
export default defineConfig({
  output: 'hybrid',
  adapter: cloudflare(),
});
---
// 하이브리드 모드에서 특정 페이지의 사전 렌더링을 해제하려면:
export const prerender = false;
// SSR 모드에서 사전 렌더링을 활성화하려면:
export const prerender = true;
---

아일랜드 아키텍처 빠른 참조

지시문수화 시점JS 전달사용 사례
client:load페이지 로드 시 즉시전체 번들접힌 부분 위 상호작용 (네비게이션, 히어로 CTA)
client:idle페이지가 유휴 상태일 때 (requestIdleCallback)전체 번들접힌 부분 아래 상호작용 (댓글 폼, 채팅)
client:visible뷰포트로 스크롤될 때전체 번들페이지 아래쪽 (푸터 위젯, 캐러셀)
client:media미디어 쿼리 일치 시전체 번들모바일 전용 네비게이션, 반응형 컴포넌트
client:only="react"즉시, SSR 완전 건너뜀전체 번들SSR 불가능한 컴포넌트 (canvas, WebGL)
(없음)절대 안 함 - 정적 HTML만JS 0정적 콘텐츠, 카드, 헤더
---
import NavBar from '../components/NavBar.tsx';
import CommentForm from '../components/CommentForm.tsx';
import ImageCarousel from '../components/ImageCarousel.svelte';
import MobileMenu from '../components/MobileMenu.vue';
import ThreeScene from '../components/ThreeScene.tsx';
---

<!-- 즉시 로드 - 중요한 상호작용 -->
<NavBar client:load />

<!-- 페이지가 유휴 상태일 때 로드 - 중요하지 않음 -->
<CommentForm client:idle />

<!-- 뷰포트로 스크롤될 때 로드 - 지연 로딩 -->
<ImageCarousel client:visible />

<!-- 모바일에서만 로드 -->
<MobileMenu client:media="(max-width: 768px)" />

<!-- 클라이언트 전용, SSR 없음 (WebGL은 서버에서 실행 불가) -->
<ThreeScene client:only="react" />

콘텐츠 컬렉션 빠른 시작

스키마 정의

// src/content.config.ts (Astro 5) 또는 src/content/config.ts (Astro 4)
import { defineCollection, z, reference } from 'astro:content';
import { glob } from 'astro/loaders';

const blog = defineCollection({
  loader: glob({ pattern: '**/*.{md,mdx}', base: './src/content/blog' }),
  schema: z.object({
    title: z.string(),
    description: z.string().max(160),
    pubDate: z.coerce.date(),
    updatedDate: z.coerce.date().optional(),
    heroImage: z.string().optional(),
    tags: z.array(z.string()).default([]),
    draft: z.boolean().default(false),
    author: reference('authors'), // 다른 컬렉션 참조
  }),
});

const authors = defineCollection({
  loader: glob({ pattern: '**/*.json', base: './src/content/authors' }),
  schema: z.object({
    name: z.string(),
    avatar: z.string(),
    bio: z.string(),
    socials: z.object({
      twitter: z.string().optional(),
      github: z.string().optional(),
    }).optional(),
  }),
});

export const collections = { blog, authors };

컬렉션 쿼리

---
import { getCollection, getEntry } from 'astro:content';

// 모든 초안이 아닌 블로그 게시물을 날짜순으로 정렬하여 가져오기
const posts = (await getCollection('blog', ({ data }) => !data.draft))
  .sort((a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf());

// 단일 항목 가져오기
const post = await getEntry('blog', 'my-first-post');

// 참조 확인
const author = await getEntry(post.data.author);

// 콘텐츠 렌더링
const { Content, headings } = await post.render();
---

<Content />

콘텐츠 컬렉션 vs 외부 CMS

기준콘텐츠 컬렉션외부 CMS (Payload 등)
콘텐츠 유형로컬 마크다운/MDX, 문서, 블로그관계형 데이터 모델
작성자개발자 (버전 관리)편집자 (관리 UI, 다중 사용자 인증)
유효성 검사빌드 시 Zod를 통한 타입 안전성CMS 측 스키마 + API 계약
업데이트 주기사이트와 함께 배포됨배포와 독립적
API 필요없음 (빌드 타임 쿼리)다른 소비자를 위한 REST/GraphQL
워크플로우Git PR, 간단한 검토편집 워크플로우, 초안, 역할

경험 법칙: 콘텐츠 컬렉션으로 시작하세요. 비개발자가 배포 없이 게시해야 하거나 콘텐츠가 진정으로 관계형일 때만 CMS를 고려하세요.

프로젝트 구조 참조

project-root/
├── astro.config.mjs          # Astro 설정
├── tsconfig.json              # TypeScript 설정 (astro/tsconfigs 확장)
├── package.json
├── public/                    # 정적 자산 (있는 그대로 복사됨)
│   ├── favicon.svg
│   ├── robots.txt
│   └── og-image.png
├── src/
│   ├── pages/                 # 파일 기반 라우팅
│   │   ├── index.astro        # → /
│   │   ├── about.astro        # → /about
│   │   ├── blog/
│   │   │   ├── index.astro    # → /blog
│   │   │   └── [slug].astro   # → /blog/:slug (동적)
│   │   ├── api/
│   │   │   └── search.ts      # → /api/search (API 엔드포인트)
│   │   └── [...slug].astro    # → 포괄/404
│   ├── layouts/
│   │   ├── BaseLayout.astro   # HTML 셸, <head>, 전역 스타일
│   │   └── BlogPost.astro     # 블로그 게시물 레이아웃
│   ├── components/
│   │   ├── Header.astro       # 정적 Astro 컴포넌트
│   │   ├── Footer.astro
│   │   ├── NavBar.tsx         # React 아일랜드
│   │   └── Counter.svelte     # Svelte 아일랜드
│   ├── content/               # 콘텐츠 컬렉션 소스 파일
│   │   ├── blog/
│   │   │   ├── post-one.md
│   │   │   └── post-two.mdx
│   │   └── authors/
│   │       └── jane.json
│   ├── content.config.ts      # 컬렉션 스키마 (Astro 5)
│   ├── middleware.ts           # 요청/응답 미들웨어
│   ├── styles/
│   │   └── global.css
│   └── lib/                   # 공유 유틸리티
│       ├── utils.ts
│       └── constants.ts
└── .env                       # 환경 변수

뷰 트랜지션 빠른 참조

---
// src/layouts/BaseLayout.astro
import { ViewTransitions } from 'astro:transitions';
---

<html>
  <head>
    <ViewTransitions />
  </head>
  <body>
    <slot />
  </body>
</html>

트랜지션 지시문

<!-- 페이지 간 요소 유지 (상태 유지, 재렌더링 방지) -->
<audio transition:persist id="player">
  <source src="/music.mp3" />
</audio>

<!-- 애니메이션 페어링을 위한 명명된 트랜지션 -->
<img transition:name="hero" src={post.heroImage} />

<!-- 사용자 정의 애니메이션 -->
<div transition:animate="slide">콘텐츠</div>
<div transition:animate="fade">콘텐츠</div>
<div transition:animate="none">애니메이션 없음</div>

<!-- 이름으로 유지 (여러 지속 요소의 경우) -->
<video transition:persist="media-player" />

수명 주기 이벤트

<script>
  document.addEventListener('astro:before-preparation', (e) => {
    // 새 페이지를 가져오기 전 - 탐색 취소, 로딩 표시
  });

  document.addEventListener('astro:after-preparation', (e) => {
    // 새 페이지를 가져온 후, 교체 전
  });

  document.addEventListener('astro:before-swap', (e) => {
    // DOM 교체 동작 사용자 정의
  });

  document.addEventListener('astro:after-swap', () => {
    // DOM 업데이트됨 - 스크립트 재초기화
  });

  document.addEventListener('astro:page-load', () => {
    // 페이지 완전히 로드됨 (초기 + 모든 탐색 시 실행)
    // 뷰 트랜지션에서는 DOMContentLoaded 대신 이것을 사용하세요.
  });
</script>

뒤로/앞으로 처리

// astro.config.mjs
export default defineConfig({
  prefetch: {
    prefetchAll: true,         // 호버 시 모든 링크 프리페치
    defaultStrategy: 'hover',  // 'hover' | 'tap' | 'viewport' | 'load'
  },
});
<!-- 링크별 프리페치 제어 -->
<a href="/about" data-astro-prefetch>호버 시 프리페치 (기본값)</a>
<a href="/blog" data-astro-prefetch="viewport">보일 때 프리페치</a>
<a href="/contact" data-astro-prefetch="load">즉시 프리페치</a>
<a href="/external" data-astro-prefetch="false">프리페치 안 함</a>

배포 결정 트리

어디에 배포할까요?
│
├─ 엣지 컴퓨팅 + Cloudflare 생태계 (KV, D1, R2)가 필요한가요?
│  └─ Cloudflare Pages/Workers
│     어댑터: @astrojs/cloudflare
│     최적 대상: 글로벌 엣지, Workers 바인딩, 비용 효율적
│
├─ 서버리스 + Vercel 생태계 (ISR, 분석)가 필요한가요?
│  └─ Vercel
│     어댑터: @astrojs/vercel
│     최적 대상: Next.js 마이그레이션, 이미지 최적화, ISR
│
├─ 서버리스 + Netlify 생태계 (폼, 신원)가 필요한가요?
│  └─ Netlify
│     어댑터: @astrojs/netlify
│     최적 대상: JAMstack, 내장 폼, 분할 테스트
│
├─ 전체 서버 제어 (Docker, 사용자 정의 런타임)가 필요한가요?
│  └─ Node.js (독립형 또는 Express/Fastify)
│     어댑터: @astrojs/node
│     최적 대상: 자체 호스팅, WebSocket, 장기 실행 프로세스
│
└─ 순수 정적 사이트 (SSR 불필요)?
   └─ 모든 정적 호스트 (GitHub Pages, S3, Cloudflare Pages)
      어댑터 불필요, output: 'static'
      최적 대상: 블로그, 문서, 마케팅 사이트

어댑터 설치

# Cloudflare
npx astro add cloudflare

# Vercel
npx astro add vercel

# Netlify
npx astro add netlify

# Node.js
npx astro add node

일반적인 문제점

문제점이유해결책
수화 불일치 오류서버 HTML이 클라이언트 렌더링과 다름 (날짜, 무작위 ID, 브라우저 API)브라우저 종속 컴포넌트에는 client:only를 사용하거나 결정적 렌더링 보장
import.meta.env가 클라이언트에서 정의되지 않음PUBLIC_ 접두사가 있는 변수만 클라이언트 측 코드에 노출됨PUBLIC_MY_VAR로 이름 변경하거나 서버에서 props로 전달
SSG에서 동적 라우트 404getStaticPaths()가 모든 가능한 매개변수를 반환하지 않음getStaticPaths()가 모든 유효한 경로를 반환하는지 확인하거나 하이브리드/SSR로 전환
이미지 최적화 안 됨Astro의 <Image /> 컴포넌트 대신 <img> 사용astro:assets에서 가져오기: import { Image } from 'astro:assets' 및 src에 로컬 가져오기 사용
어댑터 없이 SSR 실패output: 'server' 또는 'hybrid'에는 배포 어댑터 필요어댑터 설치: npx astro add cloudflare (또는 vercel, netlify, node)
MDX 컴포넌트가 렌더링되지 않음사용자 정의 컴포넌트가 MDX 콘텐츠에 전달되지 않음<Content components={{ MyComponent }} />를 통해 컴포넌트를 전달하거나 astro.config.mjs MDX 설정 사용
콘텐츠 컬렉션 스키마 변경 사항이 반영되지 않음유형 생성이 캐시되어 오래된 .astro 유형이 남음astro sync를 실행하여 유형을 재생성하고 개발 서버를 다시 시작
Astro 컴포넌트의 client:*클라이언트 지시문은 프레임워크 컴포넌트(React, Vue, Svelte)에서만 작동Astro 컴포넌트는 정적 전용입니다. 대화형 부분을 프레임워크 컴포넌트로 추출하세요.
document / window가 정의되지 않음서버 측 코드는 브라우저 전역 변수에 접근할 수 없음if (typeof window !== 'undefined')로 보호하거나 client:only로 이동
컴포넌트 간 스타일 누수전역 CSS 대신 범위 지정 스타일 사용<style> (.astro에서 기본적으로 범위 지정됨) 또는 의도적으로 <style is:global> 사용
뷰 트랜지션이 스크립트를 중단시킴DOMContentLoaded는 뷰 트랜지션에서 한 번만 실행됨대신 모든 탐색 시 실행되는 astro:page-load 이벤트 사용
프로덕션에서 환경 변수 누락.env가 로드되지 않았거나 플랫폼 환경 변수가 설정되지 않음유효성 검사를 위해 astro.config.mjs에서 envField를 사용하거나 플랫폼 대시보드에서 변수 설정

프로덕션 보안 체크리스트

모든 프로덕션 배포에서 다음을 처리하세요:

  • CSP 헤더 - 제한적인 Content-Security-Policy 구성 (references/deployment.md의 미들웨어 패턴 참조)
  • 원격 이미지 제한 - 명시적 image.domains / remotePatterns 허용 목록 적용; 사용자 입력에서 이미지 URL을 절대 파생하지 마세요 (SSRF 위험)
  • 호스트 헤더 유효성 검사 - 미들웨어에서 요청 호스트가 예상 도메인과 일치하는지 확인 (SSR/하이브리드 전용)
  • 비밀 관리 - Cloudflare에서는 Workers Bindings (wrangler secret put)를 사용하고, 코드에 포함된 환경 변수를 사용하지 마세요. 다른 곳에서는 플랫폼 비밀 저장소를 사용하세요.
  • HTTPS 전용 - 모든 외부 리소스(스크립트, 이미지, 폰트)가 HTTPS를 통해 로드되는지 확인
  • 입력 유효성 검사 - SSR 컨텍스트에서 모든 사용자 입력(쿼리 매개변수, 폼 본문, 쿠키)을 삭제

참조 파일

파일내용줄 수
references/content-collections.md스키마 패턴, Zod 유형, 쿼리, MDX, 콘텐츠 레이어 API, 마이그레이션~500
references/islands-rendering…

[잘림 - 번역 완료]

Individual skills in this repo

This repo contains 1 individual skill — each has its own dedicated page.

관련 스킬