블로그 기술 스택 Deep dive: Astro 완전 정복 학습 일지

React의 무거움을 덜어내고 웹의 본질인 콘텐츠에 집중하기 위해 Astro를 선택했습니다. 설치부터 Content Collections, 플러그인, 배포까지의 전 과정을 상세히 기록합니다.

Jun Noh

1. 프롤로그: 왜 굳이 새로운 도구를 배우는가?

그냥 뭔가 개발자스러운 흔하지 않은 블로그를 만들고 싶었고, 그렇다고 서버단부터 전부 손으로 만들기는 싫었다.

어차피 Markdown으로 모든 걸 기록하는 나라서, 그냥 단순하게 파일을 그대로 읽어서 서식에 맞춰 “예쁘게” 뿌려주는 플랫폼이면 만족이라고 생각했다.

그 와중에 Astro를 알게 됐고, Docs를 좀 읽어보니 내 가려운 곳을 정확히 긁어주고 있었다.

사실 거창한 아키텍처 용어보다 내 마음을 흔든 건 귀찮은 건 싫지만, 자유도는 포기 못 해라는 내 욕망을 충족시켜 준다는 점이었다.

블로그 하나 만드는데 복잡한 라우팅 설정을 만지고, 전역 상태 관리를 고민하는 건 너무 피곤하지 않은가?

나는 그저 내 마크다운 파일들이 빠르고 예쁘게 렌더링되길 바랄 뿐이었다. 그렇다고 정해진 테마에만 갇히는 건 싫고, 필요하다면 내가 좋아하는 React 컴포넌트를 섞어서 쓸 수 있는 유연함도 필요했다.

Astro는 딱 그 지점에 있었다. 기본적으로는 가벼운 정적 페이지를 만들어주면서도, 내가 원할 때만 익숙한 UI 프레임워크를 ‘섬(Island)‘처럼 톡 하고 얹을 수 있는 자유.

“복잡한 설정 없이, 콘텐츠에만 집중하세요.” 마치 이렇게 말하는 듯했다.

오늘은 이 매력적인 도구의 A부터 Z까지, 내가 학습하고 적용한 모든 내용을 빠짐없이 기록해두려 한다.


2. 왜 굳이 Astro였나?

블로그에 ‘거창한 프레임워크’는 사치다

블로그는 본질적으로 데이터를 DB에서 꺼내 HTML로 뿌려주는 것이 전부다.

예전 같았으면 Express에 EJS나 Pug 같은 템플릿 엔진을 붙여서 뚝딱 만들었을 것이다. 그게 가장 가볍고 직관적이니까.

하지만 2025년에 순수 템플릿 엔진만 쓰자니, 컴포넌트 재사용성이나 빌드 최적화 같은 현대적 개발 경험이 너무 아쉬웠다.

그렇다고 고작 정적 텍스트를 보여주기 위해 React/Next.js의 무거운 런타임을 싣는 건 배보다 배꼽이 더 큰 격이었다.

Astro: 현대적인 템플릿 엔진의 재발견

Astro는 이 딜레마를 완벽하게 해결해 주었다.

  1. MPA(Multi Page Application)의 귀환: Astro는 기본적으로 Express처럼 페이지 단위로 HTML을 새로 그린다. SPA의 복잡한 라우팅이나 상태 관리를 신경 쓸 필요가 없다. 이 단순함이 너무 편안했다.

  2. JS 없는 가벼움: 별다른 설정을 안 하면 JS를 아예 안 싣는다. 마치 Express 서버가 정적 HTML을 서빙하는 것처럼 빠르다.

  3. Islands Architecture: 이게 핵심이다. 전체를 React로 감싸는 게 아니라, 댓글 창이나 검색 바처럼 ‘인터랙션이 필요한 부분만’ React 컴포넌트로 콕 집어서 쓸 수 있다.

“가벼운 건 Express/EJS 감성인데, 문법은 세련된 JSX/Component.” 이것이야말로 내가 찾던, 과하지 않으면서도 강력한 도구였다.


3. 설치 및 초기 설정 (Setup & Structure)

3-1. 프로젝트 생성

npm create astro@latest
# 또는 기존 프로젝트에 추가
npm install astro

명령어 한 줄이면 충분했다.

3-2. 프로젝트 구조 분석

