목차

Acecore 공식 웹사이트를 일본어 전용에서 9개 언어 지원으로 업그레이드했습니다. 최초 도입 시 블로그 글 21개를 8개 언어로 번역해 168개 파일을 만들었습니다. 2026년 7월 29일 기준 저장소에는 일본어 글 29개와 번역 208개, 총 237개 글 파일이 있으며 빌드는 652개 페이지를 생성합니다. 번역 파일이 실제로 있는 locale에만 글 URL을 공개합니다.
다국어 전략
범위 정의
다국어 지원은 세 단계로 진행했습니다:
- i18n 기반 구축: Astro 내장 i18n 라우팅 설정, 번역 유틸리티, 9개 언어의 번역 JSON 파일
- UI 텍스트 번역: 헤더, 푸터, 사이드바 및 모든 페이지의 컴포넌트 텍스트
- 블로그 글 번역: 최초 도입 시 21개 글을 8개 언어로 번역(168개 파일 생성)
URL 설계
Astro의 prefixDefaultLocale: false를 채택하여 일본어는 루트 경로(/blog/...), 다른 언어는 접두사 포함(/en/blog/..., /zh-cn/blog/... 등)으로 제공합니다.
# 일본어(기본)
/blog/astro-performance-tuning/
# 영어
/en/blog/astro-performance-tuning/
# 중국어(간체)
/zh-cn/blog/astro-performance-tuning/
모든 언어에서 동일한 slug를 사용하여 언어 전환 시 URL 매핑을 단순하게 유지합니다.
i18n 기반 구현
Astro i18n 설정
astro.config.mjs에서 i18n 라우팅을 설정합니다.
// astro.config.mjs
export default defineConfig({
i18n: {
defaultLocale: 'ja',
locales: ['ja', 'en', 'zh-cn', 'es', 'pt', 'fr', 'ko', 'de', 'ru'],
routing: {
prefixDefaultLocale: false,
},
},
})
번역 유틸리티
설정 파일, 유틸리티 함수, 번역 JSON 파일은 src/i18n/에 통합 관리합니다.
// src/i18n/utils.ts
export function t(locale: Locale, key: string): string {
return translations[locale]?.[key] ?? translations[defaultLocale][key] ?? key
}
번역 파일은 src/i18n/locales/ 아래에 JSON 형식으로 저장되며, 네비게이션, 푸터, 블로그 UI, 메타데이터 등 약 100개의 키를 관리합니다.
View 컴포넌트 패턴
페이지 구현은 View 컴포넌트 패턴을 사용합니다. 레이아웃과 로직은 src/views/에 집중하고, 라우트 파일(src/pages/)은 locale만 전달하는 가벼운 래퍼입니다.
---
// src/pages/[locale]/about.astro (라우트 파일)
import AboutPage from '../../views/AboutPage.astro'
const { locale } = Astro.params
---
<AboutPage locale={locale} />
이 설계로 일본어 라우트(/about)와 다국어 라우트(/en/about) 사이의 로직 중복을 제거합니다.
블로그 콘텐츠 다국어 지원
디렉토리 구조
번역 글은 언어 코드 하위 디렉토리에 배치합니다. Astro의 glob 로더가 **/*.md 패턴으로 자동 재귀 감지합니다.
src/content/blog/
astro-performance-tuning.md # 일본어(기본)
website-renewal.md
en/
astro-performance-tuning.md # 영어 버전
website-renewal.md
zh-cn/
astro-performance-tuning.md # 중국어(간체) 버전
website-renewal.md
es/
...
콘텐츠 해석 유틸리티
src/utils/blog-i18n.ts에 3개의 함수를 구현했습니다.
// 기본 글 여부 판별(ID에 슬래시 없음 = 기본)
export function isBasePost(post: CollectionEntry<'blog'>): boolean {
return !post.id.includes('/')
}
// ID에서 locale 접두사를 제거하여 기본 slug 획득
export function getBaseSlug(postId: string): string {
const idx = postId.indexOf('/')
return idx !== -1 ? postId.slice(idx + 1) : postId
}
// 기본 글의 로컬라이즈 버전 취득(없으면 원본으로 폴백)
export function localizePost(
post: CollectionEntry<'blog'>,
allPosts: CollectionEntry<'blog'>[],
locale: Locale,
): CollectionEntry<'blog'> {
if (locale === defaultLocale) return post
return allPosts.find((p) => p.id === `${locale}/${post.id}`) ?? post
}
localizePost() 자체는 안전장치로 원문을 반환하지만, 공개 라우트와 목록은 isPostAvailableInLocale() 및 필터를 사용해 실제 번역만 선택합니다. 번역이 없으면 로컬라이즈된 글 URL을 생성하지 않습니다.
핵심은 기존 콘텐츠 컬렉션 스키마를 수정하지 않는 것입니다. Astro의 glob 로더가 하위 디렉토리의 파일을 자동으로 인식하여 en/astro-performance-tuning 같은 ID를 생성하므로 설정 변경이 불필요합니다.
번역 파일 규칙
번역 파일은 다음 규칙에 따라 생성합니다:
- frontmatter 키는 영어 유지(
title,description,date등) - 태그 값은 일본어 유지(
['技術', 'Astro']등) - URL, 이미지 경로, 코드 블록, HTML은 수정하지 않음
- 날짜와 저자는 변경하지 않음
- 본문 텍스트와 frontmatter 텍스트 값(title, description, callout, FAQ 등)을 번역
번역 워크플로우
번역 프로세스는 다음 순서로 진행합니다:
- 영어를 중간 언어로 생성: 일본어 원문에서 영어로 번역
- 영어에서 각 언어로 번역: 영어에서 7개 언어로 확장
- 배치 처리: GitHub Copilot으로 한 번에 5~6개 글 처리
일본어 → 영어 → 대상 언어의 2단계 번역으로 품질 편차를 줄입니다. 영어를 중간 언어로 경유하면 일본어에서 각 언어로 직접 번역하는 것보다 안정적인 품질을 얻을 수 있습니다.
다국어 View 컴포넌트
BlogPostPage 구현
블로그 글 페이지는 localizePost()로 locale 버전의 콘텐츠를 가져와 템플릿 변수에 할당합니다.
---
// src/views/BlogPostPage.astro
const localizedPost = localizePost(basePost, allPosts, locale)
const post = localizedPost // 기존 템플릿 참조가 그대로 동작
---
이 방식으로 템플릿의 post.data.title이나 post.body 참조를 변경하지 않고 다국어 지원을 구현할 수 있습니다.
목록 페이지 구현
블로그 목록, 태그 목록, 저자 목록, 아카이브 페이지는 isBasePost()로 기본 글만 필터링한 뒤, 표시 시 localizePost()로 번역 버전으로 교체합니다.
---
const allPosts = await getCollection('blog')
const basePosts = allPosts.filter(isBasePost)
const displayPosts = basePosts.map((p) => localizePost(p, allPosts, locale))
---
빌드 시 주의사항
YAML frontmatter 이스케이프
프랑스어 번역에서 아포스트로피(l'atelier, qu'on 등)가 YAML 작은따옴표와 충돌하는 문제가 발생했습니다.
# NG: YAML 파싱 에러
title: 'Le métavers est plus proche qu'on ne le pense'
# OK: 큰따옴표로 변경
title: "Le métavers est plus proche qu'on ne le pense"
Node.js 스크립트로 모든 파일을 일괄 수정했습니다. Acecore's 같은 영어 텍스트도 같은 문제가 있으므로 번역 파일 생성 시 따옴표 유형에 주의가 필요합니다.
OG 이미지 라우트 필터링
/blog/og/[slug].png.ts에서 번역 글의 slug(en/aceserver-hijacked 등)까지 캡처하여 파라미터 에러가 발생했습니다. isBasePost() 필터링으로 해결했습니다.
export const getStaticPaths: GetStaticPaths = async () => {
const allPosts = await getCollection('blog')
const posts = allPosts.filter(isBasePost)
return posts.map((post) => ({
params: { slug: post.id },
props: { title: post.data.title },
}))
}
Pages CMS 다국어 지원
Pages CMS(.pages.yml)는 지정된 path 디렉토리 바로 아래의 파일만 대상으로 하므로, 번역 하위 디렉토리를 개별 컬렉션으로 등록했습니다.
content:
- name: blog
label: ブログ(日本語)
path: src/content/blog
- name: blog-en
label: Blog(English)
path: src/content/blog/en
- name: blog-zh-cn
label: 博客(简体中文)
path: src/content/blog/zh-cn
# ... 각 언어별 설정
라벨은 각 언어로 작성하여 CMS에서 어떤 컬렉션이 어떤 언어인지 한눈에 알 수 있게 합니다.
언어 전환 UI
헤더에 LanguageSwitcher 컴포넌트를 추가하여 데스크톱과 모바일 모두에서 언어 전환 UI를 제공합니다. 언어 전환 시 같은 페이지의 해당 locale로 이동하며, 첫 방문 시 브라우저의 navigator.language를 감지하여 자동 리다이렉트합니다.
태그 다국어 표시
글의 태그는 URL에서 일본어 slug를 유지하면서 표시 이름만 번역합니다. 이로써 라우팅 복잡성을 피하면서 사용자의 모국어로 태그를 표시합니다.
태그 정의는 src/content/tags/{tagId}.json에 집약되며, 각 태그는 i18n.name 필드를 가집니다. 이로써 태그 번역의 source of truth가 번역 JSON이 아닌 태그 collection 쪽으로 이동됩니다.
{
"id": "technology",
"name": "技術",
"i18n": {
"en": { "name": "Technology" },
"fr": { "name": "Technologie" }
}
}
글 카드, 사이드바, 태그 목록, 글 상세 페이지에서는 tags collection을 참조하여 표시 이름을 전환하며, 태그 표시가 모두 해당 언어로 통일됩니다.
저자 데이터 다국어 지원
저자 이름, 약력(bio), 스킬 목록도 언어별로 전환됩니다. src/content/authors/{authorId}.json에 i18n 필드를 추가하여 각 언어의 번역을 저장합니다.
{
"id": "hatt",
"name": "ハット",
"bio": "代表取締役。Web制作・サーバー運用…",
"skills": ["TypeScript", "Astro", "..."]
"i18n": {
"en": {
"name": "Hatt",
"bio": "CEO and representative director. Web development...",
"skills": ["TypeScript", "Astro", "..."]
}
}
}
getLocalizedAuthor() 유틸리티가 locale에 맞는 저자 정보를 가져옵니다.
// src/utils/blog-i18n.ts
export function getLocalizedAuthor(author: Author, locale: Locale) {
const localized = author.i18n?.[locale]
return localized ? { ...author, ...localized } : author
}
다국어 사이트 SEO
다국어 지원의 SEO 효과를 극대화하기 위해 검색 엔진이 각 언어 버전을 올바르게 인식하고 색인할 수 있는 메커니즘을 구현했습니다.
사이트맵 hreflang 지원
@astrojs/sitemap의 i18n 옵션과 번역 파일 존재 여부를 확인하는 필터를 함께 사용합니다. sitemap에는 실제 언어 버전만 포함하고 해당 xhtml:link rel="alternate" 태그를 자동 출력합니다.
// astro.config.mjs
sitemap({
filter(page) {
return !isMissingLocalizedBlogPost(page)
},
i18n: {
defaultLocale: 'ja',
locales: {
ja: 'ja',
en: 'en',
'zh-cn': 'zh-CN',
es: 'es',
pt: 'pt',
fr: 'fr',
ko: 'ko',
de: 'de',
ru: 'ru',
},
},
})
9개 언어가 모두 있는 글에는 9개 언어의 hreflang 클러스터를 출력합니다. 일본어만 있는 글은 존재하지 않는 현지화 URL을 연결하지 않고 독립된 일본어 sitemap 항목으로 유지합니다.
JSON-LD 구조화 데이터 언어 지원
블로그 글의 BlogPosting 구조화 데이터에 inLanguage 필드를 추가하여 검색 엔진에 각 글의 언어 정보를 전달합니다.
// BlogPostPage.astro (JSON-LD 발췌)
{
"@type": "BlogPosting",
"inLanguage": htmlLangMap[locale], // "ja", "en", "zh-CN" 등
"headline": post.data.title,
// ...
}
다국어 RSS 피드
일본어 /rss.xml 외에 각 언어 버전의 RSS 피드(/en/rss.xml, /zh-cn/rss.xml 등)를 생성합니다. 피드 제목과 설명도 언어별로 번역되며, <language> 태그는 BCP47 준수 언어 코드를 출력합니다.
// src/pages/[locale]/rss.xml.ts
export const getStaticPaths = () =>
locales
.filter((l) => l !== defaultLocale)
.map((l) => ({ params: { locale: l } }))
BaseLayout.astro의 <link rel="alternate" type="application/rss+xml">도 locale에 맞는 RSS URL을 자동으로 설정합니다.
정리
현재 사이트는 Astro 7.1.3의 내장 i18n 기능을 사용해 다국어 정적 출력을 생성합니다.
- i18n 기반: Astro의
prefixDefaultLocale: false로 일본어 접두사 없음 - UI 번역: View 컴포넌트 패턴으로 로직 중복 제로
- 콘텐츠 번역: 하위 디렉토리 방식으로 스키마 변경 없음
- 태그 번역: URL은 일본어 slug, 표시 이름은 언어별 번역
- 저자 데이터 번역: bio와 skills가 언어별로 전환
- SEO: 사이트맵 hreflang, JSON-LD
inLanguage, 다국어 RSS 피드 - 번역 누락: 로컬라이즈된 글 URL은 생성하지 않고 일본어 글은 원래 URL에서 계속 공개
- CMS 지원: Pages CMS에서 각 언어의 글을 개별 편집 가능
번역 파일은 앞으로도 점진적으로 추가합니다. 번역이 생기기 전에는 일본어 글만 공개하며, locale 파일을 추가하면 해당 언어의 글 URL, sitemap 항목, hreflang 관계가 활성화됩니다.
다국어 워크플로우
i18n 기반 구축
Astro 내장 i18n 라우팅과 번역 유틸리티 설정.
UI 텍스트 번역
헤더, 푸터 및 모든 컴포넌트의 표시 텍스트 번역.
블로그 글 번역
최초 도입 시 168개 번역 파일 생성(21개 글 × 8개 언어).
CMS 및 빌드 검증
Pages CMS 다국어 설정 및 전체 페이지 빌드 검증.
일본어만
- 일본어 1개 언어만
- 블로그 글 23개
- 523개 페이지 생성(UI 다국어 지원 후)
- Pages CMS 블로그 컬렉션 1개
- 태그 및 저자 데이터 일본어만
- RSS 피드 1개
9개 언어(최초 도입 시)
- 일본어 + 8개 언어(en, zh-cn, es, pt, fr, ko, de, ru)
- 블로그 글 23개 + 번역 168개 = 총 191개
- 최초 도입 시 621개 페이지 생성
- Pages CMS 언어별 컬렉션 9개
- 25종 태그 및 저자 데이터를 각 언어로 번역
- 다국어 RSS 피드(9개 언어)
왜 9개 언어를 선택했나요?
번역 품질은 어떻게 보장하나요?
번역 글이 없으면 어떻게 되나요?
새 글을 추가할 때 번역이 필요한가요?
댓글
Gui
Acecore 대표. 업무 시스템, 웹, DB/인프라, 품질, AI 활용을 사업 과제 정리부터 설계, 도입 후 개선까지 이어지는 흐름으로 추진합니다. C#/.NET 기반의 실무 구현력을 바탕으로 PHP/JavaScript, SQL Server/PostgreSQL/MySQL, Linux/Windows Server도 고려하며, 요구사항 정리, 기술 선택, 품질 기준, GitHub 기반 개발 운영을 하나의 흐름으로 설계합니다. 생성형 AI는 개발, 검증, 정보 정리 등 업무 프로세스에 도입해 소규모 팀도 빠르고 확실하게 성과를 낼 수 있는 실무 기반으로 활용하고 있습니다.
관련 기사
Astro + Cloudflare 사이트를 기능별로 확장하는 전체 설계2026년 6월 7일 오후 07:00
Cloudflare만으로 Astro 블로그 댓글 기능 만들기2026년 6월 7일 오후 06:00
Astro 사이트 품질 개선 가이드 속편 - PageSpeed Insights 전 항목 100점을 만든 최종 조정2026년 3월 29일 오전 02:30