Astro 5.x/4.x ์ต์ ์์ ๊ฐ์ด๋: ์์ผ๋๋ ์ํคํ
์ฒ(Islands), ์ ๋ก JS(Zero-JS) ๊ธฐ๋ณธ ๋ ๋๋ง, ํด๋ผ์ด์ธํธ ๋๋ ํฐ๋ธ(client:load/visible), ์ปดํฌ๋ํธ ํ๋ก ํธ๋งคํฐ ๋ฌธ๋ฒ, ํ์ผ ๊ธฐ๋ฐ ๋์ ๋ผ์ฐํ
(getStaticPaths), Content Collections(ํ์
์์ ์คํค๋ง), ViewTransitions(ClientRouter) ์ ๋๋ฉ์ด์
๋ฐ SSR ํ์ด๋ธ๋ฆฌ๋ ์ด๋ํฐ ๋ฐฐํฌ
## 1. ํ์ ์ฌ์ ์๊ฑด & ์์คํ
์๊ตฌ์ฌ์ (Prerequisites & Setup)
### ๐ป ์์คํ
์ค์น ๋ฐ ์คํ ์๊ตฌ์ฌ์ (System Requirements)
| ํญ๋ชฉ | ์ต์ ์ฌ์ (Minimum) | ๊ถ์ฅ ์ฌ์ (Recommended) | ์ฐธ๊ณ ๋ฐ ๋น๊ณ |
| :--- | :--- | :--- | :--- |
| **์ด์์ฒด์ (OS)** | Windows 10/11, macOS, Linux | macOS Sonoma, Ubuntu 22.04 LTS+, Windows 11 (WSL2) | Vite ๊ธฐ๋ฐ ๊ณ ์ฑ๋ฅ ES ๋ชจ๋ ๋น๋ ํ๊ฒฝ |
| **CPU** | 2 ์ฝ์ด ์ด์ | 4 ์ฝ์ด ~ 8 ์ฝ์ด ์ด์ | ๋๊ท๋ชจ ๋งํฌ๋ค์ด/MDX ํ์ฑ ๋ฐ ์ด๋ฏธ์ง ์ต์ ํ ํ์ดํ๋ผ์ธ |
| **๋ฉ๋ชจ๋ฆฌ (RAM)** | 2 GB ์ด์ | 4 GB ~ 8 GB ์ด์ | Node.js ๋ฐํ์ ๋ฐ ์คํ(Sharp) ์ด๋ฏธ์ง ๋ฆฌ์ฌ์ด์ง ์บ์ |
| **๋์คํฌ (Storage)** | 500 MB ์ด์์ ์ฌ์ ๊ณต๊ฐ | 5 GB ~ 20 GB ์ด์์ ๊ณ ์ NVMe SSD | `node_modules` ๋ฐ `.astro` ๋น๋ ์บ์ ๋ณด๊ด |
| **๋ฐํ์ / ํจํค์ง ๋งค๋์ ** | Node.js 18.17.1+ ๋๋ 20+ (LTS) | Node.js 22 LTS / pnpm 9+ / Bun 1.1+ | `pnpm` ๋๋ `bun` ์ฌ์ฉ ์ ํจํค์ง ์ค์น ์๋ 3~5๋ฐฐ ํฅ์ |
---
### ๐ ํ๋ก์ ํธ ์์ฑ & ๋๋ ํ ๋ฆฌ ๊ตฌ์กฐ
```bash
# [1] ๊ณต์ ๋ํํ CLI๋ก ์ต์ Astro ํ๋ก์ ํธ ์์ฑ
npm create astro@latest my-astro-site
# ๋๋ pnpm / bun ์ฌ์ฉ
pnpm create astro@latest my-astro-site
# [2] ํ๋ก์ ํธ ๋๋ ํ ๋ฆฌ ์ด๋ ๋ฐ ๊ฐ๋ฐ ์๋ฒ ์คํ
cd my-astro-site
npm install
npm run dev
# [3] ๊ณต์ ํตํฉ ํ๋ฌ๊ทธ์ธ 1์ด ์๋ ์ถ๊ฐ (React, Tailwind, Sitemap ๋ฑ)
npx astro add react
npx astro add tailwind
npx astro add sitemap
```
#### ๐ ํ์ค ํ๋ก์ ํธ ๊ตฌ์กฐ (Standard Architecture)
```text
my-astro-site/
โโโ public/ # ๋น๋ ์ ๊ทธ๋๋ก ๋ณต์ฌ๋๋ ์ ์ ์์
(favicon, robots.txt)
โโโ src/
โ โโโ components/ # ์ฌ์ฌ์ฉ ๊ฐ๋ฅํ UI ์ปดํฌ๋ํธ (.astro, .jsx, .vue)
โ โโโ layouts/ # ๊ณตํต ํ์ด์ง ๋ผ๋ ๋ ์ด์์ (BaseLayout.astro)
โ โโโ pages/ # ํ์ผ ๊ธฐ๋ฐ ๋ผ์ฐํ
๋๋ ํ ๋ฆฌ (HTML ๋ฐ API ์๋ํฌ์ธํธ)
โ โ โโโ index.astro # ๋ฃจํธ ํ์ด์ง (/)
โ โ โโโ about.astro # /about ํ์ด์ง
โ โ โโโ posts/
โ โ โโโ [id].astro # ๋์ ๋ผ์ฐํธ (/posts/1, /posts/2)
โ โโโ content/ # ํ์
์์ ๋งํฌ๋ค์ด/MDX ์ฝํ
์ธ ์ปฌ๋ ์
โ โ โโโ blog/
โ โโโ content.config.ts # Astro 5+ ์ฝํ
์ธ ์คํค๋ง ์ ์ ํ์ผ
โโโ astro.config.mjs # Astro ์ ์ญ ํ๊ฒฝ ์ค์ ํ์ผ (Vite, ์ด๋ํฐ, ํ๋ฌ๊ทธ์ธ)
โโโ package.json
โโโ tsconfig.json # strict ๋ชจ๋๊ฐ ๊ธฐ๋ณธ ํ์ฑํ๋ ํ์
์คํฌ๋ฆฝํธ ์ค์
```
---
## 2. Astro ํต์ฌ ๋ฉํ ๋ชจ๋ธ: ์์ผ๋๋ ์ํคํ
์ฒ (Islands Architecture)
Astro๋ **์ฝํ
์ธ ์ค์ฌ(Content-Driven)** ์น์ฌ์ดํธ๋ฅผ ์ํด ํ์ด๋ฌ์ผ๋ฉฐ, ๊ธฐ๋ณธ์ ์ผ๋ก ํด๋ผ์ด์ธํธ๋ก ์๋ฐ์คํฌ๋ฆฝํธ๋ฅผ ๋จ 1๋ฐ์ดํธ๋ ์ ์กํ์ง ์๋ **์ ๋ก JS(Zero JavaScript by Default)** ์ฒ ํ์ ๊ฐ์ง๋๋ค.
### ๐ SPA vs ์์ผ๋๋ ์ํคํ
์ฒ ๋น๊ต ๋ค์ด์ด๊ทธ๋จ (ASCII Diagram)
```text
[ ์ ํต์ ์ธ React / Vue SPA ]
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ ๊ฑฐ๋ํ ๋จ์ผ ์๋ฐ์คํฌ๋ฆฝํธ ๋ฒ๋ค (Heavy JS Bundle: 500KB+) โ
โ ์ ์ฒด ํ์ด์ง๊ฐ ์ํ(Hydration)๋๊ธฐ ์ ๊น์ง ์ฌ์ฉ์ ์ธํฐ๋์
์ฐจ๋จ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
[ Astro Islands (์์ผ๋๋ ์ํคํ
์ฒ) ]
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Pure HTML / Scoped CSS (์ ์ ๋งํฌ์
- Zero JS, ์ด๊ณ ์ ๋ก๋ฉ)โ
โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ Interactive Header โ โ Image Carousel โ โ
โ โ [client:load] โ โ [client:visible] โ โ
โ โ (React ์ฌ: ์ฆ์ ์ํ) โ โ (Vue ์ฌ: ์คํฌ๋กค ๋๋ฌ ์โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ
โ ์์ ์ ์ ๋ณธ๋ฌธ ๊ธ / ํ
์คํธ / ํธํฐ (HTML๋ง ๋ ๋๋ง, JS ์์)โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
```
### โก 5๋ ํด๋ผ์ด์ธํธ ๋๋ ํฐ๋ธ (Client Directives) ์ํ ์ ๋ต
Astro ์ปดํฌ๋ํธ ๋ด๋ถ์์ React/Vue/Svelte ์ปดํฌ๋ํธ๋ฅผ ํธ์ถํ ๋, ์ธ์ ๋ธ๋ผ์ฐ์ ์์ JS๋ฅผ ๋ก๋ํ ์ง ๊ฒฐ์ ํฉ๋๋ค:
| ๋๋ ํฐ๋ธ | ๋์ ์์ ๋ฐ ์ํ ํ์ด๋ฐ | ๊ถ์ฅ ์ค๋ฌด ์๋๋ฆฌ์ค |
| :--- | :--- | :--- |
| **`client:load`** | ํ์ด์ง ๋ก๋ ์ฆ์ ์ฐ์ ์์๋ก JS ๋ค์ด๋ก๋ ๋ฐ ์ํ | ์๋จ ๊ธ๋ก๋ฒ ๋ค๋น๊ฒ์ด์
(GNB), ๊ฒ์์ฐฝ, ๋คํฌ๋ชจ๋ ํ ๊ธ |
| **`client:idle`** | ํ์ด์ง ์ด๊ธฐ ๋ก๋ฉ ์๋ฃ ํ ๋ธ๋ผ์ฐ์ ๊ฐ ์ ํด(Idle) ์ํ์ผ ๋ ๋ก๋ | ์ฑ๋ด ์์ ฏ, ๋๊ธ ์
๋ ฅ์ฐฝ, ์๋ฆผ ๋ฐฐ์ง |
| **`client:visible`** | ์ฌ์ฉ์๊ฐ ์คํฌ๋กคํ์ฌ ํด๋น ์ปดํฌ๋ํธ๊ฐ ํ๋ฉด(Viewport)์ ๋ํ๋ ๋ | ํ๋จ ์ถ์ฒ ์ํ ์บ๋ฌ์
, ๋ณต์กํ ๋ฐ์ดํฐ ์ฐจํธ, ๋ฌดํ ์คํฌ๋กค |
| **`client:media="(query)"`** | ์ง์ ํ CSS ๋ฏธ๋์ด ์ฟผ๋ฆฌ๊ฐ ์ผ์นํ ๋๋ง ๋ก๋ | ๋ชจ๋ฐ์ผ ํ๋ฒ๊ฑฐ ์ฌ๋ผ์ด๋ ๋ฉ๋ด (`client:media="(max-width: 768px)"`) |
| **`client:only="react"`** | ์๋ฒ ๋ ๋๋ง(SSR)์ ์์ ํ ๊ฑด๋๋ฐ๊ณ ํด๋ผ์ด์ธํธ์์๋ง ๋ ๋๋ง | ๋ธ๋ผ์ฐ์ ์ ์ฉ API(`window`, `localStorage`, `Canvas`) ์์กด UI |
---
## 3. [์ด๋ณด์ 1๋จ๊ณ] 3๋ถ ํฌ๋ก์๋: ์ปดํฌ๋ํธ & ๋ ์ด์์ ์ค์ต (Hello World)
Astro ์ปดํฌ๋ํธ๋ ํฌ๊ฒ ์๋จ ์ฝ๋ ํ์ค(`---`) ์ฌ์ด์ **์๋ฒ ์ปดํฌ๋ํธ ์คํฌ๋ฆฝํธ(Frontmatter)**์ ํ๋จ์ **HTML ํ
ํ๋ฆฟ ๋งํฌ์
**์ผ๋ก ๊ตฌ์ฑ๋ฉ๋๋ค.
### โ ๊ณตํต ๋ฒ ์ด์ค ๋ ์ด์์ (`src/layouts/BaseLayout.astro`)
```astro
---
// src/layouts/BaseLayout.astro
interface Props {
title: string;
description?: string;
}
const { title, description = 'Astro๋ก ์ ์๋ ์ด๊ณ ์ ๋ชจ๋ ์น์ฌ์ดํธ' } = Astro.props;
---
<!doctype html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{title}</title>
<meta name="description" content={description} />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
</head>
<body>
<header class="site-header">
<nav class="nav-container">
<a href="/" class="brand-logo">๐ Lucky Astro</a>
<div class="nav-links">
<a href="/">ํ</a>
<a href="/about">์๊ฐ</a>
</div>
</nav>
</header>
<main class="site-main">
<!-- ํ์ด์ง๋ณ ๋ณธ๋ฌธ ์ฝํ
์ธ ๊ฐ ์ฃผ์
๋๋ ์ฌ๋กฏ -->
<slot />
</main>
<footer class="site-footer">
<p>ยฉ 2026 LuckyTechWorld. All rights reserved.</p>
</footer>
</body>
</html>
<style>
/* ์ปดํฌ๋ํธ ๋ด๋ถ์ ์๋ ์ค์ฝํ(Scoped)๋๋ CSS */
body {
margin: 0;
font-family: system-ui, -apple-system, sans-serif;
background: #0f172a;
color: #f8fafc;
}
.nav-container {
max-width: 960px;
margin: 0 auto;
padding: 1rem 1.5rem;
display: flex;
justify-content: space-between;
align-items: center;
}
.brand-logo {
color: #38bdf8;
font-weight: 700;
text-decoration: none;
font-size: 1.25rem;
}
.nav-links a {
color: #94a3b8;
text-decoration: none;
margin-left: 1rem;
}
.nav-links a:hover {
color: #ffffff;
}
.site-main {
max-width: 960px;
margin: 2rem auto;
padding: 0 1.5rem;
}
.site-footer {
text-align: center;
padding: 2rem 0;
color: #64748b;
border-top: 1px solid #1e293b;
}
</style>
```
---
### โก ์ฌ์ฌ์ฉ ์นด๋ ์ปดํฌ๋ํธ (`src/components/TechCard.astro`)
```astro
---
// src/components/TechCard.astro
interface Props {
name: string;
category: string;
stars: number;
}
const { name, category, stars } = Astro.props;
---
<div class="tech-card">
<span class="category-badge">{category}</span>
<h3 class="tech-name">{name}</h3>
<div class="card-footer">
<span class="stars">โญ {stars.toLocaleString()}</span>
<!-- ์ถ๊ฐ ์ปค์คํ
์ฌ๋กฏ ์ง์ -->
<slot />
</div>
</div>
<style>
.tech-card {
background: #1e293b;
border: 1px solid #334155;
border-radius: 10px;
padding: 1.25rem;
transition: transform 0.2s, border-color 0.2s;
}
.tech-card:hover {
transform: translateY(-4px);
border-color: #38bdf8;
}
.category-badge {
font-size: 0.75rem;
color: #38bdf8;
background: rgba(56, 189, 248, 0.1);
padding: 0.25rem 0.5rem;
border-radius: 4px;
font-weight: 600;
}
.tech-name {
margin: 0.75rem 0;
font-size: 1.25rem;
}
.card-footer {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 0.875rem;
color: #94a3b8;
}
</style>
```
---
### โข ๋ฉ์ธ ํ ํ์ด์ง (`src/pages/index.astro`)
```astro
---
// src/pages/index.astro
import BaseLayout from '../layouts/BaseLayout.astro';
import TechCard from '../components/TechCard.astro';
// ํ๋ก ํธ๋งคํฐ: ์๋ฒ ๋น๋ ์์ ์ ์คํ๋๋ ์์ ์๋ฐ์คํฌ๋ฆฝํธ/ํ์
์คํฌ๋ฆฝํธ
const pageTitle = "Astro ๊ธฐ์ ์คํ ๋์๋ณด๋";
const techList = [
{ name: "Astro 5", category: "Web Framework", stars: 46000 },
{ name: "React 19", category: "UI Library", stars: 220000 },
{ name: "Tailwind CSS", category: "Styling", stars: 81000 },
];
---
<BaseLayout title={pageTitle}>
<section class="hero-section">
<h1>ํ์ํฉ๋๋ค! <span class="highlight">Astro ์ธ์์ ์ค์ ๊ฒ์</span></h1>
<p>์๋ฒ ๋น๋ ์์ ์ ๋ ๋๋ง๋์ด ์๋ฐ์คํฌ๋ฆฝํธ ๋ถ๋ด ์์ด ๋ฒ๊ฐ์ฒ๋ผ ๋น ๋ฆ
๋๋ค.</p>
</section>
<section class="card-grid">
{techList.map((tech) => (
<TechCard name={tech.name} category={tech.category} stars={tech.stars}>
<span class="status-dot">์ ์ ์๋</span>
</TechCard>
))}
</section>
</BaseLayout>
<style>
.hero-section {
text-align: center;
margin-bottom: 3rem;
}
.highlight {
background: linear-gradient(135deg, #38bdf8, #818cf8);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.card-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
gap: 1.5rem;
}
.status-dot {
color: #10b981;
font-weight: 600;
font-size: 0.8rem;
}
</style>
```
---
## 4. [2๋จ๊ณ] ํต์ฌ ๊ธฐ๋ฅ: ํ์ผ ๊ธฐ๋ฐ ๋ผ์ฐํ
& ๋์ ๋ผ์ฐํธ (Routing)
Astro๋ `src/pages/` ๋๋ ํ ๋ฆฌ ํ์์ ํ์ผ ๊ฒฝ๋ก๋ฅผ ๋ฐํ์ผ๋ก ์น URL์ ์๋ ์์ฑํฉ๋๋ค.
### ๐ ๋ผ์ฐํ
๊ท์น ๋งคํ
| ํ์ผ ๊ฒฝ๋ก | ๋ธ๋ผ์ฐ์ ์์ฒญ URL | ์ ํ |
| :--- | :--- | :--- |
| `src/pages/index.astro` | `/` | ๋ฃจํธ ํ ํ์ด์ง |
| `src/pages/about.astro` | `/about` | ๋จ์ผ ์ ์ ํ์ด์ง |
| `src/pages/blog/index.astro` | `/blog` | ๋ธ๋ก๊ทธ ๋ชฉ๋ก ํ์ด์ง |
| `src/pages/blog/[slug].astro` | `/blog/hello-world` | ์ ์ /๋์ ํ๋ผ๋ฏธํฐ ๋ผ์ฐํธ |
| `src/pages/docs/[...slug].astro` | `/docs/guide/install` | ์ ์ญ ์ผ์น(Catch-all) ๋ผ์ฐํธ |
| `src/pages/404.astro` | (์ผ์นํ์ง ์๋ ๋ชจ๋ URL) | ์ปค์คํ
404 ์๋ฌ ํ์ด์ง |
---
### ๐ ์ ์ ์ฌ์ดํธ ๋์ ๋ผ์ฐํธ (`src/pages/posts/[id].astro`)
๊ธฐ๋ณธ ์ ์ ๋ชจ๋(SSG)์์๋ `getStaticPaths()` ํจ์๋ฅผ ๋ฐ๋์ exportํ์ฌ ๋น๋ํ ํ์ด์ง ๋ชฉ๋ก(params ๋ฐ props)์ ์ฌ์ ์ ์ ์ํด์ผ ํฉ๋๋ค.
```astro
---
// src/pages/posts/[id].astro
import BaseLayout from '../../layouts/BaseLayout.astro';
// [1] ๋น๋ ์์ ์ ์์ฑํ ๋ชจ๋ ๊ฒฝ๋ก(Paths) ๋ฐํ
export async function getStaticPaths() {
// ์ค์ ํ๊ฒฝ์์๋ ์ธ๋ถ CMS๋ DB, Markdown ์ปฌ๋ ์
์์ ์กฐํ
const posts = [
{ id: '1', title: 'Astro ์์ผ๋๋์ ๋น๋ฐ', content: '์์ผ๋๋๋ ํ์ํ ๋ถ๋ถ๋ง ์ํํฉ๋๋ค.' },
{ id: '2', title: 'Content Collections ์๋ฒฝ ๊ฐ์ด๋', content: 'Zod ์คํค๋ง๋ก ํ์
์ ์๊ฒฉํ ๊ฒ์ฆํฉ๋๋ค.' },
{ id: '3', title: 'Astro 5 ์ ๊ธฐ๋ฅ ์ด์ ๋ฆฌ', content: '์๋ก์ด Content Layer API๊ฐ ๋์
๋์์ต๋๋ค.' },
];
return posts.map((post) => ({
params: { id: post.id },
props: { post },
}));
}
// [2] ํ์ฌ ์์ฒญ๋ ํ๋ผ๋ฏธํฐ ๋ฐ props ์์
const { id } = Astro.params;
const { post } = Astro.props;
---
<BaseLayout title={`${post.title} - ๊ฒ์๊ธ #${id}`}>
<article class="post-detail">
<a href="/" class="back-link">โ ๋ชฉ๋ก์ผ๋ก ๋์๊ฐ๊ธฐ</a>
<h1 class="post-title">{post.title}</h1>
<div class="post-meta">๊ฒ์๊ธ ID: <code>{id}</code></div>
<div class="post-body">
<p>{post.content}</p>
</div>
</article>
</BaseLayout>
<style>
.post-detail {
background: #1e293b;
padding: 2rem;
border-radius: 12px;
}
.back-link {
color: #38bdf8;
text-decoration: none;
font-size: 0.9rem;
}
.post-title {
margin: 1rem 0;
}
.post-meta {
color: #64748b;
font-size: 0.85rem;
margin-bottom: 1.5rem;
}
</style>
```
---
## 5. [2๋จ๊ณ] Content Collections: ํ์
์์ ์ฝํ
์ธ ๊ด๋ฆฌ (Content Collections)
Astro 5+์์๋ ๋์ฑ ๊ฐ๋ ฅํด์ง **Content Layer API**๋ฅผ ํตํด ๋ก์ปฌ ๋งํฌ๋ค์ด๋ฟ ์๋๋ผ ์๊ฒฉ API, DB ๋ฐ์ดํฐ๊น์ง Zod ์คํค๋ง๋ก ์๋ฒฝํ๊ฒ ํ์
๊ฒ์ฆํฉ๋๋ค.
### โ ์คํค๋ง ์ ์ (`src/content.config.ts`)
```typescript
// src/content.config.ts
import { defineCollection, z } from 'astro:content';
import { glob } from 'astro/loaders';
// ๋ธ๋ก๊ทธ ์ปฌ๋ ์
์ ์
const blog = defineCollection({
// src/content/blog ๋๋ ํ ๋ฆฌ์ ๋ชจ๋ md, mdx ํ์ผ ํ์
loader: glob({ pattern: '**/*.{md,mdx}', base: './src/content/blog' }),
schema: z.object({
title: z.string(),
description: z.string(),
pubDate: z.coerce.date(),
tags: z.array(z.string()).default([]),
draft: z.boolean().default(false),
}),
});
export const collections = { blog };
```
---
### โก ๋งํฌ๋ค์ด ๋ณธ๋ฌธ ํ์ฑ ๋ฐ ๋ ๋๋ง (`src/pages/blog/[...slug].astro`)
```astro
---
// src/pages/blog/[...slug].astro
import { getCollection, render } from 'astro:content';
import BaseLayout from '../../layouts/BaseLayout.astro';
export async function getStaticPaths() {
// ์ด์(draft)์ด ์๋ ๊ธ๋ง ํํฐ๋ง
const posts = await getCollection('blog', ({ data }) => !data.draft);
return posts.map((post) => ({
params: { slug: post.id },
props: { post },
}));
}
const { post } = Astro.props;
// Astro 5์ render() ํจ์๋ฅผ ํตํด ๋งํฌ๋ค์ด ํ์ฑ ์ปดํฌ๋ํธ ํ๋
const { Content, headings } = await render(post);
---
<BaseLayout title={post.data.title} description={post.data.description}>
<article class="blog-article">
<h1>{post.data.title}</h1>
<p class="date">๋ฐํ์ผ: {post.data.pubDate.toLocaleDateString('ko-KR')}</p>
<div class="tags">
{post.data.tags.map((tag) => <span class="tag">#{tag}</span>)}
</div>
<hr />
<!-- ๋ณํ๋ ๋งํฌ๋ค์ด ๋ณธ๋ฌธ ์๋ ์ถ๋ ฅ -->
<div class="markdown-content">
<Content />
</div>
</article>
</BaseLayout>
<style>
.date { color: #94a3b8; font-size: 0.9rem; }
.tag { background: #334155; padding: 2px 6px; border-radius: 4px; margin-right: 6px; font-size: 0.8rem; }
.markdown-content { line-height: 1.8; color: #e2e8f0; }
</style>
```
---
## 6. [3๋จ๊ณ] ์ค๋ฌด ์์ฉ: UI ํ๋ ์์ํฌ ์์ผ๋๋ & ์ ์ญ ์ํ ๊ณต์ (Frameworks & Nano Stores)
ํ๋์ Astro ํ์ด์ง ์์์ **React ์ปดํฌ๋ํธ**์ **Vue ์ปดํฌ๋ํธ**๋ฅผ ๋์์ ๊ตฌ๋ํ๊ณ , ์ด๊ฒฝ๋ ์ํ ๊ด๋ฆฌ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ธ **Nano Stores**๋ฅผ ํตํด ํ๋ ์์ํฌ ๊ฐ ์๋ฐฉํฅ ์ํ๋ฅผ ๊ณต์ ํฉ๋๋ค.
### โ ๊ณตํต ์ ์ญ ์คํ ์ด ์ ์ (`src/stores/cart.ts`)
```typescript
// src/stores/cart.ts
import { atom } from 'nanostores';
export interface CartItem {
id: string;
name: string;
price: number;
}
// ์ฅ๋ฐ๊ตฌ๋ ์์ดํ
๋ฐฐ์ด ์คํ ์ด
export const $cart = atom<CartItem[]>([]);
// ์์ดํ
์ถ๊ฐ ์ก์
export function addToCart(item: CartItem) {
$cart.set([...$cart.get(), item]);
}
```
---
### โก React ์ปดํฌ๋ํธ: ์ํ ์ถ๊ฐ ๋ฒํผ (`src/components/AddToCartButton.tsx`)
```tsx
// src/components/AddToCartButton.tsx
import React from 'react';
import { addToCart, type CartItem } from '../stores/cart';
export default function AddToCartButton({ item }: { item: CartItem }) {
return (
<button
onClick={() => addToCart(item)}
className="px-4 py-2 bg-blue-600 hover:bg-blue-500 text-white rounded font-medium"
>
React ์ฌ: ์ฅ๋ฐ๊ตฌ๋ ๋ด๊ธฐ ({item.price.toLocaleString()}์)
</button>
);
}
```
---
### โข Vue ์ปดํฌ๋ํธ: ์ค์๊ฐ ์ฅ๋ฐ๊ตฌ๋ ์นด์ดํฐ (`src/components/CartCounter.vue`)
```vue
<!-- src/components/CartCounter.vue -->
<template>
<div class="vue-counter">
<span>Vue ์ฌ ์ฅ๋ฐ๊ตฌ๋: <strong>{{ cartItems.length }}</strong> ๊ฐ</span>
</div>
</template>
<script setup lang="ts">
import { useStore } from '@nanostores/vue';
import { $cart } from '../stores/cart';
// Nano Stores ๋ฐ์ํ ๋ฐ์ธ๋ฉ
const cartItems = useStore($cart);
</script>
<style scoped>
.vue-counter {
background: #064e3b;
color: #34d399;
padding: 0.5rem 1rem;
border-radius: 6px;
font-weight: 600;
}
</style>
```
---
### โฃ Astro ํ์ด์ง์์ ๋ ํ๋ ์์ํฌ ๊ฒฐํฉ (`src/pages/shop.astro`)
```astro
---
// src/pages/shop.astro
import BaseLayout from '../layouts/BaseLayout.astro';
import AddToCartButton from '../components/AddToCartButton.tsx';
import CartCounter from '../components/CartCounter.vue';
const sampleItem = { id: 'p1', name: 'Astro ํ๋ํฐ', price: 49000 };
---
<BaseLayout title="๋ฉํฐ ํ๋ ์์ํฌ ์ผํ๋ชฐ">
<div class="shop-header">
<h1>ํ๋ ์์ํฌ ์์ผ๋๋ ์ฐ๋</h1>
<!-- Vue ์ฌ: ์์ ๋ก๋ -->
<CartCounter client:load />
</div>
<div class="product-box">
<h2>{sampleItem.name}</h2>
<!-- React ์ฌ: ์ฆ์ ๋ก๋ ๋ฐ ํด๋ฆญ ์ด๋ฒคํธ ์์ -->
<AddToCartButton client:load item={sampleItem} />
</div>
</BaseLayout>
```
---
## 7. [3๋จ๊ณ] ์ค๋ฌด ์์ฉ: ViewTransitions & API ์๋ํฌ์ธํธ & SSR (Transitions & SSR)
### ๐ฌ SPA ๋ถ๋๋ฌ์ด ํ๋ฉด ์ ํ: ClientRouter (View Transitions)
ํ์ด์ง ์ ์ฒด ์๋ก๊ณ ์นจ ์์ด ๋ชจ๋ฐ์ผ ์ฑ์ฒ๋ผ ๋งค๋๋ฝ๊ฒ ํ๋ฉด์ด ์ ํ๋๋๋ก `<ClientRouter />`๋ฅผ ํค๋์ ์ถ๊ฐํฉ๋๋ค.
```astro
---
// src/layouts/BaseLayout.astro (์๋จ ์ถ๊ฐ)
import { ClientRouter } from 'astro:transitions';
---
<head>
<!-- Astro 5 ํ์ค (Astro 4์์๋ ViewTransitions) -->
<ClientRouter />
</head>
```
---
### ๐ ๋ฐฑ์๋ REST API ์๋ํฌ์ธํธ (`src/pages/api/posts.json.ts`)
์๋ฒ๋ฆฌ์ค ํจ์๋ Node.js ๋ฐฑ์๋๋ก ๋์ํ๋ API ๋ผ์ฐํธ์
๋๋ค.
```typescript
// src/pages/api/posts.json.ts
import type { APIRoute } from 'astro';
export const GET: APIRoute = async ({ request, url }) => {
const data = [
{ id: 1, title: 'Astro 5 Release', views: 1250 },
{ id: 2, title: 'Islands Deep Dive', views: 3400 },
];
return new Response(JSON.stringify(data), {
status: 200,
headers: {
'Content-Type': 'application/json; charset=utf-8',
'Cache-Control': 'public, max-age=60',
},
});
};
export const POST: APIRoute = async ({ request }) => {
const body = await request.json();
return new Response(JSON.stringify({ success: true, received: body }), {
status: 201,
headers: { 'Content-Type': 'application/json' },
});
};
```
---
### ๐ SSR ํ์ด๋ธ๋ฆฌ๋(Hybrid) ๋ ๋๋ง ์ด๋ํฐ ์ค์
์ผ๋ถ ๋์ ๋ก๊ทธ์ธ ์ธ์
ํ์ด์ง๋ ์ค์๊ฐ API๋ง ์๋ฒ์์ ๊ณ์ฐํ๊ณ , ๊ธฐ๋ณธ ํ์ด์ง๋ ์ ์ HTML(SSG)๋ก ์์ฑํฉ๋๋ค.
```javascript
// astro.config.mjs
import { defineConfig } from 'astro/config';
import node from '@astrojs/node'; // ๋๋ @astrojs/cloudflare, @astrojs/vercel
export default defineConfig({
// Astro 5์์๋ output: 'static'์ด ๊ธฐ๋ณธ์ด๋ฉฐ, ํ์ด์ง๋ณ export const prerender = false ์ ์ธ
adapter: node({
mode: 'standalone', // ๋
๋ฆฝํ Node.js ์น์๋ฒ ์คํ
}),
});
```
```astro
---
// src/pages/dashboard.astro (๋์ SSR ํ์ด์ง ์ ์ธ)
export const prerender = false;
// ๋งค ์์ฒญ๋ง๋ค ์ฟ ํค ํ์ธ ๋ฐ ์ค์๊ฐ ์ฌ์ฉ์ ์ธ์ฆ
const sessionCookie = Astro.cookies.get('user_session');
if (!sessionCookie) {
return Astro.redirect('/login');
}
---
<h1>์ค์๊ฐ SSR ๋์๋ณด๋ (์ฌ์ฉ์: {sessionCookie.value})</h1>
```
---
## 8. CLI ํ์ ๋ช
๋ น์ด & ๋น๋ ํ๊ฒฝ (CLI Commands & Config)
### ๐ป ํ์ ํฐ๋ฏธ๋ ๋ช
๋ น์ด
```bash
# [1] ๋ก์ปฌ ๊ฐ๋ฐ ์๋ฒ ์์ (๊ธฐ๋ณธ ํฌํธ: 4321)
npx astro dev
# ํน์ ํฌํธ ์ง์ ์คํ
npx astro dev --port 3000 --host
# [2] ํ๋ก๋์
์ ์ HTML ๋น๋ ์คํ (dist/ ํด๋ ์์ฑ)
npx astro build
# [3] ๋น๋๋ ํ๋ก๋์
์ํฐํฉํธ ๋ก์ปฌ ๋ฏธ๋ฆฌ๋ณด๊ธฐ (Preview)
npx astro preview
# [4] ํ๋ก์ ํธ ์ ์ฒด ํ์
์คํฌ๋ฆฝํธ ๋ฐ Astro ์ปดํฌ๋ํธ ๊ฒ์ฌ
npx astro check
# [5] Astro ํ๊ฒฝ ๋ฐ ์ค์น๋ ์์กด์ฑ ์์ธ ์ง๋จ ๋ณด๊ณ ์ ์ถ๋ ฅ
npx astro info
```
---
## 9. ์ค๋ฌด ํธ๋ฌ๋ธ์ํ
& ํ๋ก๋์
์ด์ ์ฒดํฌ๋ฆฌ์คํธ (Troubleshooting & Best Practices)
### โ ๏ธ ์ด๋ณด์๊ฐ ๊ฐ์ฅ ํํ ๊ฒช๋ 5๋ ์ค์ ๋ฐ ํด๊ฒฐ์ฑ
| ์ฆ์ ๋ฐ ๋ฌธ์ ์ | ์์ธ ๋ถ์ | ํ์คํ ํด๊ฒฐ ๊ฐ์ด๋ |
| :--- | :--- | :--- |
| **ํ๋ก ํธ๋งคํฐ์ `console.log()`๊ฐ ๋ธ๋ผ์ฐ์ ์ ์ ์ฐํ** | ํ๋ก ํธ๋งคํฐ(`---`) ์ฝ๋๋ ์๋ฒ/๋น๋ ์์ ์ ์คํ๋จ | ๋ธ๋ผ์ฐ์ ์ฝ์์์ ๋ณด๋ ค๋ฉด ํ๋จ์ `<script>console.log(...)</script>` ํ๊ทธ๋ฅผ ์ฌ์ฉ |
| **React ์ปดํฌ๋ํธ์ ๋ฒํผ ํด๋ฆญ(`onClick`)์ด ๋ฌด๋ฐ์** | ํด๋ผ์ด์ธํธ ์ํ ๋๋ ํฐ๋ธ(`client:*`)๋ฅผ ์ง์ ํ์ง ์์ | `<MyButton client:load />` ๋๋ `client:visible` ๋๋ ํฐ๋ธ ํ์ ๋ช
์ |
| **`document is not defined` / `window is not defined` ์๋ฌ** | ์๋ฒ ์ฌ์ด๋ ๋ ๋๋ง ์์ ์ ๋ธ๋ผ์ฐ์ ์ ์ฉ ๊ฐ์ฒด์ ์ ๊ทผํจ | React๋ `useEffect()`, Astro๋ `<script>` ํ๊ทธ ์์์ ๋ธ๋ผ์ฐ์ ๊ฐ์ฒด ์ ๊ทผ |
| **`getStaticPaths() returned undefined` ๋น๋ ์๋ฌ** | SSG ๋์ ๋ผ์ฐํธ(`[slug].astro`)์์ ๋ฐฐ์ด์ ๋ฐํํ์ง ์์ | `return [{ params: { slug: 'a' } }]` ํ์์ ๋ฐฐ์ด์ ๋ฐ๋์ ๋ช
์์ ์ผ๋ก ๋ฐํ |
| **์ธ๋ถ ์ด๋ฏธ์ง ๋ก๋ฉ ์ ๋น๋ ์ง์ฐ ๋ฐ ๋์ญํญ ๋ญ๋น** | ์ผ๋ฐ `<img>` ํ๊ทธ๋ฅผ ์ฌ์ฉํด ์๋ณธ ํฌ๊ธฐ ๊ทธ๋๋ก ์ ์ก๋จ | Astro ๋ด์ฅ `<Image src={myImage} alt="์ค๋ช
" width={600} />` ์ต์ ํ ์ปดํฌ๋ํธ ์ฌ์ฉ |
---
### ๐ ํ๋ก๋์
๋ฐฐํฌ ์ฒดํฌ๋ฆฌ์คํธ (Production Checklist)
1. **์ ์ ์ฌ์ดํธ ์ฃผ์(`site`) ๋ช
์**:
`astro.config.mjs`์ `site: 'https://techworld.sulucky5.pe.kr'`๋ฅผ ๋ฐ๋์ ์ค์ ํ์ฌ sitemap.xml ๋ฐ Canonical URL์ด ์ฌ๋ฐ๋ฅธ ๋๋ฉ์ธ์ผ๋ก ์์ฑ๋๋๋ก ๋ณด์ฅํฉ๋๋ค.
2. **์์
๋ฉํ ํ๊ทธ (OpenGraph)**:
`<meta property="og:title" content={title} />`, `<meta property="og:image" content={new URL(image, Astro.site)} />` ๋ฑ ์ ๋ URL๋ก ๊ณต์ ์ด๋ฏธ์ง๋ฅผ ๊ตฌ์ฑํฉ๋๋ค.
3. **๋น๋ ์บ์ฑ ์ต์ ํ**:
CI/CD ํ์ดํ๋ผ์ธ(GitHub Actions)์์ `node_modules`์ `.astro` ๋๋ ํ ๋ฆฌ๋ฅผ ์บ์ํ์ฌ ๋น๋ ์๊ฐ์ 70% ์ด์ ๋จ์ถํฉ๋๋ค.
์๊ฒฌ ๋ฐ ์ง๋ฌธ
0์์ง ๋ฑ๋ก๋ ์๊ฒฌ์ด ์์ต๋๋ค. ์ฒซ ๋ฒ์งธ ๋๊ธ์ ๋จ๊ฒจ๋ณด์ธ์!
๋๊ธ ์์
๋๊ธ ์ญ์