Next.js 15 App Router + next-intl v4로 ko/en/ja 3개 언어 운영기
graxel.ai 자체를 한국어/영어/일본어 3개 언어로 운영하면서 배운 로케일 라우팅, SEO, hreflang 실전 패턴을 정리합니다.
graxel.ai는 한국어 기본에 영어와 일본어를 함께 제공하는 3개 언어 사이트입니다. Next.js 15 App Router와 next-intl v4로 이 구성을 몇 달간 돌려보면서 생긴 의외의 함정과 해결책을 정리합니다.
라우팅 — as-needed 패턴의 의미
next-intl v4의 localePrefix: "as-needed"는 기본 언어에만 prefix를 생략해주는 옵션입니다. 우리는 한국어가 기본이라 /about은 한국어, /en/about은 영어, /ja/about은 일본어로 매핑됩니다. 단순해 보이지만 미들웨어와 SEO 메타에서 여러 번 실수할 수 있는 지점이었습니다.
- 미들웨어는 next-intl 라우팅을 먼저 돌리고, 그 다음에 인증 가드가 동작해야 한다. 순서를 바꾸면
/en/dashboard가 로케일 판별 전에 보호로 막혀서 엉뚱한 리다이렉트가 발생한다. hreflang은 세 언어 모두를 절대 URL로 선언해야 한다. 상대 경로로 넣으면 Google Search Console에서 경고가 뜬다.
루트 레이아웃의 책임 분리
src/app/layout.tsx는 CSP 헤더, 폰트(Pretendard), GA4만 담당하고, src/app/[locale]/layout.tsx가 테마, NextIntlClientProvider, 헤더/푸터/쿠키 배너를 감쌉니다. 이 분리 덕분에 로케일마다 다른 메시지 번들을 로드하면서도 글로벌 리소스는 한 번만 내려갈 수 있었습니다. Next.js 레이아웃 공식 문서의 nested layout 원칙을 그대로 따른 구조입니다.
번들 크기 — server-only 네임스페이스 제거
next-intl은 기본적으로 모든 메시지를 클라이언트 프로바이더에 넘길 수 있습니다. 그러나 "관리자 페이지에서만 쓰는 번역"까지 일반 방문자의 HTML에 들어가면 낭비입니다. 그래서 레이아웃 단에서 서버 전용 네임스페이스를 미리 걸러냅니다.
const SERVER_ONLY_NAMESPACES = new Set(['admin', 'internal']);
const clientMessages = Object.fromEntries(
Object.entries(messages).filter(([ns]) => !SERVER_ONLY_NAMESPACES.has(ns)),
);
체감 상 초기 HTML이 15~20% 정도 줄었습니다. 사용자 입장에서는 무의미한 한일 번역 문자열을 다운로드하지 않아도 되니 합리적입니다.
SEO 메타 — createMetadata 헬퍼
각 페이지에서 수동으로 <head>를 짜지 않도록 @saas-factory/seo의 createMetadata() 헬퍼를 표준으로 삼았습니다. 이 헬퍼는 title, description, hreflang, Open Graph, JSON-LD를 한 번에 생성합니다. 특히 hreflang 자동 계산이 유용합니다.
export async function generateMetadata({ params }) {
const { locale } = await params;
const t = await getTranslations({ locale, namespace: 'about' });
return createMetadata({
locale,
path: '/about',
title: t('title'),
description: t('description'),
});
}
언어별 실전 이슈
- 일본어 줄바꿈 — "お問い合わせはこちら" 같은 긴 일본어 문장은 모바일에서 부자연스럽게 잘린다.
word-break: keep-all과overflow-wrap: anywhere의 조합이 안전했다. - 영어 숫자 포맷 — "1,000원"은 그대로 두고 싶지만 "1000 KRW"로 바꾸는 것이 더 자연스럽다.
Intl.NumberFormat을 로케일별로 분기했다. - 이미지 alt — 3개 언어 전부에서 의미가 통해야 하므로 추상적이고 짧게 유지하는 규칙을 팀 내부 가이드로 정리했다.
무엇을 얻었나
가장 큰 수확은 "번역 파일을 실수로 빠뜨렸을 때 런타임이 아니라 빌드 타임에 감지된다"는 점이었습니다. TypeScript의 엄격 모드와 next-intl의 타입 추론이 맞물려 키가 하나라도 빠지면 pnpm build가 실패합니다. 이게 언어를 세 개로 늘리면서도 안심할 수 있는 가장 큰 이유였습니다.
i18n 전략 전체 맥락은 소개 페이지에서, 다른 운영기는 GRAXEL 플랫폼 소개에서 이어집니다. 기술적인 질문은 문의로 받고 있습니다.
공유하기
이어 읽으면 좋은 글
같은 주제와 태그를 기준으로 GRAXEL 운영 맥락을 더 깊게 볼 수 있는 글입니다.
1인 개발자 SaaS 모노레포 vs 멀티레포 — Graxel 운영 1년 후 다시 보는 결정
pnpm과 Turborepo로 구축한 모노레포 아키텍처가 1인 개발자에게 정말 정답이었을까요? 1년간의 뼈저린 운영 회고와 실패담.
Cloudflare Pages 무료 티어로 SaaS 시작하기 — 진짜 1년 비용 후기
1인 개발자가 Cloudflare Pages 무료 티어로 1년간 portal, myhyetaek 등 5개 서비스를 운영하며 지출한 실제 비용과 뼈아픈 실패담을 공개합니다.
SaaS Factory 모노레포 운영기 — pnpm + Turbo로 12개 서비스 묶기
pnpm workspaces와 Turborepo로 공용 패키지와 12개 서비스를 동시에 관리하며 얻은 실전 패턴을 공유합니다.