생성된 폴더 구조는 직관적이었다.

  • public/: 이미지나 파비콘 같은 정적 파일.
  • src/components/: 재사용 가능한 Astro 컴포넌트 (Header, Footer 등).
  • src/content/: 블로그 포스트(.md)와 설정 파일이 위치하는 곳.
  • src/layouts/: 페이지의 골격이 되는 레이아웃 파일.
  • src/pages/: 파일 기반 라우팅을 담당하는 곳.

3-3. 설정 파일 (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://slowflowsoft.com',  // 배포될 사이트 URL
  base: '/blog',                   // 서브 디렉토리 배포 시 설정
  integrations: [
    mdx(),                          // MDX 지원
    sitemap({                       // 사이트맵 자동 생성 설정
      changefreq: 'weekly',
      priority: 0.7,
    }),
    tailwind({                      // Tailwind CSS 통합
      applyBaseStyles: false,
    }),
  ],
  output: 'static',                 // 정적 사이트 생성 모드
  build: {
    assets: 'assets',               // 빌드된 에셋 디렉토리 지정
  },
});

4. 핵심 개념과 문법 (Core Concepts & Syntax)

새로운 언어를 배울 때 문법책을 파고들듯, Astro의 기본 구성 요소를 하나하나 해체해 보았다. 가장 흥미로운 점은 .astro라는 독자적인 파일 포맷이었다.

4-1. Astro 컴포넌트의 3단 구조 (The Anatomy)

Astro 컴포넌트는 마치 마크다운, JS, CSS가 완벽한 비율로 섞인 칵테일 같다. 파일은 명확하게 세 부분으로 나뉜다.

---
// 1. Component Script (Frontmatter)
// 이 부분은 오직 '서버 사이드(빌드 타임)'에서만 실행된다.
// 브라우저로 전송되지 않으므로, 비밀키나 무거운 라이브러리를 써도 안전하다.
const title = "Hello Astro";
const items = [1, 2, 3];
---

<div>
  <h1>{title}</h1>
  {items.map(item => <p>{item}</p>)}
</div>

<style>
  /* 3. Style */
  /* 기본적으로 이 컴포넌트에만 적용되는 스코프(Scoped) 스타일이다. */
  h1 {
    color: blue;
  }
</style>

이 구조를 보며 깨달았다. React에서는 useEffectuseState로 고민해야 했던 클라이언트/서버 로직의 경계가, Astro에서는 코드 펜스(---) 하나로 명쾌하게 정리된다.

4-2. Props와 타입 안전성

TypeScript를 공부하는 입장에서 가장 반가웠던 기능이다. 컴포넌트 간 데이터를 주고받을 때 interface를 통해 타입을 강제할 수 있다.

---
// BlogPost.astro
interface Props {
  title: string;
  description: string;
  pubDate: Date;
  tags?: string[]; // 선택적 속성(Optional)
}

// Astro.props를 통해 데이터를 구조 분해 할당한다.
const { title, description, pubDate, tags = [] } = Astro.props;
---

<article>
  <h1>{title}</h1>
  {/* 날짜 포맷팅 로직도 서버에서 처리되어 HTML로 변환된다 */}
  <time>{pubDate.toISOString()}</time>
</article>

4-3. 슬롯 (Slots)의 유연함

React의 children prop과 비슷하지만, HTML의 <slot> 개념을 차용해 더 직관적이다. 특히 **Named Slots(이름이 있는 슬롯)**을 사용하면 레이아웃의 특정 구역에 원하는 콘텐츠를 정확히 꽂아 넣을 수 있다.

<div class="container">
  <header>
    <slot name="header" /> </header>
  <main>
    <slot />  </main>
</div>
<Layout>
  <Fragment slot="header">
    <h1>이 부분이 헤더로 들어갑니다</h1>
  </Fragment>
  <p>이 부분은 메인으로 들어갑니다</p>
</Layout>

4-4. 파일 기반 라우팅과 동적 라우팅

src/pages/ 디렉토리 구조가 곧 URL이 된다는 점은 Next.js와 같지만, 동적 라우팅을 처리하는 방식에서 ‘정적 사이트(Static)‘의 특성이 드러난다.

  • src/pages/index.astro/
  • src/pages/blog/[slug].astro/blog/:slug

동적 라우팅 페이지([slug].astro)는 빌드 시점에 어떤 페이지들을 만들어야 할지 미리 알아야 한다. 그래서 getStaticPaths 함수가 필수적이다.

---
// [slug].astro
export async function getStaticPaths() {
  // 여기서 리턴한 배열만큼의 HTML 파일이 생성된다.
  const posts = [
    { params: { slug: 'post-1' } },
    { params: { slug: 'post-2' } },
  ];
  return posts;
}

