개요
이전 글 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>
동작 원리
getStaticPaths()에서 해당 언어(ko)의 공개된 포스트를 모두 가져옵니다- 각 포스트의
permalink에서 앞뒤/를 제거하여params.path로 사용합니다 - 빌드 시 각 포스트에 대해 정적 HTML 파일이 생성됩니다
getLocalizedPath()로 언어 접두사가 포함된 URL을PostLayout에 전달합니다
예를 들어, permalink: /astro/installation/인 한국어 포스트는:
params.path가astro/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을 사용하여 다국어를 구현했습니다. 두 방식의 차이점을 비교합니다.
| 항목 | Jekyll | Astro |
|---|---|---|
| 다국어 방식 | 플러그인 의존 (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].astrocatch-all 라우팅으로 언어별 포스트 페이지 생성- 언어별 Google Fonts 비동기 로딩
- 사이트맵 i18n 설정
다음 글 SEO 구현에서는 검색 엔진 최적화를 위한 메타 태그, 구조화 데이터, 사이트맵 등의 구현 방법을 다룹니다.
시리즈 안내
이 포스트는 Jekyll에서 Astro로 마이그레이션 시리즈의 일부입니다.
- Jekyll에서 Astro로 마이그레이션한 이유
- Astro 설치 및 프로젝트 구성
- Content Collections와 마크다운 마이그레이션
- 다국어(i18n) 구현
- SEO 구현
- 이미지 최적화 — 커스텀 rehype 플러그인
- 댓글 시스템 (Utterances)
- 광고 연동 (Google AdSense)
- Pagefind를 이용한 검색 구현
- 레이아웃과 컴포넌트 아키텍처
- GitHub Pages 배포
- 소셜 공유 자동화 스크립트
- 트러블슈팅과 팁
제 블로그가 도움이 되셨나요? 하단의 댓글을 달아주시면 저에게 큰 힘이 됩니다!
앱 홍보
Deku가 개발한 앱을 한번 사용해보세요.Deku가 개발한 앱은 Flutter로 개발되었습니다.관심있으신 분들은 앱을 다운로드하여 사용해 주시면 정말 감사하겠습니다.