Files
2nd/10_Wiki/Topics/Coding/Frontend_Astro_Patterns.md
T
2026-05-09 22:47:42 +09:00

9.3 KiB

id, title, category, status, source_trust_level, verification_status, created_at, updated_at, tags, tech_stack, applied_in, aliases
id title category status source_trust_level verification_status created_at updated_at tags tech_stack applied_in aliases
frontend-astro-patterns Astro — Islands / Static-first / Multi-framework Coding draft B conceptual 2026-05-09 2026-05-09
frontend
astro
ssg
vibe-coding
language applicable_to
TS / Astro
Frontend
Astro
islands architecture
static-first
content-driven
multi-framework

Astro

Static-first + 작은 JS. Islands architecture. React / Vue / Svelte / Solid 동시 사용 가능. Content-heavy site (blog, marketing) 의 sweet spot.

📖 핵심 개념

  • Static (default): 0 JS shipped.
  • Island: 인터랙션 component 만 hydrate.
  • Multi-framework: React + Vue + Svelte 한 site.
  • Content collection: type-safe MDX.

💻 코드 패턴

시작

npm create astro@latest

기본 page

---
// Server-side (build time 또는 SSR)
const users = await fetch('https://api.example.com/users').then(r => r.json());
---

<html>
  <head>
    <title>Users</title>
  </head>
  <body>
    <h1>Users</h1>
    <ul>
      {users.map(u => <li>{u.email}</li>)}
    </ul>
  </body>
</html>

→ Static HTML 가 generate. 0 JS.

Island (인터랙션)

---
import { Counter } from '../components/Counter.tsx';
---

<div>
  <h1>Static heading</h1>
  <Counter client:load />  <!-- island — JS shipped -->
  <p>More static</p>
</div>
// components/Counter.tsx (React)
import { useState } from 'react';

export function Counter() {
  const [count, setCount] = useState(0);
  return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}

client:* directives

<Component client:load />        <!-- 즉시 hydrate -->
<Component client:idle />        <!-- requestIdleCallback -->
<Component client:visible />     <!-- IntersectionObserver -->
<Component client:media="(min-width: 768px)" />  <!-- 조건부 -->
<Component client:only="react" />  <!-- SSR 안 — client only -->

→ 작은 island 만 JS load.

Multi-framework

---
import ReactCounter from './ReactCounter.tsx';
import VueCounter from './VueCounter.vue';
import SvelteCounter from './SvelteCounter.svelte';
---

<ReactCounter client:visible />
<VueCounter client:visible />
<SvelteCounter client:visible />

→ 같은 page 안 다른 framework. Migration 또는 team별.

Content collections (type-safe MDX)

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

const blog = defineCollection({
  schema: z.object({
    title: z.string(),
    date: z.date(),
    tags: z.array(z.string()),
    draft: z.boolean().default(false),
  }),
});

export const collections = { blog };
---
title: My First Post
date: 2026-05-09
tags: [intro]
---

# Hello World

This is my first **blog post**.

import Counter from '../components/Counter.tsx';

<Counter client:load />
---
import { getCollection } from 'astro:content';

const posts = await getCollection('blog', ({ data }) => !data.draft);
posts.sort((a, b) => b.data.date.getTime() - a.data.date.getTime());
---

<ul>
  {posts.map(post => (
    <li><a href={`/blog/${post.slug}`}>{post.data.title}</a></li>
  ))}
</ul>

→ Type-safe content + frontmatter.

Dynamic route

---
// src/pages/blog/[slug].astro
import { getCollection, getEntry } from 'astro:content';

export async function getStaticPaths() {
  const posts = await getCollection('blog');
  return posts.map(post => ({
    params: { slug: post.slug },
    props: { post },
  }));
}

const { post } = Astro.props;
const { Content } = await post.render();
---

<article>
  <h1>{post.data.title}</h1>
  <Content />
</article>

→ Static generation 모든 post.

SSR mode

// astro.config.mjs
import { defineConfig } from 'astro/config';
import vercel from '@astrojs/vercel/serverless';

export default defineConfig({
  output: 'server',  // 또는 'hybrid'
  adapter: vercel(),
});

→ Static (default) 또는 SSR per route.

API routes

// src/pages/api/users.ts
import type { APIRoute } from 'astro';

export const GET: APIRoute = async ({ request }) => {
  const users = await db.user.findMany();
  return new Response(JSON.stringify(users), {
    headers: { 'Content-Type': 'application/json' },
  });
};

export const POST: APIRoute = async ({ request }) => {
  const data = await request.json();
  // ...
};

View Transitions (built-in)

---
import { ViewTransitions } from 'astro:transitions';
---

<html>
  <head>
    <ViewTransitions />
  </head>
</html>
<!-- 자동 page transition -->
<a href="/about">About</a>

<!-- Shared element -->
<img transition:name="hero" src="..." />

Image optimization

