다국어 지원 ‐ react‐i18next - study-pals/frontend GitHub Wiki
UI의 텍스트를 코드 내에 하드코딩하면 유지보수성 및 확장성이 저해된다. 스크립트를 별도로 분리하여 한 곳에서 관리하는 것이 유지보수적으로 좋다.
이를 위한 구조를 작성하는 김에, 다국어 지원도 가능하도록 구현해보고자 했다.
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
언어 전환 시 리렌더링
onI18nChanged
나setLanguage
호출 후에는 해당 훅/컴포넌트를 구독한 부분만 리렌더링되는데, 네임스페이스를 세분화해 필요한 화면에서만 로드하도록 구성하면 전체 페이지에 미치는 리렌더링 비용을 최소화할 수 있습니다. 반면 여러 중첩된 HOC를 남용하면 번역 파일 로드 완료 시점에 수초 단위의 지연이 발생할 수 있으므로 주의가 필요합니다. github.com메모리 및 네트워크
레이지 로딩 설정 시 네트워크 요청은 최초 한 번만 발생하고, 로드된 리소스는 내부 캐싱 메커니즘으로 재사용됩니다. 메모리 점유 또한 JSON 객체 형태로 관리되기 때문에 추가 부담이 크지 않습니다. buttercms.com
위 포스팅을 참고했다.
아래와 같이 코드를 작성한 후, 앱의 진입점에서 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하게 사용할 수 있도록 키값에 대한 타입 정의를 추가했다.
현재 내가 작성한 코드에서 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>;
};
윈도우에서 위와 같은 에러가 떴다. 원인은 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해주니 해결되었다.