안드로이드 앱을 개발하다 보면 사용자가 앱 내에서 언어를 변경할 수 있는 기능이 필요한 경우가 있다.
특히 글로벌 서비스라면 필수적인 기능이기도 하다.
하지만 단순히 시스템 언어를 변경하는 것이 아니라, 앱 내에서 독립적으로 언어를 변경하고 즉시 적용되어야 하는 경우에는 몇 가지 구현 방법을 고려해야 한다.
이번 글에서는 Jetpack Compose 환경에서 런타임 언어 변경을 구현하는 방법을 간단하게 구현해 보았다.
우선,
기본적으로 앱을 실행시킨 다음, 디바이스의 언어를 변경 후 앱을 재 접근하게 되면 앱이 재 실행되면서 새로운 언어 리소스를 참조하여 보이게 된다.
필자는 이런 것이 아닌, 디바이스의 언어를 건들지 않고 앱 자체의 언어만을 변경하고 싶었다.
따라서, 이런 방식을 어떻게 구현하면 좋을까 생각을 해보다 다음과 같은 프로세스를 생각하였다.
- SharedPreferences로 사용자 언어 설정 저장
- Configuration.setLocale()로 실시간 로케일 변경
- createConfigurationContext()로 새 컨텍스트 생성
- 새 컨텍스트에서 strings.xml 리소스 가져오기
현재 버전에서는 SharedPreferences로 사용하는 것보다는 dataStore를 사용하는 것이 더 좋겠지만,
우선 빠르게 샘플을 구현해 보기 위해 비교적 작성할 코드가 적은 SharedPreferences로 작성했음을 참고하길 바란다.
위의 순서대로 코드를 구현해 보도록 하자.
첫 번째로 SharedPreferences(dataStore)에 데이터를 저장하고 가져올 수 있도록, 언어 관리를 담당할 유틸리티 클래스를 만들어보자.
object LanguageManager {
private const val LANGUAGE_PREFS = "language_prefs"
private const val IS_KOREAN_KEY = "is_korean"
fun saveLanguagePreference(context: Context, isKorean: Boolean) {
val sharedPrefs = context.getSharedPreferences(LANGUAGE_PREFS, Context.MODE_PRIVATE)
sharedPrefs.edit { putBoolean(IS_KOREAN_KEY, isKorean) }
}
fun getLanguagePreference(context: Context): Boolean {
val sharedPrefs = context.getSharedPreferences(LANGUAGE_PREFS, Context.MODE_PRIVATE)
return sharedPrefs.getBoolean(IS_KOREAN_KEY, true)
}
fun createLocalizedContext(context: Context): Context {
val isKorean = getLanguagePreference(context)
val locale = if (isKorean) Locale.KOREAN else Locale.ENGLISH
val config = Configuration(context.resources.configuration)
config.setLocale(locale)
return context.createConfigurationContext(config)
}
fun getCurrentLanguageDisplayName(context: Context): String {
val isKorean = getLanguagePreference(context)
return if (isKorean) "한국어" else "English"
}
}
sharedPreferences를 사용하는 부분은 너무나도 간단하고 많이 사용해 왔던 부분이기 때문에 설명은 넘어가도록 하자.
여기서 핵심인 부분은 createLocalizedContext 부분이다.
저장된 언어 설정에 따라 새로운 Configuration을 만들고 이를 기반으로 새로운 Context를 생성한다.
즉,
사용자가 설정하여 저장한 데이터를 기반으로 개발자의 의도에 따라 언어 설정을 바꾼 Context를 새롭게 만들어서 적용한다는 것이다.
단순하게 Context에 저장되어 있는 locale 정보를 의도적으로 수정한 다음, 해당 정보를 기반으로 이후 플로우를 처리한다고 생각하면 된다.
단순하게 생각해서,
context.getResources().getConfiguration().getLocales()
이와 같은 함수를 사용해 가져올 수 있는 locale 정보를 변경하고, createConfigurationContext 메서드를 사용하여 별도의 객체를 만들어서 사용하는 것이다.
그리고 우리가 strings.xml에 저장된 문자열을 가져올 때는
context.getString(R.string.some_string) 과 같은 형태를 통해 가져오기 때문에,
context 부분에서 우리가 원하는 locale로 변경된 context를 사용할 수 있어 디바이스의 언어를 변경하지 않아도 앱 내부에서 변경된 string을 보여줄 수 있는 것이다.
위와 같이 유틸리티를 만들어주었으면,
실제로 구현해 보도록 하자.
val context = LocalContext.current
// LanguageManager를 사용하여 언어 설정 관리
var isKorean by remember {
mutableStateOf(LanguageManager.getLanguagePreference(context))
}
// 언어 변경 함수
fun changeLanguage(useKorean: Boolean) {
LanguageManager.saveLanguagePreference(context, useKorean)
isKorean = useKorean
}
// 현재 설정에 따른 Context 생성
val localizedContext = remember(isKorean) {
LanguageManager.createLocalizedContext(context)
}
...
LazyColumn {
stickyHeader {
Box(
modifier = Modifier
.background(color = Color.White)
.padding(top = 10.dp, bottom = 10.dp)
) {
Row(modifier = Modifier.fillMaxWidth()) {
IconButton(
onClick = {
onBackButtonClick.invoke()
}
) {
Icon(Icons.Filled.ArrowBack, contentDescription = "뒤로가기")
}
Text(
modifier = Modifier
.align(Alignment.CenterVertically)
.padding(start = 8.dp),
text = localizedContext.getString(R.string.local_language_title),
fontSize = 18.sp,
fontWeight = FontWeight.Bold
)
}
}
}
...
}
구현한 UI의 일부만 가져왔다.
여기서 중요한 부분을 확인해 보자.
fun changeLanguage(useKorean: Boolean) {
LanguageManager.saveLanguagePreference(context, useKorean)
isKorean = useKorean
}
해당 부분에서는, 간단하게 true, false 값만 사용하여 한국어 / 영어로 변경하도록 하였다.
언어가 늘어나면 boolean 값으로 처리하는 것이 아닌, ko, en과 같은 언어 코드를 사용하고 유틸리티 함수 내부에서도 when 절로 추가적인 처리를 하면 될 것이다.
가장 중요한 부분은,
val localizedContext = remember(isKorean) {
LanguageManager.createLocalizedContext(context)
}
해당 부분이다.
isKorean이라는 값을 사용하긴 했지만, 언어 설정이 변경될 때마다 createLocalizedContext를 호출하여 Locale 설정이 변경된 새로운 context를 만들어서 저장하도록 만들었다.
또한
var isKorean by remember {
mutableStateOf(LanguageManager.getLanguagePreference(context))
}
이와 같이 언어 변경 시마다 데이터를 감지하도록 만들어, 언어 설정이 변경되면 recomposition을 발생시켜 UI를 갱신, 새로운 context에 따른 언어 리소스를 적용한 다음 화면을 보여주도록 하였다.
즉,
localizedContext는 현재 코드 상으로는 deafult로 디바이스 언어 설정을 따라가지만,
이후 수정이 한 번이라도 발생하면 별도 디바이스 언어 설정이 아닌 앱 내 데이터 기반으로 새로운 context를 생성하여 사용하도록 만들어둔 것이다.
이와 같은 3가지 부분이 실시간 언어 변경의 핵심이라고 볼 수 있겠다.
이렇게 까지 구현해 두면, 다음부터는 아주 간단하다.
확인을 위하여 strings.xml 파일에 2가지 언어에 대한 대응을 해두어야 한다.
res/values/strings.xml (기본값 - 영어), res/values-ko/strings.xml (한국어) 파일을 만들어서, 똑같은 이름을 갖고 다른 언어의 value가 같은 리소스 파일을 생성해 준다.
위와 같은 것은 기본적인 방식이지만, 필자는 이번에 반대로
res/values/strings.xml (기본값 - 한국어), res/values-en/strings.xml (영어) 파일을 만들어서 사용하였다.
해당 부분은 사실 중요하지 않다. 그냥 다르게 한번 사용해보고 싶었을 뿐이다.
<string name="local_language_title">로컬 언어 변경 예제</string>
<string name="current_language">현재 언어</string>
<string name="local_language_title">Local Language Change Example</string>
<string name="current_language">Current Language</string>
두 가지 파일에 위와 같이 선언해 두고 사용하면 된다.
그다음,
Text(
modifier = Modifier
.align(Alignment.CenterVertically)
.padding(start = 8.dp),
text = localizedContext.getString(R.string.local_language_title),
fontSize = 18.sp,
fontWeight = FontWeight.Bold
)
이처럼 localizedContext를 사용하여 해당 resource를 불러오면,
저장된 언어 설정에 따라서 실시간으로 화면이 갱신되어 다국어를 보여주는 것을 확인할 수 있을 것이다.
이와 같은 형태로 간단한 샘플을 만들어볼 수 있었는데,
필자가 생각했던 해당 방식의 장단점은 다음과 같다.
장점
- 앱 재시작 없이 즉시 언어 설정 변경 가능
- SharedPreferences(dataStore)로 저장하기 때문에 별다른 리소스 사용이 적음.
- Compose 상태 관리와 자연스럽게 통합되어 사이드 이펙트가 없음.
- 기존 strings.xml 리소스 시스템 사용
단점
- Activity 전환 시 새로운 Activity에서 별도의 처리가 필요함
- 시스템 언어와 독립적으로 동작하기 때문에, 기획에서부터 관리가 잘 되어야 함.
- 단순 앱 실행뿐 아니라, Deep link 등으로 직접 접근하는 경우 언어 설정 관리가 힘들 수 있음.
Compose 환경에서 사용한다면, 생각보다 쉽게 전체 리소스를 변경하여 다국어 처리가 가능할 것으로 보이나,
단점에서 서술한 것처럼 액티비티의 이동, 최초 접근 화면에 따라 설정이 적용되지 않을 수 있으므로, BaseActivity나 Application 레벨에서 추가적인 처리를 제대로 하지 않으면 서비스의 품질이 떨어질 수 있을 것이다.
실제 구현을 해보면서 느낀 점은, 생각보다 해당 방식이 UX 측면에서 매우 부드럽고 좋다는 것이다.
앱을 재시작할 필요 없이 즉시 언어가 변경되는 것도 좋거니와, 디바이스 자체에 설정된 값이 아닌 사용자가 선택에 따라 독립적으로 서비스를 제공할 수 있다는 점에서 큰 장점이 아닐까 생각한다.
하지만,
Compose를 사용하고 있더라고 "생각보다" 쉽게 전체 리소스를 바꿀 수 있는 것이지, 물리적으로 수정이 들어가는 시간과 테스트에는 많은 시간이 들어갈 것으로 보이고,
xml을 사용하는 환경에서는 어떻게 해야 하나..라는 생각이 들었다.
해당 게시글에 사용한 예제는 Github에 올려두었다.
https://github.com/HeeGyeong/ComposeSample
GitHub - HeeGyeong/ComposeSample: This project provides various examples needed to actually use Jetpack Compose.
This project provides various examples needed to actually use Jetpack Compose. - HeeGyeong/ComposeSample
github.com
'Android > Jetpack Compose' 카테고리의 다른 글
[Android] Compose 환경에서 데이터 init 방식에 대한 고찰. (2) | 2025.06.29 |
---|---|
[Android] Text Shimmer UI 구현하기 (0) | 2025.01.26 |
[Android] 사용성 높은 StickyHeader 구현하기 (0) | 2024.10.24 |
[Android] Shimmer UI 구현하기 (2) | 2024.10.06 |
[android] BottomNavigation의 구현 및 방법에 따른 차이 (1) | 2024.09.24 |