React Native(Expo) 앱에 한/영 다국어 지원 추가하기 — locale 감지 삽질기
"한국어밖에 안 되는 앱을 스토어에 올릴 수 있을까?"
개인용으로 쓰던 유튜브 플레이리스트 앱을 Play Store에 올리려다 보니, 앱 안의 텍스트가 죄다 한국어였다. 심사 통과도 통과지만, 영어권 사용자 입장에서는 버튼이 뭔지도 모르는 앱을 쓸 이유가 없다.
그래서 간단하게 시스템 언어가 한국어면 한국어, 나머지는 영어로 보여주는 걸 목표로 잡았다. i18next 같은 라이브러리를 붙이기엔 앱이 너무 작았고, 문자열도 20개 남짓이라서 직접 만들기로 했다.
방법은 세 가지가 있었다
React Native에서 시스템 언어를 읽는 방법은 대표적으로 세 가지다.
| 방법 | 패키지 | 문제 |
|---|---|---|
expo-localization | expo-localization | 네이티브 빌드에서 링크 실패 |
NativeModules.I18nManager | 없음 (RN 내장) | Android에서 undefined 반환 |
Intl.DateTimeFormat() | 없음 (Hermes 내장) | 없음 ✓ |
결론부터 말하면 세 번째인 Intl API가 정답이었다. 하지만 첫 번째, 두 번째에서 삽질한 내용이 더 도움이 될 것 같아서 순서대로 정리했다.
Step 1. expo-localization — 가장 먼저 시도
Expo 공식 문서에 나온 방법이라 당연히 제일 먼저 써봤다.
npx expo install expo-localization
import * as Localization from 'expo-localization';
const locale = Localization.getLocales()[0]?.languageCode ?? 'en';
const isKorean = locale === 'ko';
Expo Go로 테스트할 때는 잘 됐다. 문제는 APK로 빌드했을 때였다.
TypeError: Cannot read property 'getLocales' of undefined
expo-localization은 네이티브 모듈이라서 npx expo prebuild로 android/ 디렉토리를 생성할 때 자동링킹이 제대로 연결돼야 한다. 그런데 이미 prebuild를 한 번 실행한 상태에서 패키지를 추가하면 링킹이 누락되는 경우가 있었다.
npx expo prebuild --clean으로 다시 생성하면 해결될 수도 있지만, 그러면 android/ 폴더에 직접 수정해 둔 설정들이 날아가는 위험이 있다.
Step 2. NativeModules.I18nManager — React Native 내장이라 괜찮겠지?
외부 패키지 없이 RN 내장 모듈로 해결하려고 시도했다.
import { NativeModules } from 'react-native';
const locale = NativeModules.I18nManager?.localeIdentifier ?? 'en';
iOS에서는 I18nManager.localeIdentifier가 "ko_KR" 같은 값을 잘 반환한다. 하지만 Android에서는 이 속성이 없다.
Android의 I18nManager 네이티브 모듈은 RTL(오른쪽→왼쪽 쓰기) 레이아웃 관련 기능만 담당하고, localeIdentifier 같은 프로퍼티를 노출하지 않는다. 그래서 항상 undefined가 반환되고, 폴백인 'en'으로 떨어졌다.
Step 3. Intl API — Hermes가 이미 가지고 있었다
React Native 0.71부터 기본 JS 엔진이 Hermes로 바뀌었다. Hermes는 Intl API를 내장하고 있어서, 별도 패키지 없이 표준 JavaScript API로 시스템 언어를 읽을 수 있다.
const loc = Intl.DateTimeFormat().resolvedOptions().locale;
// → "ko-KR", "en-US", "ja-JP" 등
Intl.DateTimeFormat()은 인자 없이 호출하면 시스템 기본 로케일로 포매터를 생성한다. .resolvedOptions().locale로 그 로케일 코드를 꺼내오면 된다.
최종 구현: src/i18n.ts
세 방법을 폴백 체인으로 묶어서 파일 하나로 정리했다.
import { NativeModules, Platform } from 'react-native';
function detectLocale(): string {
// 1순위: Hermes 내장 Intl (RN 0.71+, 가장 신뢰도 높음)
try {
const loc = Intl.DateTimeFormat().resolvedOptions().locale;
if (loc && loc !== 'und' && loc.length >= 2) return loc;
} catch {}
// 2순위: iOS 폴백
if (Platform.OS === 'ios') {
const sm = (NativeModules as any).SettingsManager;
return sm?.settings?.AppleLocale ?? sm?.settings?.AppleLanguages?.[0] ?? 'en';
}
// 3순위: Android 폴백
return (NativeModules as any).I18nManager?.localeIdentifier ?? 'en';
}
export const detectedLocale = detectLocale();
const isKorean = detectedLocale.startsWith('ko');
export const t = {
playlistHeader: isKorean ? '재생 목록' : 'Playlist',
itemCount: (n: number) => isKorean ? `${n}개` : `${n}`,
addUrlBtn: isKorean ? '+ URL 추가' : '+ Add URL',
playerEmpty: isKorean ? '플레이리스트에 영상을 추가해주세요' : 'Add videos to get started',
listEmpty: isKorean ? '플레이리스트가 비어있습니다' : 'Your playlist is empty',
listEmptyHint: isKorean ? '아래 + 버튼으로 유튜브 URL을 추가해보세요' : 'Tap + below to add a YouTube URL',
modalTitle: isKorean ? 'YouTube URL 추가' : 'Add YouTube URL',
inputPlaceholder: isKorean ? 'YouTube URL을 붙여넣기 하세요' : 'Paste YouTube URL here',
addButton: isKorean ? '+ 플레이리스트에 추가' : '+ Add to Playlist',
urlHint: isKorean
? 'youtube.com/watch?v=... 또는 youtu.be/... 형식 지원'
: 'Supports youtube.com/watch?v=... or youtu.be/...',
invalidUrl: isKorean ? '유효한 유튜브 URL이 아닙니다.' : 'Invalid YouTube URL.',
alreadyExists: isKorean ? '이미 플레이리스트에 있습니다.' : 'Already in your playlist.',
};
t 객체를 import해서 쓰기만 하면 된다.
// App.tsx
import { t } from './src/i18n';
<Text>{t.playlistHeader}</Text> // → '재생 목록' or 'Playlist'
<Text>{t.itemCount(3)}</Text> // → '3개' or '3'
Step 4. 실기기에서 영어 테스트하기
시스템 언어를 영어로 바꾸지 않고도 테스트하는 방법이 있다. 삼성 갤럭시 등 안드로이드 12+ 기기는 앱별 언어 설정을 지원한다.
ADB로 특정 앱의 언어를 따로 바꿀 수 있다.
# 영어로 변경
adb shell cmd locale set-app-locales com.backdev.chainplay --locales en
# 한국어로 되돌리기
adb shell cmd locale set-app-locales com.backdev.chainplay --locales ko
앱을 완전히 종료하고 다시 열면 변경된 언어가 적용된다. 시스템 언어는 그대로 두고 앱만 바꿔서 테스트할 수 있어서 편하다.
주의:
set-app-locales는 Android 12 (API 32) 이상에서만 동작한다. 그 이하 버전에서는 시스템 언어를 직접 바꿔야 한다.
트러블슈팅
Intl을 써도 'und' 가 반환된다
'und' (undetermined)는 로케일을 특정할 수 없다는 뜻이다. 에뮬레이터나 일부 커스텀 롬에서 발생한다. 코드에서 loc !== 'und' 체크를 해두면 폴백으로 넘어간다.
APK 빌드 후에도 변경이 안 된다
i18n.ts는 모듈이 처음 import될 때 딱 한 번 실행된다. 앱을 완전히 종료(스와이프 제거)하고 다시 열어야 언어 변경이 반영된다. 앱을 백그라운드에서 포어그라운드로 올리는 것만으로는 재실행되지 않는다.
expo-localization을 굳이 써야 한다면
기존 android/ 디렉토리를 유지하면서 링킹 문제를 해결하려면:
# android/settings.gradle 및 android/app/build.gradle에
# expo-localization 자동링킹 라인이 있는지 확인
grep -r "expo-localization" android/
없으면 npx expo prebuild --clean이 필요한데, 이 경우 android/ 폴더의 커스텀 설정이 초기화된다는 점을 감안해야 한다.
정리
1. Intl.DateTimeFormat().resolvedOptions().locale ← 쓸 것
2. NativeModules.I18nManager.localeIdentifier ← Android에선 undefined
3. expo-localization ← 네이티브 빌드에서 링크 실패 주의
간단한 앱에서는 i18n 라이브러리 없이 t 객체 하나로 충분하다. 언어가 늘어나거나 복수형 처리가 필요해지면 그때 i18next 같은 걸 붙여도 늦지 않는다.
실기기 테스트는 adb shell cmd locale set-app-locales 명령어로 시스템 언어를 바꾸지 않고도 앱별로 확인할 수 있다. 이거 처음 알았을 때 꽤 편했다.
backtodev
40대 PM, 다시 개발자로 돌아갑니다. 실패하고 배우며 성장하는 기록.