[Astro] 다국어(i18n) 구현

2026-03-27 hit count image

Astro에서 일본어, 한국어, 영어 3개 언어를 지원하는 다국어 시스템을 구현하는 방법을 공유합니다. URL 기반 라우팅, 번역 시스템, 동적 라우팅 등을 다룹니다.

astro

개요

이전 글 Content Collections와 마크다운 마이그레이션에서 콘텐츠 관리 방법을 다루었습니다. 이번 글에서는 이 블로그에서 일본어, 한국어, 영어 3개 언어를 지원하기 위해 구현한 다국어(i18n) 시스템을 상세히 설명합니다.

Astro에는 공식 i18n 라이브러리가 없지만, 파일 기반 라우팅과 간단한 번역 시스템을 조합하면 효과적인 다국어 사이트를 구축할 수 있습니다.

다국어 라우팅 전략

URL 구조

이 블로그는 URL 접두사 기반 라우팅을 사용합니다.

언어URL 패턴예시
일본어 (기본)/path//astro/installation/
한국어/ko/path//ko/astro/installation/
영어/en/path//en/astro/installation/

기본 언어인 일본어는 URL에 언어 접두사가 없고, 한국어와 영어는 각각 /ko/, /en/ 접두사가 붙습니다.

디렉토리 기반 구현

Astro의 파일 기반 라우팅을 활용하여 디렉토리 구조로 다국어를 구현합니다.

src/pages/
├── index.astro              # 일본어 홈
├── [...path].astro          # 일본어 포스트 (catch-all)
├── latest/
│   └── index.astro          # 일본어 최신글
├── search/
│   └── index.astro          # 일본어 검색
├── ko/
│   ├── index.astro          # 한국어 홈
│   ├── [...path].astro      # 한국어 포스트
│   ├── latest/
│   │   └── index.astro      # 한국어 최신글
│   └── search/
│       └── index.astro      # 한국어 검색
└── en/
    ├── index.astro          # 영어 홈
    ├── [...path].astro      # 영어 포스트
    ├── latest/
    │   └── index.astro      # 영어 최신글
    └── search/
        └── index.astro      # 영어 검색

각 언어 디렉토리에 동일한 구조의 페이지 파일을 두어, 언어별로 독립적인 라우팅을 구현합니다.

번역 시스템 구현

UI 텍스트 번역은 src/i18n/translations.ts에서 중앙 집중적으로 관리합니다.

언어 타입 정의

// src/i18n/translations.ts
export const defaultLang = 'ja';
export const languages = { ja: '日本語', ko: '한국어', en: 'English' } as const;
export type Lang = keyof typeof languages;
  • defaultLang: 기본 언어를 일본어로 설정
  • languages: 지원 언어 목록. 언어 전환 UI에서 사용
  • Lang: TypeScript 타입으로 'ja' | 'ko' | 'en'

번역 레코드

export const translations: Record<Lang, Record<string, string>> = {
  ja: {
    'nav.home': 'Home',
    'nav.latest': 'Latest',
    'nav.category': 'Category',
    'latest.title': '最新記事',
    'search.title': '検索',
    'search.placeholder': '検索キーワードを入力...',
    'post.commentEncouragement':
      '私のブログが役に立ちましたか?下にコメントを残してください。',
    'footer.share': 'SHARE',
    'home.title': 'プログラミングでアートを夢見る.',
    // ...
  },
  ko: {
    'nav.home': 'Home',
    'nav.latest': 'Latest',
    'nav.category': 'Category',
    'latest.title': '최신글',
    'search.title': '검색',
    'search.placeholder': '검색어를 입력하세요...',
    'post.commentEncouragement':
      '제 블로그가 도움이 되셨나요? 하단의 댓글을 달아주시면 저에게 큰 힘이 됩니다!',
    'footer.share': 'SHARE',
    'home.title': '프로그래밍으로 예술을 꿈꾼다',
    // ...
  },
  en: {
    // ...
  },
};

키 이름은 섹션.항목 형식의 점 표기법을 사용하여 구조적으로 관리합니다. 이 점 표기법을 활용하면 네비게이션(nav.), 검색(search.), 포스트(post.) 등 영역별로 구분할 수 있습니다.

헬퍼 함수

번역 시스템을 편리하게 사용하기 위한 헬퍼 함수들입니다.

getLangFromUrl — URL에서 언어 감지

export function getLangFromUrl(url: URL): Lang {
  const path = url.pathname;
  if (path.startsWith('/ko/') || path === '/ko') return 'ko';
  if (path.startsWith('/en/') || path === '/en') return 'en';
  return 'ja';
}

URL 경로의 접두사를 확인하여 현재 언어를 판별합니다. /ko/ 또는 /en/으로 시작하지 않으면 기본 언어(일본어)를 반환합니다.

useTranslations — 번역 함수 생성

export function useTranslations(lang: Lang) {
  return function t(key: string): string {
    return translations[lang][key] || translations[defaultLang][key] || key;
  };
}

언어를 받아 번역 함수 t()를 반환하는 클로저 패턴입니다. 해당 언어의 번역이 없으면 기본 언어(일본어)의 번역을 사용하고, 그마저도 없으면 키 자체를 반환합니다.

컴포넌트에서 다음과 같이 사용할 수 있습니다.

---
// 컴포넌트의 frontmatter 영역
import { useTranslations } from '../i18n/translations';
const t = useTranslations('ko');
---

