Astro로 개인 웹사이트 만들기
General
- 01 Mac에서 ₩ 표시와 작별하기
- 02 맥북 초기 세팅 + 어플리케이션 추천
- 03 Astro로 개인 웹사이트 만들기
왜 Astro인가?
개인 웹사이트를 만들기 위해 여러 프레임워크를 고민했다. Next.js, Gatsby, Hugo 등 선택지가 많았지만, 결국 Astro를 선택했다.
Next.js
물론 좋은 프레임워크지만, 개인 웹사이트만큼은 다른 걸로 만들어보고 싶었다.
그리고 개인 웹사이트에 Next.js는 좀 과하다고 생각했다. SSR과 API Routes는 굳이였다. 정적 페이지면 충분한데, 굳이 SSR 프레임워크를 쓸 이유가 없다.
Gatsby vs Hugo vs Astro
Gatsby는 GraphQL을 강제하는 게 마음에 안 들었다. 블로그 만드는데 GraphQL까지 써야 하나? 그리고 빌드가 느리다는 악명이 자자하다.
Hugo는 Go 기반이라 빠르긴 한데, 템플릿 문법이 너무 낯설었다. React/Vue에 익숙한 프론트엔드 개발자에게는 러닝커브가 있다.
Astro는 둘의 장점을 합친 느낌이었다. 정적 사이트 생성에 최적화되어 있으면서도, 컴포넌트 문법이 React/Vue와 비슷해서 친숙했다.
Astro의 장점
- 정적 사이트 생성 - 블로그는 대부분 정적 콘텐츠라 SSG가 적합하다
- 마크다운 지원 - Content Collection으로 마크다운 파일을 쉽게 관리할 수 있다
- Zero JavaScript by Default - 기본적으로 JavaScript를 안 보내서 빠르다. 필요한 곳에만 추가하면 된다
- 유연한 컴포넌트 - React, Vue, Svelte 등 원하는 프레임워크를 섞어 쓸 수 있다 (아일랜드 아키텍처)
- 개발 경험 - 핫 리로드가 빠르고, 타입스크립트 지원이 잘 된다
무엇보다 새로운 걸 배워보고 싶었다.
프로젝트 시작하기
npm create astro@latest -- --template blog
Astro에서 제공하는 blog 템플릿으로 시작했다. 기본적인 구조가 잡혀있어서 편했다.
구현한 기능들
1. 반응형 레이아웃
헤더와 본문의 너비를 일관되게 맞추고, 반응형으로 구현했다.
/* 데스크탑 */
.content-wrapper {
display: grid;
grid-template-columns: 60vw 250px;
gap: 2em;
justify-content: center;
}
/* 태블릿 이하 */
@media (max-width: 1199px) {
.content-wrapper {
grid-template-columns: 90vw;
}
}
데스크탑에서는 본문 60vw + 목차 250px, 태블릿 이하에서는 본문만 90vw로 표시된다.
2. 시리즈 기능
블로그 글을 시리즈로 묶어서 관리할 수 있게 했다. 먼저 content.config.ts에 스키마를 정의했다.
const blog = defineCollection({
loader: glob({ base: "./src/content/blog", pattern: "**/*.{md,mdx}" }),
schema: ({ image }) =>
z.object({
title: z.string(),
description: z.string(),
pubDate: z.coerce.date(),
heroImage: image().optional(),
series: z.string().optional(),
}),
});
그리고 SeriesNav.astro 컴포넌트를 만들어서 같은 시리즈의 글들을 보여준다.
---
const { series, currentSlug } = Astro.props;
// 파일명에서 날짜 추출해서 정렬
function getDateFromSlug(slug: string): Date {
const filename = slug.split('/').pop() || '';
const match = filename.match(/^(\d{4}-\d{2}-\d{2})/);
return match ? new Date(match[1]) : new Date(0);
}
const allPosts = await getCollection('blog');
const seriesPosts = allPosts
.filter((post) => post.data.series === series)
.map((post) => ({
slug: post.id,
title: post.data.title,
date: getDateFromSlug(post.id),
}))
.sort((a, b) => a.date.getTime() - b.date.getTime());
---
처음에는 seriesOrder 필드로 순서를 관리했는데, 중간에 글이 추가되면 뒤의 모든 글 순서를 바꿔야 하는 문제가 있었다. 그래서 파일명의 날짜(2025-12-23-title.md)를 파싱해서 자동 정렬하도록 변경했다.
3. 다크모드
CSS 변수와 data-theme 속성을 활용해서 다크모드를 구현했다.
:root {
--accent: #2337ff;
--black: 15, 18, 25;
--gray-dark: 34, 41, 57;
--bg: #fff;
}
[data-theme="dark"] {
--accent: #6b7eff;
--black: 255, 255, 255;
--gray-dark: 230, 235, 245;
--bg: #1a1a2e;
}
ThemeToggle.astro 컴포넌트에서 테마를 전환하고 localStorage에 저장한다.
const theme = (() => {
if (localStorage.getItem("theme")) {
return localStorage.getItem("theme");
}
if (window.matchMedia("(prefers-color-scheme: dark)").matches) {
return "dark";
}
return "light";
})();
document.documentElement.setAttribute("data-theme", theme);
시스템 설정을 우선 감지하고, 사용자가 변경하면 localStorage에 저장해서 유지한다.
4. 목차(TOC)
글 옆에 목차가 표시되어 긴 글도 쉽게 탐색할 수 있다. Astro의 render() 함수에서 headings를 추출할 수 있다.
---
// [...slug].astro
const { Content, headings } = await render(post);
---
<BlogPost headings={headings}>
<Content />
</BlogPost>
TableOfContents.astro에서 h2, h3만 필터링해서 표시한다.
---
const { headings = [] } = Astro.props;
const toc = headings.filter((h) => h.depth >= 2 && h.depth <= 3);
---
<nav class="toc">
<h4>목차</h4>
<ul>
{toc.map((heading) => (
<li class={`depth-${heading.depth}`}>
<a href={`#${heading.slug}`}>{heading.text}</a>
</li>
))}
</ul>
</nav>
클릭하면 해당 섹션으로 스크롤된다. 1199px 이하에서는 목차가 숨겨진다.
5. 페이지네이션
글이 많아지면 페이지네이션이 필요하다. Astro의 paginate() 함수로 간단하게 구현했다.
---
// [...page].astro
export const getStaticPaths = (async ({ paginate }) => {
const posts = (await getCollection('blog')).sort(
(a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf(),
);
return paginate(posts, { pageSize: 10 });
}) satisfies GetStaticPaths;
const { page } = Astro.props;
---
{page.data.map((post) => (
<PostCard post={post} />
))}
<nav class="pagination">
{page.url.prev && <a href={page.url.prev}>← 이전</a>}
<span>{page.currentPage} / {page.lastPage}</span>
{page.url.next && <a href={page.url.next}>다음 →</a>}
</nav>
파일명을 index.astro에서 [...page].astro로 변경하면 /blog, /blog/2, /blog/3 형태로 페이지가 생성된다.
폴더 구조
src/
├── content/
│ └── blog/
│ ├── general/ # 일반 글
│ │ └── 2025-12-23-astro-blog.md
│ ├── hanghae-plus/ # 항해플러스 시리즈
│ └── FE/ # 프론트엔드 관련
├── components/
│ ├── Header.astro
│ ├── Footer.astro
│ ├── SeriesNav.astro
│ ├── TableOfContents.astro
│ └── ThemeToggle.astro
├── layouts/
│ └── BlogPost.astro
├── pages/
│ ├── blog/
│ │ ├── [...page].astro # 블로그 목록 (페이지네이션)
│ │ └── [...slug].astro # 개별 글
│ ├── series.astro # 시리즈 목록
│ └── about.astro
└── styles/
└── global.css
시리즈별로 폴더를 나누고, 파일명에 날짜를 넣어서 정렬이 자동으로 되도록 했다.
Obsidian으로 글 작성하기
글은 Obsidian에서 작성한다. Obsidian의 장점:
- 로컬 마크다운 파일 - 그냥
.md파일이라 Astro와 궁합이 좋다 - 링크와 백링크 - 글 간의 연결을 쉽게 파악할 수 있다
- 플러그인 생태계 - 필요한 기능은 플러그인으로 확장 가능
- 빠른 검색 - 예전에 쓴 글도 금방 찾을 수 있다
워크플로우는 간단하다:
- Obsidian에서 글 작성
src/content/blog/폴더에 저장- frontmatter 작성 (title, description, pubDate, series)
npm run dev로 확인- 배포
Obsidian vault를 Astro 프로젝트 안에 두면, 글 작성과 개발을 한 곳에서 할 수 있다.
마무리
Astro는 개인 웹사이트에 정말 적합한 프레임워크다. Content Collection으로 마크다운을 쉽게 관리하고, 필요한 기능만 컴포넌트로 추가하면 된다. Obsidian과의 조합도 훌륭하다.
특히 좋았던 점:
- 마크다운 파일만 추가하면 글이 바로 반영된다
- JavaScript를 최소화해서 빠르다
- 타입스크립트 지원이 잘 되어있다
- Obsidian에서 작성한 글을 그대로 사용할 수 있다
단점이라면 Astro가 MPA(Multi-Page Application)라는 점이다. 페이지 이동할 때마다 전체 페이지가 새로고침된다. SPA처럼 부드러운 전환을 원한다면 View Transitions API를 추가로 설정해야 한다.
앞으로 꾸준히 글을 써봐야겠다.

![[항해플러스] 최종 회고](https://velog.velcdn.com/images/suadesu/post/2d89266f-dc6a-43c9-a491-1996be5f2c2a/image.png)