mason
mason-log.

i18n으로 글로벌 언어 처리.

mason

masonfe-hyunsu

🌐 1. 개요.

i18n 배너
  • 언어가 하나일 때는 컴포넌트에 문구를 그대로 넣어도 큰 문제가 없다.

  • 두세 개로 늘어나면 같은 화면을 복제하거나, 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-USen-GB는 표기가 달라질 수 있다.

> 파일을 많이 만드는 일보다, 문장을 어떤 단위로 키에 담을지가 더 중요하다.


🧩 3. 기본 흐름.

번역 문구는 JSON 같은 리소스에 두고, 컴포넌트는 키만 호출한다.

i18n 흐름
{
  "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 라이브러리는 여러 가지가 있다. 아래부터는 실무에서 자주 쓰는 i18nextreact-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에 하드코딩된 문구를 키로 분리했는가

  • [ ] 초기 lngfallbackLng의 출처가 명확한가 (URL / 설정 / 브라우저)

  • [ ] 문장 단위 키와 interpolation을 쓰는가 (어순을 쪼개지 않는가)

  • [ ] 복수형, 날짜, 숫자 locale을 문구 연결과 분리했는가

  • [ ] SSR이라면 서버와 클라이언트 언어가 hydration에서 맞는가

  • [ ] 필요하면 namespace, hreflang, 라우트 전략을 정했는가


🔗 7. 참고 자료.

이전 글
렌더링 타입 (CSR, SSR, SSG, ISR).

댓글

불러오는 중...
목록으로