<h1>{t('home.title')}</h1>
<!-- 출력: 프로그래밍으로 예술을 꿈꾼다 -->

getLocalizedPath — 언어별 경로 생성

export function getLocalizedPath(path: string, lang: Lang): string {
  // 기존 언어 접두사 제거
  const cleanPath = path
    .replace(/^\/(ko|en)\//, '/')
    .replace(/^\/(ko|en)$/, '/');
  if (lang === defaultLang) return cleanPath;
  return `/${lang}${cleanPath}`;
}

경로에 언어 접두사를 추가하는 함수입니다. 기본 언어(일본어)에는 접두사를 붙이지 않습니다.

getLocalizedPath('/astro/installation/', 'ja'); // → '/astro/installation/'
getLocalizedPath('/astro/installation/', 'ko'); // → '/ko/astro/installation/'
getLocalizedPath('/astro/installation/', 'en'); // → '/en/astro/installation/'

언어 전환 링크를 생성할 때 유용합니다. 예를 들어, Navbar에서 언어 전환 버튼을 만들 때 현재 페이지의 다른 언어 버전 URL을 생성하는 데 사용합니다.

동적 라우팅과 언어별 페이지

블로그 포스트는 [...path].astro을 사용하여 catch-all 라우팅으로 처리합니다. 각 언어별로 별도의 라우팅 파일을 갖습니다.

한국어 포스트 라우팅

---
// 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>

동작 원리

  1. getStaticPaths()에서 해당 언어(ko)의 공개된 포스트를 모두 가져옵니다
  2. 각 포스트의 permalink에서 앞뒤 /를 제거하여 params.path로 사용합니다
  3. 빌드 시 각 포스트에 대해 정적 HTML 파일이 생성됩니다
  4. getLocalizedPath()로 언어 접두사가 포함된 URL을 PostLayout에 전달합니다

예를 들어, permalink: /astro/installation/인 한국어 포스트는:

  • params.pathastro/installation이 되어
  • 최종 URL이 /ko/astro/installation/이 됩니다

일본어(기본 언어)는 src/pages/[...path].astro에서 처리하며, 영어는 src/pages/en/[...path].astro에서 처리합니다. 구조는 동일하고 lang 값만 다릅니다.

언어별 Google Fonts

각 언어에 적합한 폰트를 로딩하기 위해 Head.astro에서 언어별 Google Fonts를 설정합니다.

---
// Head.astro 일부
const fontUrl = lang === 'ja'
  ? 'https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@400;700&display=swap'
  : lang === 'ko'
    ? 'https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@400;700&display=swap'
    : 'https://fonts.googleapis.com/css2?family=Noto+Sans:wght@400;700&display=swap';
---

<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link rel="stylesheet" href={fontUrl} media="print" onload="this.media='all'" />
  • 일본어: Noto Sans JP
  • 한국어: Noto Sans KR
  • 영어: Noto Sans

media="print" + onload="this.media='all'" 기법으로 폰트를 비동기 로딩하여 렌더링 차단을 방지합니다.

사이트맵 i18n 설정

@astrojs/sitemap 플러그인에서 다음과 같이 설정하여 다국어 사이트맵을 생성합니다.

// astro.config.mjs
sitemap({
  serialize(item) {
    item.lastmod = new Date();
    return item;
  },
  i18n: {
    defaultLocale: 'ja',
    locales: {
      ja: 'ja',
      ko: 'ko',
      en: 'en',
    },
  },
}),

이 설정으로 생성되는 사이트맵에는 hreflang 태그가 포함되어, 검색 엔진이 각 페이지의 언어별 버전을 인식할 수 있습니다.

Jekyll과의 비교

Jekyll에서는 jekyll-multiple-languages-plugin을 사용하여 다국어를 구현했습니다. 두 방식의 차이점을 비교합니다.

항목JekyllAstro
다국어 방식플러그인 의존 (jekyll-multiple-languages-plugin)자체 구현 (파일 기반 라우팅)
번역 파일YAML 파일 (_i18n/ko.yml)TypeScript 파일 (translations.ts)
타입 안전성없음TypeScript 타입 지원
URL 패턴플러그인 설정에 의존디렉토리 구조로 직접 제어
폴백플러그인 기능useTranslations 함수에서 처리
사이트맵별도 설정 필요@astrojs/sitemap i18n 설정

Astro에서는 외부 플러그인 없이 파일 기반 라우팅과 간단한 TypeScript 함수만으로 다국어를 구현할 수 있어, 더 유연하고 유지보수가 용이합니다.

자세한 내용은 이전에 작성한 다국어 지원 플러그인 포스트를 참고하세요.

완료

이번 글에서는 Astro에서 다국어 시스템을 구현하는 방법을 살펴보았습니다.

  • URL 접두사 기반 라우팅으로 3개 언어 지원 (/, /ko/, /en/)
  • translations.ts에서 UI 번역을 중앙 집중 관리
  • getLangFromUrl, useTranslations, getLocalizedPath 헬퍼 함수
  • [...path].astro catch-all 라우팅으로 언어별 포스트 페이지 생성
  • 언어별 Google Fonts 비동기 로딩
  • 사이트맵 i18n 설정

다음 글 SEO 구현에서는 검색 엔진 최적화를 위한 메타 태그, 구조화 데이터, 사이트맵 등의 구현 방법을 다룹니다.

시리즈 안내

이 포스트는 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