[G1-Sync] Manual knowledge update
This commit is contained in:
@@ -0,0 +1,476 @@
|
||||
---
|
||||
id: frontend-astro-patterns
|
||||
title: Astro — Islands / Static-first / Multi-framework
|
||||
category: Coding
|
||||
status: draft
|
||||
source_trust_level: B
|
||||
verification_status: conceptual
|
||||
created_at: 2026-05-09
|
||||
updated_at: 2026-05-09
|
||||
tags: [frontend, astro, ssg, vibe-coding]
|
||||
tech_stack: { language: "TS / Astro", applicable_to: ["Frontend"] }
|
||||
applied_in: []
|
||||
aliases: [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.
|
||||
|
||||
## 💻 코드 패턴
|
||||
|
||||
### 시작
|
||||
```bash
|
||||
npm create astro@latest
|
||||
```
|
||||
|
||||
### 기본 page
|
||||
```astro
|
||||
---
|
||||
// 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 (인터랙션)
|
||||
```astro
|
||||
---
|
||||
import { Counter } from '../components/Counter.tsx';
|
||||
---
|
||||
|
||||
<div>
|
||||
<h1>Static heading</h1>
|
||||
<Counter client:load /> <!-- island — JS shipped -->
|
||||
<p>More static</p>
|
||||
</div>
|
||||
```
|
||||
|
||||
```tsx
|
||||
// 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
|
||||
```astro
|
||||
<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
|
||||
```astro
|
||||
---
|
||||
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)
|
||||
```ts
|
||||
// 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 };
|
||||
```
|
||||
|
||||
```mdx
|
||||
---
|
||||
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 />
|
||||
```
|
||||
|
||||
```astro
|
||||
---
|
||||
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
|
||||
```astro
|
||||
---
|
||||
// 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
|
||||
```ts
|
||||
// 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
|
||||
```ts
|
||||
// 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)
|
||||
```astro
|
||||
---
|
||||
import { ViewTransitions } from 'astro:transitions';
|
||||
---
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<ViewTransitions />
|
||||
</head>
|
||||
</html>
|
||||
```
|
||||
|
||||
```astro
|
||||
<!-- 자동 page transition -->
|
||||
<a href="/about">About</a>
|
||||
|
||||
<!-- Shared element -->
|
||||
<img transition:name="hero" src="..." />
|
||||
```
|
||||
|
||||
### Image optimization
|
||||
```astro
|
||||
---
|
||||
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
|
||||
```bash
|
||||
npx astro add tailwind
|
||||
```
|
||||
|
||||
```astro
|
||||
<div class="grid gap-4 md:grid-cols-3">
|
||||
<article class="rounded border p-4">...</article>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Markdown / MDX rendering
|
||||
```mdx
|
||||
---
|
||||
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;
|
||||
```
|
||||
|
||||
```astro
|
||||
<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
|
||||
```ts
|
||||
// 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)
|
||||
```
|
||||
|
||||
```ts
|
||||
// Sanity
|
||||
import { sanityClient } from 'sanity:client';
|
||||
|
||||
const posts = await sanityClient.fetch(`*[_type == "post"]`);
|
||||
```
|
||||
|
||||
### Astro DB
|
||||
```ts
|
||||
// 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
|
||||
```ts
|
||||
// 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
|
||||
```bash
|
||||
yarn add -D vitest @vitest/ui
|
||||
yarn vitest
|
||||
```
|
||||
|
||||
```ts
|
||||
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 친화.
|
||||
|
||||
## 🔗 관련 문서
|
||||
- [[Frontend_Progressive_Enhancement]]
|
||||
- [[Frontend_View_Transitions_Deep]]
|
||||
- [[React_Server_Components]]
|
||||
Reference in New Issue
Block a user