목차
개요
이전 글 Pagefind를 이용한 검색 구현에서 검색 기능을 다루었습니다. 이번 글에서는 블로그의 전체 레이아웃과 컴포넌트 아키텍처를 설명합니다.
Astro에서는 페이지의 공통 요소(Head, Navbar, Footer 등)를 컴포넌트로 만들고, 레이아웃으로 조합하여 일관된 구조를 유지합니다. Jekyll의 _layouts/와 _includes/ 디렉토리에 대응하는 구조인데, Astro에서는 TypeScript 타입 지원과 JSX 표현식 덕분에 훨씬 직관적으로 관리할 수 있습니다.
Astro 컴포넌트의 기본 구조
Astro 컴포넌트(.astro 파일)는 크게 두 부분으로 나뉩니다. 상단의 frontmatter 영역과 하단의 템플릿 영역입니다.
---
// 1. Frontmatter (서버 사이드 JavaScript/TypeScript)
// --- 사이에 작성한 코드는 빌드 타임에 서버에서 실행됩니다.
import Component from './Component.astro';
interface Props {
title: string;
}
const { title } = Astro.props;
const data = await fetchData();
---
<!-- 2. 템플릿 (HTML + JSX 표현식) -->
<!-- frontmatter 아래의 HTML은 빌드 시 정적 HTML로 변환됩니다. -->
<div>
<h1>{title}</h1>
<Component />
</div>
- Frontmatter (
---사이): 서버 사이드에서 실행되는 코드입니다. 데이터 가져오기, props 처리, 로직 수행 등을 담당합니다. 여기서 작성한 코드는 브라우저에 전송되지 않습니다. - Template: HTML과 JSX 표현식을 사용하여 UI를 렌더링합니다.
{변수명}으로 frontmatter에서 정의한 값을 출력할 수 있습니다. 빌드 타임에 정적 HTML로 변환됩니다.
이 구조가 Jekyll의 Liquid 템플릿과 가장 크게 다른 점입니다. Liquid에서는 템플릿 안에 로직을 섞어야 했지만, Astro에서는 로직과 마크업이 깔끔하게 분리됩니다.
BaseLayout — 모든 페이지의 뼈대
BaseLayout.astro는 블로그의 모든 페이지를 감싸는 최상위 레이아웃입니다. HTML 문서의 기본 구조(<!DOCTYPE html>, <html>, <head>, <body>)를 정의하고, 공통 컴포넌트를 배치합니다.
---
// src/layouts/BaseLayout.astro
import Head from '../components/Head.astro';
import Navbar from '../components/Navbar.astro';
import Footer from '../components/Footer.astro';
import type { Lang } from '../i18n/translations';
interface Props {
title: string;
description: string;
image?: string;
lang: Lang;
permalink: string;
isPost?: boolean;
date?: string;
categoryPosts?: CategoryPostItem[];
noindex?: boolean;
category?: string;
isHome?: boolean;
isCategory?: boolean;
}
const { title, description, image, lang, permalink, isPost, date,
categoryPosts, noindex, category, isHome, isCategory } = Astro.props;
---
<!DOCTYPE html>
<html lang={lang}>
<Head
title={title} description={description} image={image}
lang={lang} permalink={permalink} isPost={isPost} date={date}
categoryPosts={categoryPosts} noindex={noindex} category={category}
isHome={isHome} isCategory={isCategory}
/>
<body>
<Navbar lang={lang} />
<slot /> <!-- 페이지별 콘텐츠가 들어가는 위치 -->
<Footer lang={lang} permalink={permalink} />
</body>
</html>
Props가 꽤 많은데, 이는 Head.astro에서 페이지 유형에 따라 다른 SEO 태그(JSON-LD 등)를 출력해야 하기 때문입니다. 각 Props의 역할은 다음과 같습니다.
<Head>: SEO 메타 태그, JSON-LD, 스타일, 스크립트 등<head>영역 전체를 관리합니다. 이전 포스트 SEO 구현에서 상세히 다루었습니다.<Navbar>: 네비게이션 바와 언어 전환 기능을 제공합니다.<slot />: 이 위치에 각 페이지의 고유 콘텐츠가 삽입됩니다.Jekyll의{{ content }}에 해당합니다.<Footer>: 푸터와 소셜 공유 버튼을 표시합니다.
PostLayout — 블로그 포스트 전용 레이아웃
PostLayout.astro는 블로그 포스트에만 사용되는 레이아웃입니다. BaseLayout을 감싸면서 포스트에 필요한 추가 요소(히어로 헤더, 브레드크럼, 광고, 관련 포스트, 댓글 등)를 제공합니다.
---
// src/layouts/PostLayout.astro
import BaseLayout from './BaseLayout.astro';
import Breadcrumbs from '../components/Breadcrumbs.astro';
import Comments from '../components/Comments.astro';
import TopAds from '../components/ads/TopAds.astro';
import BottomAds from '../components/ads/BottomAds.astro';
import LeftAds from '../components/ads/LeftAds.astro';
import RightAds from '../components/ads/RightAds.astro';
// ...
interface Props {
title: string;
description: string;
image?: string;
lang: Lang;
permalink: string;
category: string;
date: string;
comments: boolean;
}
---
<BaseLayout title={title} description={description} ...>
<!-- 히어로 헤더: 포스트 제목과 배경 이미지 -->
<header class="masthead" style={image ? `background-image: url('${image}')` : undefined}>
<div class="overlay"></div>
<div class="container">
<h1>{title}</h1>
<div class="heading-date">{formattedDate}</div>
<h2 class="subheading">{description}</h2>
</div>
</header>
<TopAds />
<div class="container container-contents">
<div class="row">
<Breadcrumbs items={breadcrumbItems} />
</div>
<div class="row">
<LeftAds />
<article class="col-lg-8 col-md-10 mx-auto" data-pagefind-body>
<slot /> <!-- 마크다운 콘텐츠가 렌더링되는 위치 -->
</article>
<RightAds />
</div>
<BottomAds />
<!-- 같은 카테고리의 관련 포스트 목록 -->
<div class="post-list-container">...</div>
<Comments enabled={comments} />
</div>
</BaseLayout>
포스트 페이지를 열면 가장 위에 배경 이미지가 깔린 히어로 헤더가 보이고, 그 아래에 브레드크럼 네비게이션, 본문 콘텐츠, 관련 포스트 목록, 댓글 순으로 표시됩니다. 광고는 상하좌우 4개 위치에 배치됩니다.
관련 포스트 목록 데이터
포스트 하단에는 같은 카테고리의 다른 포스트 목록이 표시됩니다. 이 데이터는 빌드 타임에 getCollection()으로 가져와 JSON으로 클라이언트에 전달합니다.
// src/layouts/PostLayout.astro의 frontmatter 영역
const allPosts = await getCollection(
'blog',
(post) => post.data.lang === lang && post.data.published !== false
);
const catPosts = allPosts.filter((p) => p.data.category === category);
const sortedPosts = catPosts.sort(
(a, b) => new Date(b.data.date).getTime() - new Date(a.data.date).getTime()
);
이 데이터를 클라이언트 JavaScript에서 받아 페이지네이션과 포스트 내 검색 기능을 구현합니다. 별도의 API 호출 없이 빌드 타임에 모든 데이터가 준비되는 것이 정적 사이트의 장점입니다.
Hit counter
각 포스트의 조회수는 myhits.vercel.app API를 사용하여 표시합니다. 별도의 백엔드 없이 외부 서비스로 간단하게 구현할 수 있습니다.
주요 컴포넌트 소개
블로그에서 사용하는 주요 컴포넌트들을 간단히 소개합니다.
Navbar.astro
네비게이션 바와 언어 전환 기능을 제공하는 컴포넌트입니다. Home, Latest, Category, Apps, Contact 메뉴와 함께, 현재 페이지의 다른 언어 버전으로 이동할 수 있는 언어 전환 버튼을 포함합니다.
<!-- src/components/Navbar.astro -->
<nav>
<a href={getLocalizedPath('/', lang)}>Home</a>
<a href={getLocalizedPath('/latest/', lang)}>Latest</a>
<!-- 언어 전환: 현재 페이지의 다른 언어 버전으로 이동 -->
{Object.entries(languages).map(([code, name]) => (
<a href={getLocalizedPath(currentPath, code)}>{name}</a>
))}
</nav>
CategoryCard.astro
홈페이지에서 각 카테고리를 카드 형태로 표시하는 컴포넌트입니다. 카테고리 이미지, 제목, 설명, “자세히 보기” 링크를 포함합니다.
---
// src/components/CategoryCard.astro
interface Props {
slug: string;
image: string;
title: string;
description: string;
permalink: string;
seeMore: string;
lang: Lang;
}
---
PostCard.astro
포스트 목록(최신글 페이지 등)에서 각 포스트를 카드 형태로 표시하는 컴포넌트입니다. 제목, 설명, 날짜, 카테고리 정보를 포함합니다.
Breadcrumbs.astro
현재 페이지의 위치를 계층 구조로 보여주는 네비게이션 컴포넌트입니다. 사용자가 상위 페이지로 쉽게 이동할 수 있도록 도와줍니다.
Home > Astro > 레이아웃과 컴포넌트 아키텍처
SponsorButtons.astro
후원 버튼을 표시하는 컴포넌트입니다. 포스트 히어로 헤더와 본문 하단 두 곳에 배치하여, 글이 도움이 되었을 때 후원할 수 있는 링크를 제공합니다.
AppPromo.astro
직접 개발한 앱을 홍보하는 컴포넌트입니다. 언어에 따라 다른 콘텐츠를 표시하며, 한국어 페이지에서는 React Native 관련 서적 홍보도 함께 포함됩니다.
동적 라우팅으로 포스트 페이지 생성하기
블로그의 각 포스트 페이지는 하나하나 수동으로 만드는 것이 아니라, [...path].astro라는 catch-all 라우팅 파일에서 자동으로 생성됩니다. getStaticPaths 함수에서 모든 포스트의 URL을 반환하면, 빌드 시 각각에 대한 정적 HTML 파일이 만들어집니다.
---
// src/pages/ko/[...path].astro
import type { GetStaticPaths } from 'astro';
import PostLayout from '../../layouts/PostLayout.astro';
import { getCollection, render } from 'astro:content';
import { getLocalizedPath } from '../../i18n/translations';
const lang = 'ko';
export const getStaticPaths = (async () => {
const posts = await getCollection('blog', (post) =>
post.data.lang === 'ko' && post.data.published !== false
);
return posts.map((post) => {
const pathStr = post.data.permalink.replace(/^\//, '').replace(/\/$/, '');
return {
params: { path: pathStr },
props: { post },
};
});
}) satisfies GetStaticPaths;
const { post } = Astro.props;
const { Content } = await render(post);
---
<PostLayout
title={post.data.title}
description={post.data.description}
image={post.data.image}
lang={lang}
permalink={getLocalizedPath(post.data.permalink, lang)}
category={post.data.category}
date={post.data.date.toISOString()}
comments={post.data.comments}
>
<Content />
</PostLayout>
예를 들어 permalink: /astro/installation/인 한국어 포스트가 있다면, params.path가 astro/installation이 되어 최종 URL이 /ko/astro/installation/으로 생성됩니다. 일본어는 src/pages/[...path].astro, 영어는 src/pages/en/[...path].astro에서 동일한 구조로 처리합니다.
Jekyll Liquid 템플릿과의 비교
Jekyll에서 Astro로 옮기면서 템플릿 작성 방식이 완전히 달라졌습니다. 아래 표로 주요 차이점을 비교합니다.
| 항목 | Jekyll (Liquid) | Astro |
|---|---|---|
| 레이아웃 상속 | layout: post frontmatter | import와 <slot /> |
| 변수 출력 | {{ "{{ page.title " }}}} | {title} (JSX 표현식) |
| 조건부 렌더링 | {% raw %}{% if %}{% endraw %} | {condition && <Element />} |
| 반복문 | {% raw %}{% for %}{% endraw %} | {items.map(item => <Element />)} |
| 컴포넌트 포함 | {% raw %}{% include component.html %}{% endraw %} | import + <Component /> |
| 타입 안전성 | 없음 | TypeScript interface Props |
| 데이터 접근 | site.posts, page.title | getCollection(), Astro.props |
개인적으로 가장 만족스러운 부분은 TypeScript 타입 지원입니다. Jekyll에서는 Props에 오타가 있어도 아무런 경고 없이 빈 값이 출력되었지만, Astro에서는 interface Props로 타입을 정의하면 IDE에서 자동 완성과 타입 체크를 받을 수 있습니다. 컴포넌트가 늘어날수록 이 차이가 크게 느껴집니다.
완료
이번 글에서는 Astro 블로그의 레이아웃과 컴포넌트 아키텍처를 살펴보았습니다.
BaseLayout.astro: Head, Navbar, Footer를 감싸는 최상위 레이아웃PostLayout.astro: 히어로 헤더, 광고, 관련 포스트, 댓글 등을 포함하는 포스트 전용 레이아웃- Navbar, Breadcrumbs, CategoryCard, PostCard, SponsorButtons, AppPromo 등 다양한 컴포넌트
[...path].astrocatch-all 동적 라우팅으로 포스트 페이지 자동 생성- Jekyll Liquid 템플릿 대비 TypeScript 타입 안전성과 직관적인 JSX 문법의 장점
다음 글 GitHub Pages 배포에서는 빌드된 사이트를 GitHub Pages에 배포하는 방법을 다룹니다.
시리즈 안내
이 포스트는 Jekyll에서 Astro로 마이그레이션 시리즈의 일부입니다.
- Jekyll에서 Astro로 마이그레이션한 이유
- Astro 설치 및 프로젝트 구성
- Content Collections와 마크다운 마이그레이션
- 다국어(i18n) 구현
- SEO 구현
- 이미지 최적화 — 커스텀 rehype 플러그인
- 댓글 시스템 (Utterances)
- 광고 연동 (Google AdSense)
- Pagefind를 이용한 검색 구현
- 레이아웃과 컴포넌트 아키텍처
- GitHub Pages 배포
- 소셜 공유 자동화 스크립트
- 트러블슈팅과 팁
제 블로그가 도움이 되셨나요? 하단의 댓글을 달아주시면 저에게 큰 힘이 됩니다!
앱 홍보
Deku가 개발한 앱을 한번 사용해보세요.Deku가 개발한 앱은 Flutter로 개발되었습니다.관심있으신 분들은 앱을 다운로드하여 사용해 주시면 정말 감사하겠습니다.