포스트

Next.js + next-intl i18n 삽질 모음

Next.js + next-intl i18n 삽질 모음

BibleCardAI를 한국어/영어 이중 언어로 운영하면서 겪은 i18n 삽질들. next-intl 기반이고, SSO 사이트(blessflow.com)와 서비스 사이트(biblecardai.blessflow.com)가 분리된 구조다.

가장 큰 삽질. 한국어로 쓰고 있다가 카드를 클릭하면 갑자기 영어로 바뀌었다.

1
2
3
4
5
6
// 잘못된 코드
import Link from 'next/link'
<Link href="/gallery?card=123">카드 보기</Link>
// → /gallery?card=123 (locale prefix 없음)
// → next-intl: "prefix 없음 = 기본 locale(en)"
// → 영어로 전환됨
1
2
3
4
// 올바른 코드
import { Link } from '@/i18n/navigation'
<Link href="/gallery?card=123">카드 보기</Link>
// → /ko/gallery?card=123 (현재 locale 유지)

next-intl의 Link는 현재 locale을 읽어서 자동으로 prefix를 붙인다. next/link는 순수 Next.js 라우터라서 locale을 모른다.

규칙: [locale]/ 안의 모든 컴포넌트는 @/i18n/navigation의 Link 사용. next/link[locale] 밖(예: app/not-found.tsx)에서만.

8개 파일에서 이 실수를 발견하고 일괄 교체했다.

2. SSO 간 locale 전달

blessflow.com(SSO)에서 로그인 후 biblecardai.blessflow.com으로 돌아올 때, locale 정보가 사라졌다. 영어로 쓰고 있었는데 로그인하고 오면 한국어.

해결: returnUrl에 locale을 포함시킨다.

1
2
biblecardai → blessflow.com/login?returnUrl=https://biblecardai.blessflow.com/en/gallery&locale=en
blessflow.com → 로그인 후 returnUrl로 리다이렉트 (locale 유지)

blessflow.com 쪽에서 detectLocale(searchParams)로 returnUrl의 /en/ 패턴이나 locale=en 파라미터를 읽어서 로그인 페이지도 영어로 표시한다.

3. 하드코딩 한글

곳곳에 한국어가 하드코딩되어 있었다:

1
2
3
4
5
// 전
<button>구글로 로그인</button>
<span>무료</span>
<span>만나 {count}/3</span>
<span>내일 새벽에 만나가 내려요</span>

영어 사용자가 보면 당황스럽다. 전부 i18n 키로 전환:

1
2
3
4
5
// 후
<button>{t('googleLogin')}</button>
<span>{t('free')}</span>
<span>{t('mannaCount', { count })}</span>
<span>{t('mannaRefresh')}</span>

찾는 법: 브라우저를 영어로 바꾸고 전체 페이지를 돌아다니면서 한글이 보이면 잡는다.

4. API 응답이 locale을 모른다

DB에 verse_reference: "Isaiah 43:19"로 저장되어 있으면, 한국어 사용자한테도 영어 구절명이 보인다. 반대로 한국어로 저장된 말씀 텍스트가 영어 사용자에게 보이기도 한다.

해결:

  • 구절명(verse_reference): 프론트에서 localizeVerseReference() 유틸로 변환. bible-books.json 매핑 테이블 사용.
  • 말씀 본문(bible_verse): 모달 열릴 때 /api/v1/bible/verse/{book}/{chapter}/{verse}?lang=ko|en API 호출. 로딩 중 블러+스피너, 응답 후 표시.

주의: books API 필드명이 english_name이 아니라 name_en이었고, 1Samuel vs 1 Samuel 같은 공백 불일치도 있었다. normalize 함수로 해결.

교훈

가장 위험한 실수는 next/link 그대로 쓴 것이었다. 컴파일 에러도 안 나고 동작도 멀쩡한데 locale만 슬쩍 바뀐다. 그래서 발견이 늦다. [locale]/ 안에서는 무조건 @/i18n/navigation의 Link를 쓰는 걸 규칙으로 잡았다.

i18n은 처음부터 깔고 가는 게 맞다. 나중에 넣으면 하드코딩 찾아다니느라 시간이 배로 든다. SSO처럼 도메인이 갈라져 있으면 locale은 URL 파라미터로 명시적으로 넘겨야 한다. DB에는 정규화된 형태로 저장하고 표시 시점에 locale로 변환한다는 원칙도 같이 정해뒀다 — 데이터와 표시를 섞으면 나중에 둘 다 망가진다.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.