const { slug } = Astro.params;
---

4-5. 서버 엔드포인트 (API Routes)

HTML 페이지뿐만 아니라 JSON 같은 데이터 파일도 생성할 수 있다. .ts 파일로 작성하면 된다.

// src/pages/api/data.json.ts
import type { APIContext } from 'astro';

export async function GET(context: APIContext) {
  return new Response(JSON.stringify({ data: 'hello' }), {
    headers: { 'Content-Type': 'application/json' },
  });
}

5. Content Collections (데이터베이스가 된 파일 시스템)

이번 학습의 하이라이트다. 단순히 마크다운 파일을 읽어오는 것을 넘어, 스키마 검증(Schema Validation)을 통해 데이터의 무결성을 보장한다. 마치 로컬 파일 시스템을 DB처럼 다루는 느낌이었다.

5-1. 컬렉션 설정과 Zod 스키마

src/content/config.ts 파일에서 zod 라이브러리를 사용해 데이터 구조를 정의한다.

import { defineCollection, z } from 'astro:content';

const blog = defineCollection({
  type: 'content',  // 마크다운 파일임을 명시
  schema: z.object({
    // 필수 필드
    title: z.string(),
    description: z.string(),
    // 문자열을 날짜 객체로 자동 변환 (Coercion)
    pubDate: z.coerce.date(),
    
    // 선택적 필드 및 기본값 설정
    updatedDate: z.coerce.date().optional(),
    author: z.string().default('Jun Noh'),
    tags: z.array(z.string()).default([]),
    draft: z.boolean().default(false),
    
    // Enum을 통해 값의 범위를 제한 (타입 안전성 극대화)
    persona: z.enum(['PL', 'Worker', 'Student', 'Human']),
  }),
});

export const collections = { blog };

이제 마크다운 Frontmatter에 오타가 있거나 필수 항목이 빠지면, 빌드 자체가 실패한다. 실수를 원천 봉쇄하는 것이다.

5-2. 데이터 쿼리와 렌더링 사용법

정의한 컬렉션은 getCollectiongetEntry로 쉽게 불러올 수 있다.

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

// 1. 모든 포스트 가져오기 (필터링 포함)
const publishedPosts = await getCollection('blog', ({ data }) => {
  // 프로덕션 환경에서는 draft가 true인 글은 제외한다.
  return import.meta.env.PROD ? data.draft !== true : true;
});

// 2. 개별 포스트 렌더링
const post = publishedPosts[0];
const { Content } = await post.render(); // 마크다운 본문을 HTML 컴포넌트로 변환
---

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

6. 프로젝트를 완성하는 플러그인 (Integrations)

Astro는 ‘바퀴를 다시 발명하지 않는다’. 검증된 도구들을 integrations라는 이름으로 손쉽게 흡수한다.

6-1. @astrojs/mdx

마크다운 안에서 React나 Astro 컴포넌트를 직접 쓰고 싶을 때 사용한다.

  • 설치: npm install @astrojs/mdx
  • 사용: .mdx 파일 내에서 import Button from '../components/Button.astro' 형태로 사용 가능.

6-2. @astrojs/sitemap

SEO의 기본인 사이트맵을 자동 생성한다.

  • 설정 (astro.config.mjs):
    sitemap({
      changefreq: 'weekly',
      priority: 0.7,
      // 필요한 경우 커스텀 페이지 추가 가능
      customPages: ['https://slowflowsoft.com/blog'],
    })
  • 팁: 동적 라우팅 페이지에서 lastmod 속성을 반환하면 사이트맵에 반영된다.

6-3. @astrojs/tailwind

CSS 프레임워크인 Tailwind를 통합한다.

  • 설정: applyBaseStyles: false 옵션을 통해 기본 스타일 충돌을 방지할 수 있다.
  • 전역 스타일: src/styles/global.css에서 @tailwind base; 등을 선언하여 사용한다.

6-4. @astrojs/rss

블로그 구독자를 위한 RSS 피드를 생성한다.

  • 파일 위치: src/pages/rss.xml.ts
  • 구현: getCollection으로 글을 가져와 pubDate 역순으로 정렬한 뒤, XML 아이템으로 매핑하여 반환한다.

7. 실전 활용 패턴 (Deep Dive Patterns)

단순한 튜토리얼을 넘어, 실제 블로그를 운영하기 위해 고민했던 아키텍처 패턴들이다.