---
import { Image } from 'astro:assets';
import heroImg from '../assets/hero.jpg';
---

<Image src={heroImg} alt="Hero" width={1200} height={600} format="avif" />

→ Build 시 다양 size + format 자동 generate.

Tailwind / styling

npx astro add tailwind
<div class="grid gap-4 md:grid-cols-3">
  <article class="rounded border p-4">...</article>
</div>

Markdown / MDX rendering

---
title: ...
---

# Heading

import Chart from '../components/Chart.tsx';

<Chart client:visible data={[1, 2, 3]} />

Code:

```ts
function hello() { return 'world'; }

→ Content + interactive component.

### Pagination
```ts
// src/pages/blog/[page].astro
export async function getStaticPaths({ paginate }) {
  const posts = await getCollection('blog');
  return paginate(posts, { pageSize: 10 });
}

const { page } = Astro.props;
<ul>
  {page.data.map(post => <li>...</li>)}
</ul>

{page.url.prev && <a href={page.url.prev}>Prev</a>}
{page.url.next && <a href={page.url.next}>Next</a>}

RSS feed

// src/pages/rss.xml.ts
import rss from '@astrojs/rss';
import { getCollection } from 'astro:content';

export async function GET(context) {
  const posts = await getCollection('blog');
  return rss({
    title: 'My Blog',
    description: '...',
    site: context.site,
    items: posts.map(post => ({
      title: post.data.title,
      pubDate: post.data.date,
      link: `/blog/${post.slug}`,
    })),
  });
}

Performance

Static page (no island): 0 JS.
Marketing site: 95+ Lighthouse.
Blog: 100/100 가능.

→ 작은 JS = 빠른 load.

vs Next.js

Astro:
+ Static-first
+ 0 JS default
+ Multi-framework
+ Content-driven
- Less interactive (heavy SPA 어려움)

Next:
+ App Router (RSC)
+ 큰 ecosystem
+ Vercel optimization
- More JS (SPA-friendly)
- Single framework (React)

→ Marketing / blog / docs = Astro. App = Next.

vs SvelteKit / Nuxt

Astro: framework-agnostic, content-first.
SvelteKit: Svelte SPA + SSR.
Nuxt: Vue + meta-framework.

Use cases

✅ Blog / personal site
✅ Marketing site
✅ Documentation
✅ Landing page
✅ E-commerce (catalog)
✅ Portfolio

⚠️ Heavy interactive app (SPA 가 낫음)

Deploy

- Vercel / Netlify (Static + SSR)
- Cloudflare Pages
- GitHub Pages (static only)
- 자체 server (Node)

CMS 통합

- Sanity / Contentful / Strapi
- Markdoc
- Decap CMS (git-based)
- Astro DB (built-in)
// Sanity
import { sanityClient } from 'sanity:client';

const posts = await sanityClient.fetch(`*[_type == "post"]`);

Astro DB

// db/config.ts
import { defineDb, defineTable, column } from 'astro:db';

const Comment = defineTable({
  columns: {
    id: column.number({ primaryKey: true }),
    body: column.text(),
    postSlug: column.text(),
    createdAt: column.date({ default: NOW }),
  },
});

export default defineDb({ tables: { Comment } });

→ libSQL 기반. 빠른 시작.

i18n

// astro.config.mjs
i18n: {
  defaultLocale: 'en',
  locales: ['en', 'ko', 'ja'],
  routing: { prefixDefaultLocale: false },
},
src/pages/
├── index.astro
├── ko/index.astro
└── ja/index.astro

Streaming SSR

Astro 4+ 가 streaming.
Suspense-like — 일부 부분 점진 send.

Test

yarn add -D vitest @vitest/ui
yarn vitest
import { describe, it, expect } from 'vitest';
import { experimental_AstroContainer } from 'astro/container';
import Card from './Card.astro';

it('renders title', async () => {
  const container = await experimental_AstroContainer.create();
  const result = await container.renderToString(Card, { props: { title: 'Hello' } });
  expect(result).toContain('Hello');
});

🤔 의사결정 기준

상황 추천
Blog / docs / marketing Astro
Content-first Astro + content collection
일부 interactive Astro + island
Heavy SPA Next / Tanstack Start
Multi-framework migration Astro
Static export only Astro / Hugo / 11ty

안티패턴

  • 모든 게 client:load: JS bundle 폭발.
  • Big SPA in Astro: 잘못 선택. Next / Remix.
  • content schema 무: type 안전 X.
  • Image plain <img>: optimization 없음. Use <Image>.
  • Build 매 변경 (큰 site): incremental build 필요.
  • SSR 모든 page: 정적 generation 가 더 빠름.

🤖 LLM 활용 힌트

  • Static + island = 빠른 site.
  • Content collection 으로 type-safe.
  • View Transitions built-in.
  • Multi-framework 가 migration 친화.

🔗 관련 문서