앱 안에서 언어를 바꿨는데 가끔 안 바뀌는 이유: setApplicationLocales 는 떠 있는 액티비티만 다시 만든다
"한국어에서 English 로 바꿔도 그대로다, 껐다 켜야 바뀐다" 는 제보의 원인은 AppCompat 이 그 순간 화면에 떠 있는 액티비티만 재생성하기 때문이었습니다. 뒤에 멈춰 있던 화면은 옛 언어로 돌아옵니다. 앱 8개에 적용한 해결책과, 같이 걸린 함정 5가지를 씁니다.
증상
리모컨 앱 설정에서 언어를 English 로 바꾸면 설정 화면은 영어가 되는데, 뒤로 나가 메인 화면으로 돌아오면 가끔 한국어 그대로였습니다. 앱을 완전히 껐다 켜면 영어로 뜹니다. "재시작하면 된다"는 말의 정체가 궁금했습니다.
원인
AppCompatDelegate.setApplicationLocales() 는 그 시점에 started 상태인 액티비티만 다시 만듭니다. 설정 화면은 새 언어로 재생성되지만, 그 뒤에 stopped 로 멈춰 있던 메인 화면은 옛 Configuration 을 들고 그대로 resume 됩니다. 앱을 껐다 켜면 Application.onCreate 에서 로케일이 다시 적용되므로 정상으로 보입니다.
그래서 이 문제는 액티비티가 여러 개인 앱에서만 생깁니다. 단일 액티비티 앱은 그 하나가 재생성되면 전체가 갱신됩니다. 제 앱 중 리모컨 4종(액티비티 6~7개), 혜택 캘린더, 링크 관리, 스크린타임, 로또 앱이 해당했습니다.
해결
Application.registerActivityLifecycleCallbacks 의 onActivityResumed 에서, 전역 선택값(AppCompatDelegate.getApplicationLocales())과 그 액티비티 자신의 Configuration 로케일을 비교합니다. 어긋나면 recreate().
두 가지가 중요합니다.
- 전역값만 보면 안 됩니다. 전역은 이미 새 언어라서, "어긋났는지"는 액티비티 자기 설정과 비교해야만 알 수 있습니다.
- 무한 재생성을 막습니다.
WeakHashMap<Activity, Boolean>으로 인스턴스당 1회만 허용합니다. 재생성되면 새 인스턴스라 정상 경로에서는 1회로 끝납니다.
8개 앱에 같은 클래스를 넣었습니다.
같이 발견한 것: 로케일 저장 스위치가 10개 앱에 없었다
API 33 미만에서 AppCompat 이 선택한 로케일을 저장하고 복원하려면 매니페스트에 AppLocalesMetadataHolderService 와 autoStoreLocales 메타데이터가 있어야 합니다. 이게 10개 앱에 없었습니다. 있던 앱에서는 재시작 후 언어가 유지되고 없던 앱에서는 시스템 언어로 돌아갔던 이유입니다.
함정 5가지 (전부 실제로 걸린 것)
- 하드코딩된 한글은 언어 설정과 무관하게 그대로 뜹니다. 리모컨 버튼 라벨 97개가
android:text="외부입력"식으로 박혀 있었습니다.values-en에 131개 번역이 전부 있었는데도 화면에는 한글이었습니다. - 한글 탐색에
grep -P '[\x{AC00}-\x{D7A3}]'을 쓰지 않습니다. Git Bash 의 비 UTF-8 로케일에서는 "unibyte and UTF-8 locales only" 로 실패하면서 0건을 돌려줍니다. 거짓 음성입니다. 파이썬으로 찾습니다. - 코드에 한글이 있다고 다 보이는 게 아닙니다. 어떤 문자열은 변수에 담기기만 하고 읽는 곳이 없어 로그로만 갔습니다. 번역 전에 실제 노출 경로를 끝까지 추적합니다.
getResources().getIdentifier()로 리소스를 이름으로 찾지 않습니다. R8 이 이름을 지워 release 에서만 깨집니다.- 원본 주석의 경고를 읽습니다.
NO_NAME = "(이름 없음)"위에 "문구 판정에 쓰므로 자원으로 빼지 않는다"고 적혀 있었는데 상수만 자원화했다가 컴파일이 깨졌습니다. 자원화하려면 표시와 판정이 같은 문자열이 되도록 로직까지 옮겨야 합니다.
인앱 언어는 액티비티만 바꾼다
하나 더. AppCompat 인앱 언어는 액티비티 리소스에만 적용됩니다. 알림, 홈 위젯, 백그라운드에서 만드는 날짜 포맷은 여전히 시스템 언어를 씁니다. 앱 언어가 영어인데 알림만 한국어로 오는 상태가 됩니다. 알림·위젯을 만드는 코드에서는 저장된 언어 태그로 Context 를 따로 감싸(createConfigurationContext) 리소스를 가져옵니다.
검증
이건 타이밍 문제라 컴파일 통과로는 확인되지 않습니다. 실기기에서 화면 A → 설정 → 언어 변경 → 뒤로 → A 순서를 손으로 밟아야 합니다.
정리
setApplicationLocales는 떠 있는 액티비티만 재생성합니다. 멈춰 있던 화면은 옛 언어로 돌아옵니다.onActivityResumed에서 액티비티 자기 설정과 전역값을 비교해 어긋나면 1회recreate().autoStoreLocales메타데이터가 없으면 API 33 미만에서 선택이 저장되지 않습니다.- 하드코딩 한글, 거짓 음성 grep, 노출 경로, getIdentifier, 원본 주석.
- 알림·위젯은 인앱 언어가 적용되지 않으므로 따로 감쌉니다.