7-1. SEO를 고려한 레이아웃 (BaseLayout)

모든 페이지의 <head>를 책임지는 BaseLayout.astro를 만들었다. Canonical URL과 Open Graph 태그를 동적으로 생성한다.

---
// BaseLayout.astro
interface Props {
  title: string;
  description?: string;
  ogImage?: string;
}
const { title, description, ogImage } = Astro.props;
const canonicalURL = new URL(Astro.url.pathname, Astro.site);
---
<link rel="canonical" href={canonicalURL} />
<meta property="og:url" content={canonicalURL} />

7-2. 검색 엔진을 위한 JSON-LD 주입

BlogPost.astro에서는 구글이 글의 구조를 더 잘 이해하도록 Schema.org 형식의 데이터를 주입했다.

---
const jsonLd = {
  "@context": "https://schema.org",
  "@type": "BlogPosting",
  "headline": title,
  "datePublished": pubDate.toISOString(),
  // ...
};
---
<script type="application/ld+json" set:html={JSON.stringify(jsonLd)} />

7-3. 동적 라우팅과 배포 안정성 확보

[slug].astrogetStaticPaths 함수에서, 배포 환경(PROD)일 때 초안(draft: true)을 자동으로 제외하는 로직을 추가했다. 실수로 작성 중인 글이 배포되는 것을 막기 위함이다.

export async function getStaticPaths() {
  const posts = await getCollection('blog', ({ data }) => {
    return import.meta.env.PROD ? data.draft !== true : true;
  });
  // ... 매핑 로직
}

7-4. 페르소나별 UI 분기 (Component Reuse)

이 블로그의 핵심인 4가지 페르소나(PL, Worker, Student, Human)에 따라 카드의 스타일을 다르게 보여주기 위해 매핑 객체를 활용했다.

---
const personaConfig = {
  PL: { icon: '💼', class: 'persona-pl' },
  Worker: { icon: '☕', class: 'persona-worker' },
  Student: { icon: '🌱', class: 'persona-student' },
  Human: { icon: '🍺', class: 'persona-human' },
};
const config = personaConfig[persona]; // 데이터에 따라 설정 자동 선택
---

8. 배포 및 최적화 (Production Ready)

마지막으로, 로컬에서만 도는 코드가 아니라 실제 서비스로 배포하는 과정이다.

8-1. 빌드 (Build)

npm run build를 실행하면 dist/ 폴더에 최종 산출물이 생성된다.

dist/
├── blog/
│   ├── index.html
│   └── post-1/index.html
├── assets/ (최적화된 CSS, JS)
├── sitemap-index.xml
└── rss.xml

8-2. Docker 배포 전략 (Multi-stage Build)

이미지 크기를 줄이기 위해 Multi-stage 빌드를 적용한 Dockerfile을 작성했다.

  1. Builder Stage: node:20-alpine 이미지에서 의존성을 설치하고 빌드한다.
  2. Runner Stage: 가벼운 nginx:alpine 이미지를 사용하여, Builder 단계에서 생성된 dist 폴더만 복사해 온다.
# Dockerfile 예시
FROM node:20-alpine AS builder
# ... 빌드 과정 ...

FROM nginx:alpine
COPY --from=builder /blog/dist /usr/share/nginx/html/blog

8-3. 최적화 포인트

  • 이미지: astro:assets<Image /> 컴포넌트를 사용해 포맷 변환과 리사이징을 자동화했다.
  • CSS: 빌드 과정에서 사용하지 않는 스타일은 자동으로 제거(Purge)된다.
  • 정적 모드: output: 'static' 설정을 통해 서버 연산 없이 파일만 서빙되도록 하여 속도를 극대화했다.

9. 에필로그: 학습을 마치며

이번 탐구를 통해 Astro가 단순히 ‘또 하나의 자바스크립트 프레임워크’가 아님을 알게 되었다. 그것은 **“웹 콘텐츠를 가장 효율적으로 전달하는 방법은 무엇인가?”**라는 질문에 대한 기술적 응답이었다.

Zero JS를 지향하면서도 필요한 순간에는 유연하게 인터랙션을 허용하고, Content Collections를 통해 데이터 관리의 엄격함까지 갖춘 Astro. 기본기를 중시하고 원리를 탐구하는 개발자에게 이보다 더 좋은 교보재는 없을 것이다.

이 블로그는 앞으로도 이런 깊이 있는 배움의 기록들로 채워질 것이다.

다른 글 보기