다국어 지원 ‐ react‐i18next - study-pals/frontend GitHub Wiki

UI의 텍스트를 코드 내에 하드코딩하면 유지보수성 및 확장성이 저해된다. 스크립트를 별도로 분리하여 한 곳에서 관리하는 것이 유지보수적으로 좋다.

이를 위한 구조를 작성하는 김에, 다국어 지원도 가능하도록 구현해보고자 했다.

react-i18next 라이브러리

https://github.com/i18next/react-i18next

react 환경에서 다국어 지원을 도와주는 가장 유명한 라이브러리이다. (주간 다운로드 수 500만 이상)

런타임 환경에서 동작하지만, 성능에 오버헤드가 크진 않은 듯하다.

GPT o4-mini

  • 번들 크기 오버헤드

    react-i18next를 포함하면 i18next 코어(약 15–20 KB gzipped)와 래퍼 코드가 추가되어 초기 번들에 대략 18–19 KB 정도의 부하가 생깁니다. 이 정도 용량은 모던 웹/모바일 앱에서 체감하기 어려운 수준이며, 네임스페이스 단위로 레이지 로딩을 설정하면 초기 로드 오버헤드는 더욱 줄일 수 있습니다. locize.com

  • 런타임 조회 비용

    번역 키를 해석(t → string)하는 과정은 단순한 해시 조회와 문자열 포맷팅을 수행하므로, 보통 밀리초 이하 수준으로 매우 빠릅니다. Best practice(훅/컨텍스트 사용, 불필요한 HOC 지양)를 따르면 “성능 저하를 걱정할 필요가 거의 없다”는 평가가 많습니다. stackoverflow.com

  • 언어 전환 시 리렌더링

    onI18nChangedsetLanguage 호출 후에는 해당 훅/컴포넌트를 구독한 부분만 리렌더링되는데, 네임스페이스를 세분화해 필요한 화면에서만 로드하도록 구성하면 전체 페이지에 미치는 리렌더링 비용을 최소화할 수 있습니다. 반면 여러 중첩된 HOC를 남용하면 번역 파일 로드 완료 시점에 수초 단위의 지연이 발생할 수 있으므로 주의가 필요합니다. github.com

  • 메모리 및 네트워크

    레이지 로딩 설정 시 네트워크 요청은 최초 한 번만 발생하고, 로드된 리소스는 내부 캐싱 메커니즘으로 재사용됩니다. 메모리 점유 또한 JSON 객체 형태로 관리되기 때문에 추가 부담이 크지 않습니다. buttercms.com

프로젝트 설정하기

React에서 react-i18next 사용하는 방법

위 포스팅을 참고했다.

아래와 같이 코드를 작성한 후, 앱의 진입점에서 import해주면 기본적인 설정은 끝난다.

// src/locales/i18n.ts
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';

import translationEN from './en';
import translationKO from './ko';

export const resources = {
  en: {
    translation: translationEN,
  },
  ko: {
    translation: translationKO,
  },
};

i18n
  .use(initReactI18next)
  .init({
    resources,
    lng: 'ko',
    fallbackLng: 'ko',
    interpolation: {
      escapeValue: false,
    },
  });

export default i18n;
// index.js
import './src/locales/i18n';

...

여기서 translationEN, translationKO 는 번역 텍스트를 가지고 있는 객체이다. 텍스트 데이터는 json으로 관리하고 화면별(tasks, home)로 json 파일을 분리하였다.

// src/locales/ko/index.ts
import { default as tasksKO } from './tasks.json';
import { default as homeKO } from './home.json';

const translationKO = {
  tasks: tasksKO,
  home: homeKO,
};
export default translationKO;
// src/locales/ko/tasks.json
{
  "my-routine": "나의 루틴",
  "daily-routine": "일간 루틴",
  "weekly-routine": "주간 루틴",
  "registered-task-count": "등록된 목표 {{count}}개",
  "goal-time": "목표 시간 {{time}}"
}

컴포넌트로 추상화하기

앱 내에서 라이브러리를 사용하는 방법은 다음과 같다.

const { t } = useTranslation();
return <Text>{t('target-key')}</Text>;

하지만 이대로 사용하면 매 컴포넌트마다 useTranslation 훅을 작성해야 한다. 이를 TText 컴포넌트로 추상화하여 불필요한 보일러플레이트를 없애고자 했다.

// 타입 정의 없이 작성된 TText 컴포넌트
const TText = ({ tKey, values, ...rest }) => {
  const { t } = useTranslation();
  return <Text {...rest}>{t(tKey, values)}</Text>;
};

Type-Safe하게 만들기

앱에 수많은 텍스트가 존재하므로, 프로젝트가 커지면 텍스트에 대한 키값을 관리하기 쉽지 않을 것이다.

따라서 아래 이미지처럼 키값을 type-safe하게 사용할 수 있도록 키값에 대한 타입 정의를 추가했다.

image

현재 내가 작성한 코드에서 resources 객체는 다음과 같은 구조로 정의된다.

resources.{language}.translation.{screen}.{key}

ex) resources.ko.translation.home.welcome

위 구조에서 {screen}.{key} 부분이 사실상의 키값이므로, 이 부분에 대한 타입을 정의하여 TText 컴포넌트에서 사용하였다.

type I18nNamespaces = keyof typeof resources.ko.translation;
type KeysForNS<NS extends I18nNamespaces> = Extract<
  keyof typeof resources.ko.translation[NS], string
>;
export type KeyWithNamespace = { [NS in I18nNamespaces]: `${NS}.${KeysForNS<NS>}` }[I18nNamespaces];
// components/TText
type Describable = string | number | null | undefined;
interface TTextProps extends TextProps {
  tKey: KeyWithNamespace;
  values?: Record<string, Describable>;
}

const TText = ({ tKey, values, ...rest }: TTextProps) => {
  const { t } = useTranslation();
  return <Text {...rest}>{t(tKey, values)}</Text>;
};

Windows에서의 문제 - Property 'Intl' doesn't exist

image

윈도우에서 위와 같은 에러가 떴다. 원인은 RN-Windows환경에서 기본적으로 Intl (국제화 API)가 없기 때문이었고, 에러 로그에서도 Intl polyfill을 사용하라고 알려준다.

// src/locales/polyfill.ts
import 'intl';
import 'intl/locale-data/jsonp/en';
import 'intl/locale-data/jsonp/ko';
import './i18n';

위와 같이 polyfill 코드를 작성한 뒤, 앱의 진입점의 최상단에 import해주니 해결되었다.

⚠️ **GitHub.com Fallback** ⚠️