[Astro] 레이아웃과 컴포넌트 아키텍처

2026-04-02 hit count image

Astro 블로그의 레이아웃과 컴포넌트 구조를 공유합니다. BaseLayout, PostLayout, Navbar, Footer 등 핵심 컴포넌트의 설계와 동적 라우팅을 다룹니다.

astro

개요

이전 글 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를 사용하여 표시합니다. 별도의 백엔드 없이 외부 서비스로 간단하게 구현할 수 있습니다.

주요 컴포넌트 소개

블로그에서 사용하는 주요 컴포넌트들을 간단히 소개합니다.

네비게이션 바와 언어 전환 기능을 제공하는 컴포넌트입니다. 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

포스트 목록(최신글 페이지 등)에서 각 포스트를 카드 형태로 표시하는 컴포넌트입니다. 제목, 설명, 날짜, 카테고리 정보를 포함합니다.

현재 페이지의 위치를 계층 구조로 보여주는 네비게이션 컴포넌트입니다. 사용자가 상위 페이지로 쉽게 이동할 수 있도록 도와줍니다.

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.pathastro/installation이 되어 최종 URL이 /ko/astro/installation/으로 생성됩니다. 일본어는 src/pages/[...path].astro, 영어는 src/pages/en/[...path].astro에서 동일한 구조로 처리합니다.

Jekyll Liquid 템플릿과의 비교

Jekyll에서 Astro로 옮기면서 템플릿 작성 방식이 완전히 달라졌습니다. 아래 표로 주요 차이점을 비교합니다.

항목Jekyll (Liquid)Astro
레이아웃 상속layout: post frontmatterimport<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.titlegetCollection(), Astro.props

개인적으로 가장 만족스러운 부분은 TypeScript 타입 지원입니다. Jekyll에서는 Props에 오타가 있어도 아무런 경고 없이 빈 값이 출력되었지만, Astro에서는 interface Props로 타입을 정의하면 IDE에서 자동 완성과 타입 체크를 받을 수 있습니다. 컴포넌트가 늘어날수록 이 차이가 크게 느껴집니다.

완료

이번 글에서는 Astro 블로그의 레이아웃과 컴포넌트 아키텍처를 살펴보았습니다.

  • BaseLayout.astro: Head, Navbar, Footer를 감싸는 최상위 레이아웃
  • PostLayout.astro: 히어로 헤더, 광고, 관련 포스트, 댓글 등을 포함하는 포스트 전용 레이아웃
  • Navbar, Breadcrumbs, CategoryCard, PostCard, SponsorButtons, AppPromo 등 다양한 컴포넌트
  • [...path].astro catch-all 동적 라우팅으로 포스트 페이지 자동 생성
  • Jekyll Liquid 템플릿 대비 TypeScript 타입 안전성과 직관적인 JSX 문법의 장점

다음 글 GitHub Pages 배포에서는 빌드된 사이트를 GitHub Pages에 배포하는 방법을 다룹니다.

시리즈 안내

이 포스트는 Jekyll에서 Astro로 마이그레이션 시리즈의 일부입니다.

  1. Jekyll에서 Astro로 마이그레이션한 이유
  2. Astro 설치 및 프로젝트 구성
  3. Content Collections와 마크다운 마이그레이션
  4. 다국어(i18n) 구현
  5. SEO 구현
  6. 이미지 최적화 — 커스텀 rehype 플러그인
  7. 댓글 시스템 (Utterances)
  8. 광고 연동 (Google AdSense)
  9. Pagefind를 이용한 검색 구현
  10. 레이아웃과 컴포넌트 아키텍처
  11. GitHub Pages 배포
  12. 소셜 공유 자동화 스크립트
  13. 트러블슈팅과 팁

제 블로그가 도움이 되셨나요? 하단의 댓글을 달아주시면 저에게 큰 힘이 됩니다!

앱 홍보

책 홍보

블로그를 운영하면서 좋은 기회가 생겨 책을 출판하게 되었습니다.

아래 링크를 통해 제가 쓴 책을 구매하실 수 있습니다.
많은 분들에게 도움이 되면 좋겠네요.



SHARE
Twitter Facebook RSS