i18n으로 글로벌 언어 처리.
masonfe-hyunsu
🌐 1. 개요.

언어가 하나일 때는 컴포넌트에 문구를 그대로 넣어도 큰 문제가 없다.
두세 개로 늘어나면 같은 화면을 복제하거나,
if (lang === 'ko')같은 분기가 금방 지저분해진다.그래서 UI 문구는 키로 빼고, 현재 언어에 맞는 값만 읽어 오는 쪽이 기본이 된다.
이 포스팅에서는 i18n(internationalization, 여러 언어를 받을 수 있게 앱 구조를 짜는 일)이 무엇인지, react-i18next로 어떻게 쓰는지, 실무에서 자주 막히는 지점은 어디인지 정리해 본다.
🔤 2. i18n과 l10n.
구분 | i18n | l10n |
|---|---|---|
풀네임 | internationalization | localization |
의미 | 다국어를 받을 수 있는 구조 | 특정 언어, 지역에 맞게 실제로 맞추는 일 |
예 | 키 분리, locale 전환, 포맷 API | ko/en 번역문, 날짜 표기, 통화 |
i18n은 뼈대, l10n은 그 위에 얹는 실제 번역과 지역화다.
프론트 작업에서는 둘을 한 세트로 다루는 경우가 많다. 키를 나누고, 번역을 채우고, 날짜 포맷까지 같이 맞춘다.
이때 기준이 되는 값이 locale(언어+지역 식별자, 예:
ko-KR,en-US)이다. 같은 영문이라도en-US와en-GB는 표기가 달라질 수 있다.
> 파일을 많이 만드는 일보다, 문장을 어떤 단위로 키에 담을지가 더 중요하다.
🧩 3. 기본 흐름.
번역 문구는 JSON 같은 리소스에 두고, 컴포넌트는 키만 호출한다.

{
"home": {
"title": "Welcome",
"greeting": "Hello, {{name}}"
}
}import { useTranslation } from 'react-i18next';
export default function HomeHero() {
const { t } = useTranslation();
return <h1>{t('home.title')}</h1>;
}t('home.title')은 현재 언어 기준으로home.title키의 문자열을 돌려준다.이름처럼 바뀌는 값은 interpolation(문장 안 치환)으로 넣는다. 예:
t('home.greeting', { name })i18n 라이브러리는 여러 가지가 있다. 아래부터는 실무에서 자주 쓰는 i18next와 react-i18next 기준으로 본다.
⚙️ 4. 설정과 사용.
최소 설정은 "어떤 언어 리소스를 쓸지"와 "없을 때 어디로 떨어질지"만 정해 주면 된다.
초기화.
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import en from './locales/en.json';
import ko from './locales/ko.json';
i18n.use(initReactI18next).init({
resources: {
en: { translation: en },
ko: { translation: ko },
},
lng: 'en',
fallbackLng: 'en',
interpolation: { escapeValue: false }, // React가 XSS를 이미 처리하는 경우
});lng: 처음 쓸 언어fallbackLng: 키가 비었을 때 대신 볼 언어resources: 언어별 번역 묶음
언어 전환.
const { t, i18n } = useTranslation();
void i18n.changeLanguage('ko');언어만 바꾸면 같은
t()호출이 다른 문구를 돌려준다.문제는 전환 자체보다 초기 언어를 어디서 정하느냐다. 브라우저 설정, URL(
/ko/...), 쿠키 중 하나를 단일 소스로 두는 편이 덜 꼬인다.Next처럼 SSR을 쓰면 서버와 클라이언트의 초기 언어가 달라질 수 있다. 어긋나면 hydration(서버 HTML과 클라이언트 첫 렌더를 맞추는 과정) 경고로 이어진다.
네임스페이스.
화면이 커지면
common,home,checkout처럼 namespace(번역 키 묶음)로 나눈다.첫 번들에 전부를 넣지 않고 라우트 단위로 불러오면, 초기 용량을 줄일 수 있다.
⚠️ 5. 유의점.
번역 파일을 채우는 단계보다, 문장 설계와 locale, 라우트에서 더 자주 막힌다. 그 지점만 짧게 정리한다.

문장을 조각내지 않기.
// 나쁜 예: 어순이 언어마다 달라서 깨진다
<p>{t('welcome.hello')} {userName} {t('welcome.suffix')}</p>
// 나은 예: 문장 단위 키 + interpolation
<p>{t('welcome.full', { name: userName })}</p>영어는
Hello, {{name}}, 한국어는{{name}}님, 안녕하세요처럼 어순이 다르다.컴포넌트에서 문자열을
+로 이어 붙이면, 번역자가 문장 전체를 고칠 수 없다.
복수형, 성별, 분기.
1 item/2 items처럼 개수에 따라 형태가 바뀌는 언어가 많다.그래서
count === 1 ? t('one') : t('many')를 JSX에 직접 쓰기보다, i18next plural이나 ICU MessageFormat(복수, 선택, 성별을 문장 규칙으로 표현하는 포맷) 쪽이 낫다.언어가 늘수록 컴포넌트 분기는 점점 감당이 안 된다.
날짜, 숫자, 통화.
문구 번역과 표기 포맷은 따로 다루는 편이 맞다.
Intl.DateTimeFormat,Intl.NumberFormat에 locale을 넘기고,"2026.07.25"같은 고정 문자열은 피한다.
CJK 타이포.
CJK(Chinese, Japanese, Korean을 묶는 말) 중에서도, 줄바꿈은 일본어와 중국어에서 더 자주 이슈가 된다.
word-break전략이 언어마다 달라질 수 있다.그래서 언어 코드만 보고 스타일을 살짝 분기하는 패턴을 쓰기도 한다.
const isCJK = i18n.language === 'ja' || i18n.language === 'zh';라우트와 SEO.
URL에 locale을 넣을지(
/en/about), 서브도메인이나 도메인으로 나눌지는 초기에 정한다.검색 노출이 필요하면 hreflang(언어, 지역별 대응 페이지 표시)과 canonical을 같이 맞춘다.
번역문만 바꾸고 URL과 메타를 그대로 두면, 언어별 페이지가 검색에서 섞이기 쉽다.
키 설계.
button.ok보다checkout.submit처럼 화면과 의도가 보이는 키가 나중에 찾기 쉽다.같은 "확인"이라도 맥락이 다르면 키를 나누는 편이 덜 아프다.
✅ 6. 체크리스트.
[ ] UI에 하드코딩된 문구를 키로 분리했는가
[ ] 초기
lng와fallbackLng의 출처가 명확한가 (URL / 설정 / 브라우저)[ ] 문장 단위 키와 interpolation을 쓰는가 (어순을 쪼개지 않는가)
[ ] 복수형, 날짜, 숫자 locale을 문구 연결과 분리했는가
[ ] SSR이라면 서버와 클라이언트 언어가 hydration에서 맞는가
[ ] 필요하면 namespace, hreflang, 라우트 전략을 정했는가
댓글