Google Play In-App updates
사용자는 기기에서 앱을 최신 상태로 유지하여 새로운 기능을 사용해 보고 성능 향상과 버그 수정을 통한 이점도 얻을 수 있습니다. 사용자 중에는 기기가 무제한 데이터에 연결되어 있을 때 백그라운드 업데이트를 진행하도록 설정하는 경우도 있지만 업데이트 설치 알림이 필요한 사용자도 있을 수 있습니다. 인앱 업데이트는 활성 사용자에게 앱을 업데이트하라고 메시지를 표시하는 Google Play Core 라이브러리 기능입니다.
서비스 중인 Ear Alarm 앱에 In-App Review 기능을 추가하면서 동시에 In-App updates 기능을 함께 추가하여 사용자에게 빠르게 업데이트를 알리고 적용할 수 있도록 In-App updates 기능을 적용하는 과정에 대해 정리하였습니다.
업데이트 방식
Immediate updates (즉시 업데이트)
즉시 업데이트는 사용자가 앱을 계속 사용하려면 앱을 업데이트하고 다시 시작해야 하는 전체 화면 UX 흐름입니다. 이 UX 흐름은 업데이트가 앱의 핵심 기능에 중요한 경우에 가장 적합합니다. 사용자가 즉시 업데이트를 수락하면 Google Play가 업데이트 설치 및 앱 다시 시작을 처리합니다.
Flexible updates (유연한 업데이트)
유연한 업데이트는 원활한 상태 모니터링 기능과 함께 백그라운드 다운로드 및 설치를 제공합니다. 이 UX 흐름은 사용자가 업데이트를 다운로드하는 동안 앱을 사용할 수 있는 경우에 적합합니다. 예를 들어 사용자가 앱의 핵심 기능에 중요하지 않은 새 기능을 사용해 보도록 유도할 수 있습니다.
유연한 업데이트와 즉시 업데이트의 차이
앱에서 업데이트가 가능한 상황인지 여부를 판단하고 앱 내에서 업데이트를 진행하는 것은 동일하지만, 유연한 업데이트와 즉시 업데이트는 몇 가지 차이가 존재합니다.
즉시 업데이트를 적용해야 할 경우
- 앱을 업데이트하지 않으면 핵심 기능을 사용하는 데 문제가 발생할 경우
- 사용자가 업데이트를 거절하면 앱을 사용하는 데 문제가 발생할 경우
유연한 업데이트를 적용해야 할 경우
- 앱을 업데이트하지 않아도 핵심 기능을 사용하는 데 문제가 없을 경우
- 사용자가 업데이트를 거절해도 핵심 기능을 사용하는 데 문제가 없을 경우
- 사용자가 원할 때 업데이트를 진행해도 핵심 기능을 사용하는 데 문제가 없을 경우
간단하게 정리하면, 사용자가 앱을 사용하는 데 필수적인 업데이트의 경우 즉시 업데이트를 적용해야 하고, 그렇지 않은 경우 유연한 업데이트를 사용하는 것을 권장합니다.
공식 문서에서도 유연한 업데이트는 핵심 기능에 중요하지 않은 기능이 업데이트되었을 경우 유연한 업데이트를 사용하도록 권장하고 있으며, 즉시 업데이트는 사용자가 거절할 경우 앱이 종료되는 흐름으로 설계하도록 권장하고 있습니다.
즉시 업데이트의 로직
즉시 업데이트의 경우는 사용자가 업데이트를 거절하면 앱을 사용하지 못하고 앱을 종료하는 로직으로 구성합니다.
업데이트가 다운로드 되는 동안에도 앱을 사용할 수 없으며 설치가 완료되면 재시작 및 업데이트가 적용됩니다.
업데이트를 다운로드 하고 있는 중에 앱을 종료하고, 다시 실행하여도 앱을 사용하지 못하고 업데이트 과정을 보여주어야 합니다.
유연한 업데이트의 로직
유연한 업데이트는 사용자가 업데이트를 거절해도 기존의 앱을 계속 사용할 수 있어야 합니다.
유연한 업데이트를 설치하면 백그라운드에서 업데이트를 다운로드하고, 설치가 완료되면 스낵바를 통해 설치 완료 여부를 알리며 직접 설치할 수 있도록 구성하는 것을 권장하고 있습니다. 사용자가 원하는 시점에서 업데이트를 설치하면 앱이 재시작되고 업데이트된 앱을 사용할 수 있습니다.
업데이트 중에 앱을 종료하여도 백그라운드에서 업데이트는 계속 동작하고, 이후 앱을 실행하면 업데이트 다운로드 여부에 따라 스낵바로 안내할 수 있어야 합니다.
백그라운드에서 업데이트를 다운로드하는 것을 강제로 종료하여도 다시 유연한 업데이트 로직을 통해 업데이트를 진행할 수 있습니다.
계획
유연한 업데이트와 즉시 업데이트는 성격이 다르고, 적용 시점을 분기할 수 있을 것 같습니다.
즉시 업데이트의 경우는 업데이트가 발생했을 경우 앱을 사용할 수 없는 상태가 되어야 하기 때문에 앱에 완전히 진입하기 전 Splash 화면 같은 곳에서 적용하여 앱에 진입하는 것 자체를 막도록 설계할 수 있을 것 같습니다.
유연한 업데이트의 경우는 사용자가 업데이트를 설치하든 거절하든 상관없이 앱을 사용할 수 있어야 하기 때문에 조작할 수 있는 화면에서 업데이트의 여부를 물어보는 것이 일반적인 것 같습니다.
하지만, 적용하려고하는 서비스인 Ear Alarm에서는 Splash화면이 따로 분리되어 있지 않은 상태이기 때문에 앱의 첫 화면에서 유연한 업데이트와 즉시 업데이트를 분기하여 처리할 수 있도록 설계하기로 하였습니다.
유연한 업데이트와 즉시 업데이트의 분기
공식 문서에서는 업데이트 우선순위를 확인하여 유연한 업데이트와 즉시 업데이트를 분기하거나 둘 다 요청하지 않도록 처리할 수 있다고 알려주고 있습니다.
Google Play Developer API를 사용하면 각 업데이트의 우선순위를 설정할 수 있습니다. 이렇게 하면 앱에서 사용자에게 업데이트를 얼마나 강력하게 권장할지 결정할 수 있습니다. 업데이트 우선순위를 설정하는 다음 전략을 예로 들어 보겠습니다.
사소한 UI 개선: 낮은 우선순위 업데이트. 유연한 업데이트와 즉시 업데이트를 둘 다 요청하지 않습니다. 사용자가 앱과 상호작용하지 않을 때만 업데이트합니다
성능 개선: 중간 우선순위 업데이트. 유연한 업데이트를 요청합니다.
중요 보안 업데이트: 높은 우선순위 업데이트. 즉시 업데이트를 요청합니다.
하지만 이를 적용하기 위해서는 Google Play Developer API를 사용하여야 하는데 이것을 공부하고 적용하기 위해서 방대한 양의 학습이 필요한 것 같고, 다양한 적용사례에서 Google Play Developer API를 사용한 우선순위로 적용하지 않고 더 간단한 방식으로 분기 처리를 하는 것을 확인할 수 있었습니다.
즉시 업데이트만 적용하거나, 앱의 Version Code, Version Name을 활용하여 분기 처리하는 등 다양한 방법을 찾아볼 수 있었습니다. 이번 적용에서는 Version Code를 사용하여 분기하는 방법으로 간단하게 구현할 예정입니다.
현재 앱의 Version Code와 업데이트 가능한 Version Code의 차이가 5 이상이라면 즉시 업데이트를, 그렇지 않다면 유연한 업데이트를 제공하여 유연한 업데이트와 즉시 업데이트를 분기 처리하도록 계획하였습니다.
개발 환경 설정
dependencies {
implementation("com.google.android.play:app-update:2.1.0")
implementation("com.google.android.play:app-update-ktx:2.1.0")
}
현재 프로젝트는 코틀린을 사용하기 때문에 app-update-ktx 만 추가해도 In-App updates 기능을 사용할 수 있습니다.
인앱 업데이트 요청이 필요한 module에 추가하였습니다.
기본 구현
공식 문서에서도 자세히 설명하고 있지만, 유연한 업데이트와 즉시 업데이트의 설명이 번갈아가며 복잡하게 설명되어 있기 때문에 구현에 대해 제대로 알아보기 어려운 부분이 있었습니다. 공통부분과 각각의 간단한 구현 방법에 대해 설명한 후, Compose에서 적용한 방법에 대해서 차례로 작성하겠습니다.
val appUpdateManager = AppUpdateManagerFactory.create(context)
AppUpdateManagerFactory를 통해 AppUpdateManager를 생성하여 인앱 업데이트 Flow를 시작할 준비를 합니다.
// Returns an intent object that you use to check for an update.
val appUpdateInfoTask = appUpdateManager.appUpdateInfo
// Checks that the platform will allow the specified type of update.
appUpdateInfoTask.addOnSuccessListener { appUpdateInfo ->
if (appUpdateInfo.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE
// This example applies an immediate update. To apply a flexible update
// instead, pass in AppUpdateType.FLEXIBLE
&& appUpdateInfo.isUpdateTypeAllowed(AppUpdateType.IMMEDIATE)
) {
// Request the update.
}
}
AppUpdateManager에서 Task<AppUpdateInfo!>을 생성하고, AppUpdateInfo 객체를 요청 및 반환합니다.
AppUpdateInfo에는 업데이트 사용 가능 여부 상태가 포함되어 있으며 업데이트를 시작하기 위한 인텐트와 진행 중인 업데이트 상태도 포함하고 있습니다.
appUpdateInfo.updateAvailability()를 통해 업데이트가 가능한 상황인지, 업데이트가 진행 중인 상황인지 확인할 수 있습니다.
public @interface UpdateAvailability {
int UNKNOWN = 0;
int UPDATE_NOT_AVAILABLE = 1;
int UPDATE_AVAILABLE = 2;
int DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS = 3;
}
즉시 업데이트의 기본 구현
val appUpdateImmediateResultLauncher = rememberLauncherForActivityResult(
ActivityResultContracts.StartIntentSenderForResult(),
) { result ->
val activity = context as? Activity
if (result.resultCode != Activity.RESULT_OK) {
activity?.finish()
}
}
val appUpdateInfoTask = appUpdateManager.appUpdateInfo
appUpdateInfoTask.addOnSuccessListener { appUpdateInfo ->
if (appUpdateInfo.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE
&& appUpdateInfo.isUpdateTypeAllowed(AppUpdateType.IMMEDIATE)
) {
appUpdateManager.startUpdateFlowForResult(
appUpdateInfo,
appUpdateImmediateResultLauncher,
AppUpdateOptions.newBuilder(AppUpdateType.IMMEDIATE).build()
)
}
}
appUpdateInfo에서 업데이트 가능 여부를 확인하고, isUpdateAvailability()를 통해 즉시 업데이트가 가능한 지도 판별합니다.
즉시 업데이트가 가능한 상황이면 startUpdateFlowForResult를 통해 즉시 업데이트를 요청할 수 있습니다.
ResultLauncher를 설정하여 업데이트를 거부하거나 완료했을 때의 로직을 추가로 설정할 수 있습니다.
onActivityResult() 콜백으로 수신할 수 있는 값은 다음과 같습니다.
RESULT_OK: 사용자가 업데이트를 수락했습니다. 즉시 업데이트인 경우 앱에 업데이트 제어 권한이 다시 주어졌을 때는 이미 업데이트가 완료된 상태여야 하기 때문에 개발자는 이 콜백을 수신하지 못할 수 있습니다.
RESULT_CANCELED: 사용자가 업데이트를 거부하거나 취소했습니다.
ActivityResult.RESULT_IN_APP_UPDATE_FAILED: 기타 오류로 인해 사용자가 동의하지 못했거나 업데이트가 진행되지 못했습니다.
즉시 업데이트의 경우 RESULT_OK가 아닌 값을 수신하게 된다면, 업데이트를 거절하거나 진행되지 못하는 상황임으로 현재 액티비를 종료하여 앱을 종료합니다.
LaunchedEffect(lifecycleState) {
if (lifecycleState == Lifecycle.State.RESUMED) {
val appUpdateInfoTask = appUpdateManager.appUpdateInfo
appUpdateInfoTask.addOnSuccessListener { appUpdateInfo ->
if (appUpdateInfo.updateAvailability() == UpdateAvailability.DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS) {
appUpdateManager.startUpdateFlowForResult(
appUpdateInfo,
appUpdateImmediateResultLauncher,
AppUpdateOptions.newBuilder(AppUpdateType.IMMEDIATE).build(),
)
}
}
}
}
즉시 업데이트가 진행되는 동안 사용자가 앱을 닫거나 종료하여도 업데이트는 백그라운드에서 계속 다운로드되고 설치되어야 합니다.
업데이트가 진행되고 있는 중간에 앱을 닫거나 종료하고, 재실행했을 경우 UpdateAvailability의 상태는 DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS으로 확인할 수 있습니다.
라이프 사이클 상태가 Resumed일 경우 앱의 진행 상태를 판단하여 즉시 업데이트를 이어서 진행할 수 있도록 설정합니다.
업데이트가 진행 중인 경우 startUpdateFlowForResult로 즉시 업데이트를 호출하면, 다시 즉시 업데이트 화면으로 이동하여 업데이트 상태를 확인할 수 있습니다.
유연한 업데이트의 기본 구현
유연한 업데이트의 경우 즉시 업데이트의 구현보다 조금 더 넓은 범위에 있다고도 말할 수 있을 것 같습니다.
val appUpdateFlexibleResultLauncher = rememberLauncherForActivityResult(
ActivityResultContracts.StartIntentSenderForResult(),
) { result ->
if (result.resultCode != Activity.RESULT_OK) {
rejectFlexibleUpdate()
}
}
즉시 업데이트와 마찬가지로 유연한 업데이트도 ResultLaucher를 설정해야 합니다. 이는 업데이트를 요청하는 대화상자에 대한 결과를 처리할 수 있습니다.
공식 문서에서는 업데이트 요청을 너무 자주 보내면 안 된다고 권장합니다. 유연한 업데이트에서는 사용자가 업데이트를 거부해도 여전히 사용 가능하도록 설계해야 하므로, 유연한 업데이트의 대화상자에 대한 result로 이를 관리할 수 있습니다.
ResultLaucher를 사용할 경우, 주의해야할 점은 요청을 보내는 시점까지 ResultLaucher를 유지해야 한다는 점입니다.
유연한 업데이트의 경우, 앱을 사용하는 업데이트를 거절할 수도 있고, 계속 사용할 수도 있기 때문에 업데이트를 호출하는 시점에서 ResultLaucher가 존재해야 합니다.
관련한 이슈를 해결했던 게시글을 참고해 주세요.
rejectFlexibleUpdate()에서 유연한 업데이트를 거절한 시간이나, boolean 값을 저장하여 업데이트 요청 횟수를 조절하도록 처리할 수 있습니다.
val appUpdateInfoTask = appUpdateManager.appUpdateInfo
appUpdateInfoTask.addOnSuccessListener { appUpdateInfo ->
if (appUpdateInfo.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE
&& appUpdateInfo.isUpdateTypeAllowed(AppUpdateType.FLEXIBLE)
) {
appUpdateManager.startUpdateFlowForResult(
appUpdateInfo,
appUpdateFlexibleResultLauncher,
AppUpdateOptions.newBuilder(AppUpdateType.FLEXIBLE).build()
)
}
}
즉시 업데이트와 마찬가지로 appUpdateInfo에서 업데이트 가능 여부를 확인하고, isUpdateAvailability()를 통해 유연한 업데이트가 가능한 지도 판별합니다.
유연한 업데이트가 가능한 상황이면 startUpdateFlowForResult를 통해 유연한 업데이트를 요청을 할 수 있습니다.
유연한 업데이트 요청을 하기 전, 사용자가 이미 거절한 적이 있는지를 확인하는 코드를 추가하여 잦은 업데이트 요청을 막을 수도 있습니다.
val installStateUpdatedListener = remember {
InstallStateUpdatedListener { state ->
if (state.installStatus() == InstallStatus.DOWNLOADED) {
scope.launch {
showFlexibleUpdateSnackBar(
snackBarHostState,
appUpdateManager,
"Update download Complete",
"Install"
)
}
}
}
}
DisposableEffect(Unit) {
appUpdateManager.registerListener(installStateUpdatedListener)
onDispose {
appUpdateManager.unregisterListener(installStateUpdatedListener)
}
}
LaunchedEffect(lifecycleState) {
if (lifecycleState == Lifecycle.State.RESUMED) {
val appUpdateInfoTask = appUpdateManager.appUpdateInfo
appUpdateInfoTask.addOnSuccessListener { appUpdateInfo ->
if (appUpdateInfo.installStatus() == InstallStatus.DOWNLOADED) {
scope.launch {
showFlexibleUpdateSnackBar(
snackBarHostState,
appUpdateManager,
"Update download Complete",
"Install"
)
}
}
}
}
}
유연한 업데이트 역시, 중간에 사용자가 앱을 닫거나 종료하여도 업데이트는 백그라운드에서 계속 다운로드되고 설치되어야 합니다.
유연한 업데이트에서는 설치 상태를 확인하고 사용자에게 SnackBar를 통해 설치 상태를 보여주어 수동으로 설치할 수 있도록 안내합니다.
즉시 업데이트와는 다르게 유연한 업데이트에서는 설치 상태에 대한 리스너를 등록하여 다운로드가 완료되었을 때 스낵바를 보여줄 수 있도록 설정합니다.
@Retention(RetentionPolicy.CLASS)
public @interface InstallStatus {
int UNKNOWN = 0;
/** @deprecated */
@Deprecated
int REQUIRES_UI_INTENT = 10;
int PENDING = 1;
int DOWNLOADING = 2;
int DOWNLOADED = 11;
int INSTALLING = 3;
int INSTALLED = 4;
int FAILED = 5;
int CANCELED = 6;
}
다운로드가 완료된 DOWNLOADED 상태에 대해서 스낵바 duration을 SnackbarDuration.Indefinite로 설정하여 표시합니다.
isUpdateTypeAllowed()
appUpdateInfo.isUpdateTypeAllowed(AppUpdateType.IMMEDIATE)
appUpdateInfo.isUpdateTypeAllowed(AppUpdateType.FLEXIBLE)
업데이트 가능 여부를 판별하면서 isUpdateTypeAllowed()를 통해 즉시 업데이트가 가능한지, 유연한 업데이트가 가능한지 판별할 수 있었습니다.
중요한 점은 현재 구현하고 있는 방법에서는 appUpdateInfo에서 특정 버전이 즉시 업데이트 타입인지, 유연한 업데이트 타입인지 구분할 수 없습니다. 그렇다면 isUpdateTypeAllowed()는 무슨 값을 반환하는지 알아보겠습니다.
isUpdateTypeAllowed()는 말 그대로 특정 타입의 업데이트 요청이 가능한 상황인지를 판별합니다.
즉시 업데이트에서의 TypeAllowed
업데이트가 가능한 경우, 처음에는 즉시 업데이트, 유연한 업데이트 모두 요청이 가능하기 때문에 둘 다 true를 반환합니다.
즉시 업데이트에서는 라이프사이클 상태가 Resumed일 때 즉, 앱이 재실행되어도 다시 즉시 업데이트 요청을 하여 업데이트 화면으로 이동할 수 있습니다.
하지만 다운로드 중에 유연한 업데이트 호출은 불가능하므로 false를 반환합니다.
유연한 업데이트에서의 TypeAllowed
업데이트가 가능한 경우, 처음에는 즉시 업데이트, 유연한 업데이트 모두 요청이 가능하기 때문에 둘 다 true를 반환합니다.
유연한 업데이트 요청을 수락하면 백그라운드에서 다운로드가 진행됩니다. 그렇기 때문에 추가적인 유연한 업데이트 호출은 불가능하므로 TypeAllowed는 false를 반환합니다. 하지만 즉시 업데이트의 TypeAllowed는 true라는 것을 알 수 있습니다.
실제로 테스트해 본 결과, 유연한 업데이트 설치를 수락하여 백그라운드에서 다운로드가 진행되고 있는 중간에, 즉시 업데이트 요청을 보내면 업데이트 화면으로 전환되어 즉시 업데이트처럼 진행 상황을 확인할 수 있는 화면으로 전환됩니다.
두 가지 로직에서도 확인할 수 있듯이 isUpdateTypeAllowed()는 startUpdateFlowForResult를 통해 특정 업데이트 요청을 할 수 있는 상태를 반환합니다. 즉 TypeAllowed는 요청이 가능한 상황을 나타내므로 유연한 업데이트로 시작했을 경우, 중간에 즉시 업데이트와 함께 사용할 수도 있습니다. (하지만 이 방법은 가능할 뿐 두 가지 업데이트 유형을 동시에 사용할 필요는 없어 보입니다.)
즉시 업데이트와 유연한 업데이트의 분기 처리 구현
가능한 업데이트가 존재할 때, 처음에는 즉시 업데이트와 유연한 업데이트 모두 요청을 시작할 수 있습니다. 그렇기 때문에 이 부분은 개발자가 분기 처리하거나 우선순위를 설정해야 합니다.
위에서 설명한 것처럼 Google Play Developer API를 사용하여 우선순위를 설정할 수도 있지만, 이번에는 더 간단하게 Version Code를 활용하여 분기 처리를 구현하였습니다.
현재 버전에서 업데이트가 가능한 버전이 존재할 때
현재 버전과 업데이트 가능한 버전의 코드의 차이가 5 이상일 경우 즉시 업데이트를
그렇지 않을 경우 유연한 업데이트를 진행하도록 분기하였습니다.
핵심 기능에 중요한 업데이트라면 출시할 버전 코드를 +5 하여 출시하고, 그렇지 않다면 +1 하여 기존처럼 출시하는 방법으로 즉시 업데이트와 유연한 업데이트를 분기할 수 있을 것이라고 생각하였습니다.
private fun getAppVersionCode(context: Context): Long {
val packageInfo: PackageInfo = context.packageManager.getPackageInfo(context.packageName, 0)
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
packageInfo.longVersionCode
} else {
@Suppress("DEPRECATION")
packageInfo.versionCode.toLong()
}
}
현재 앱의 버전 코드를 가져오는 방법은 다음과 같습니다. API 28부터는 앱의 버전 코드를 가져오는 방법이 deprecated 되었기 때문에 분기 처리하여 버전 코드를 가져오고, 업데이트 가능한 버전 코드와 비교하도록 처리하였습니다.
첫 화면에서 즉시 업데이트와 유연한 업데이트 분기 구현
Compose로 작성된 화면에서 Side Effect를 Composable로 분리하여 첫 화면에서 업데이트를 요청하는 방법으로 구현하였습니다.
private const val MIN_VERSION_DIFF_FOR_IMMEDIATE_UPDATE = 5L
LaunchedEffect(rejectFlexibleUpdateDate) {
val now = ZonedDateTime.now(ZoneOffset.UTC)
val versionCode = getAppVersionCode(context)
val appUpdateInfoTask = appUpdateManager.appUpdateInfo
appUpdateInfoTask.addOnSuccessListener { appUpdateInfo ->
val availableImmediate =
appUpdateInfo.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE
&& appUpdateInfo.isUpdateTypeAllowed(AppUpdateType.IMMEDIATE)
&& appUpdateInfo.availableVersionCode()
.toLong() - versionCode >= MIN_VERSION_DIFF_FOR_IMMEDIATE_UPDATE
val availableFlexible =
appUpdateInfo.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE
&& appUpdateInfo.isUpdateTypeAllowed(AppUpdateType.FLEXIBLE)
&& rejectFlexibleUpdateDate?.plusDays(1)?.isBefore(now) ?: true
when {
availableImmediate -> {
appUpdateManager.startUpdateFlowForResult(
appUpdateInfo,
appUpdateImmediateResultLauncher,
AppUpdateOptions.newBuilder(AppUpdateType.IMMEDIATE).build(),
)
}
availableFlexible -> {
appUpdateManager.startUpdateFlowForResult(
appUpdateInfo,
appUpdateFlexibleResultLauncher,
AppUpdateOptions.newBuilder(AppUpdateType.FLEXIBLE).build(),
)
}
}
}
}
LaunchedEffect(lifecycleState) {
if (lifecycleState == Lifecycle.State.RESUMED) {
val versionCode = getAppVersionCode(context)
val appUpdateInfoTask = appUpdateManager.appUpdateInfo
appUpdateInfoTask.addOnSuccessListener { appUpdateInfo ->
if (appUpdateInfo.updateAvailability() == UpdateAvailability.DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS
&& appUpdateInfo.availableVersionCode()
.toLong() - versionCode >= MIN_VERSION_DIFF_FOR_IMMEDIATE_UPDATE
) {
appUpdateManager.startUpdateFlowForResult(
appUpdateInfo,
appUpdateImmediateResultLauncher,
AppUpdateOptions.newBuilder(AppUpdateType.IMMEDIATE).build(),
)
} else if (appUpdateInfo.installStatus() == InstallStatus.DOWNLOADED) {
scope.launch {
showFlexibleUpdateSnackBar(
snackBarHostState,
appUpdateManager,
"Update download Complete",
"Install"
)
}
}
}
}
}
즉시 업데이트의 경우 버전 코드를 확인하여 차이가 5 이상일 경우 즉시 업데이트 요청을 수행합니다.
그렇지 않은 경우는 사용자가 유연한 업데이트를 거절했는지 추가로 판별하고 유연한 업데이트 요청을 수행합니다.
앱을 종료하고 다시 시작했을 경우, 다운로드 상태를 먼저 확인합니다. 즉시 업데이트와 유연한 업데이트 모두 다운로드 중인 상태가 되겠지만, 한번 더 버전 코드를 확인하면서 즉시 업데이트가 다운로드 중이라면 다시 즉시 업데이트 요청을 수행합니다.
그렇지 않은 경우라면 설치 상태를 확인하여 스낵바를 표시합니다.
테스트
인앱 업데이트에 대한 테스트는 공식문서에서 내부 앱 공유를 사용한 테스트를 권장합니다.
다음 단계를 진행하여 내부 앱 공유로 인앱 업데이트를 테스트합니다.
1. 테스트 기기에 설치된 앱 버전이 인앱 업데이트를 지원하고 내부 앱 공유 URL을 사용하여 설치되었는지 확인합니다.
2. Play Console 안내에 따라 앱을 내부적으로 공유합니다. 테스트 기기에 이미 설치된 버전 코드보다 높은 버전 코드를 사용하는 앱 버전을 업로드합니다.
3. 테스트 기기에서 업데이트된 앱 버전의 내부 앱 공유 링크를 클릭합니다. 단, 링크를 클릭한 후 표시되는 Google Play 스토어 페이지에서 앱을 설치하지 않습니다.
4. 기기의 앱 검색 창이나 홈 화면에서 앱을 엽니다. 이제 앱에서 업데이트를 사용할 수 있으며 인앱 업데이트 구현을 테스트할 수 있습니다.
Play Console에서 테스트 및 출시 - 테스트 - 내부 앱 공유를 선택합니다.
내부테스트와 비슷하게 테스터 목록을 만들고 테스트할 이메일을 추가 후 하단에 테스트 참여 대상 관리를 설정합니다.
업로더 관리 부분의 링크에 접속하여 3개의 서로 다른 앱 버전의 앱을 업로드합니다.
업로드가 완료되고 링크가 활성화되는 데까지 기다리는 시간이 필요했습니다.
저는 버전 코드 9, 10, 14를 업로드하여 테스트를 진행하였습니다.
먼저 버전 코드 9의 앱의 링크를 통해서 9 버전을 다운로드합니다.
이 상태에서 테스트하고자 하는 업데이트 버전의 내부 앱 공유 링크를 실행만 합니다. 14 버전의 앱 링크를 실행하고, 업데이트 가능한 상황으로 만듭니다. 이때 업데이트 버튼을 누르지 않고 설치된 9 버전의 앱을 실행시켜 즉시 업데이트를 테스트할 수 있습니다.
같은 방법으로 유연한 업데이트의 경우는 10 버전의 링크를 실행시킨 상태에서 9 버전의 앱을 실행시키면 유연한 업데이트를 테스트할 수 있습니다.
즉시 업데이트 테스트
9 버전을 설치한 후, 즉시 업데이트를 테스트합니다. 14 버전의 내부 앱 공유 링크를 실행한 상태에서 9 버전의 앱을 켜면 설계한 로직대로 즉시 업데이트 요청을 시작합니다.
즉시 업데이트를 취소하면 앱이 종료되며, 업데이트를 시작하면 업데이트 과정을 볼 수 있습니다.
업데이트 중간에 앱을 닫고 다시 실행시켜도 이어서 업데이트 상황을 확인할 수 있습니다.
업데이트가 종료되면 자동으로 앱이 재실행됩니다.
유연한 업데이트 테스트
9 버전을 설치한 후, 유연한 업데이트를 테스트합니다. 10 버전의 내부 앱 공유 링크를 실행한 상태에서 9 버전의 앱을 켜면 설계한 로직대로 유연한 업데이트 요청을 시작합니다.
유연한 업데이트를 취소하면 자체적인 로직을 통해 업데이트 요청을 계속 반복하지 않습니다.
업데이트 다운로드는 백그라운드에서 진행되며, 다운로드가 진행 중일 때도 앱을 사용할 수 있습니다.
업데이트 중간에 앱을 닫고 다시 실행시켜도 앱을 사용할 수 있습니다.
업데이트 다운로드 설치가 완료되면 스낵바를 통해 업데이트 설치 완료를 확인할 수 있고, 사용자가 원할 때 설치할 수 있습니다.
마무리
인앱 업데이트 기능은 사용자에게 빠르게 업데이트를 알리고, 업데이트를 반영할 수 있도록 유도하는 좋은 기능인 것 같습니다.
인앱 업데이트 기능이 없을 때에도 새로운 버전을 배포하면 하루에서 이틀 사이에 모든 활성 사용자 최신버전으로 업데이트를 한 것을 알 수 있었습니다. 플레이스토어에서 자동으로 업데이트 기능을 활성화해 놓은 사용자들이 대부분일 것으로 예상하지만, 그럼에도 인앱 업데이트 기능은 사용자에게 업데이트를 즉시 알릴 수 있다는 점에서 더 좋은 사용자 경험을 제공할 수 있는 기능이라고 생각합니다.
인앱 업데이트 기능을 테스트하면서 굉장히 귀찮은 부분이 많았던 것 같습니다. 일반적으로 에뮬레이터로 테스트할 수 있는 기능이 아니었고, 앱 파일로 빌드하고, 내부 앱 공유를 사용해도 업로드까지 시간이 걸리기도 하였으며 링크가 기기에서 제대로 동작하지 않아 Play Store의 로컬 데이터를 초기화해야 하는 일도 잦았습니다.
내부 앱 공유를 통해 정말 많이 업로드를 진행했습니다. 동작을 제대로 확인할 수 있는 로그를 잘 작성해서 업로드를 진행하도록 노력했지만, 하다 보니 "왜 여기서 이렇게 동작하는 거지? 왜 여기 로그를 남기지 않았을까..." 하는 부분이 많았던 것 같습니다.
인앱 리뷰와 마찬가지로 인앱 업데이트 역시 테스트하는 방법이 기존과는 조금 다르게 진행해야 하는 하는 것과 앱 배포를 하지 않은 상황이라면 개발해 볼 수 없는 특별한 기능이기에 공부하고 적용하면서 어려움도 있었지만, 배워가는 게 더 많았다고 생각합니다.
다른 앱을 사용할 때는 당연하게 있는 기능이라 별생각 없이 사용했던 기능이었지만, 즉시 업데이트나 유연한 업데이트 등 여러 가지 부분에서 좋은 사용자 경험을 제공하는 것에 대한 노력이 필요하다는 것을 느낄 수 있었습니다.
전체 코드는 여기를 참고해 주세요.
참고 자료
https://developer.android.com/guide/playcore/in-app-updates/kotlin-java?hl=ko
인앱 업데이트 지원(Kotlin 또는 Java) | Google Play | Android Developers
이 페이지는 Cloud Translation API를 통해 번역되었습니다. 인앱 업데이트 지원(Kotlin 또는 Java) 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. 이 가이드에서는 Ko
developer.android.com
https://velog.io/@mraz3068/Implementing-In-app-update-with-Compose
[Android] In-app update 적용 해보기 with Compose
사실 예전부터 '한번 적용해봐야지...' 라고 생각하면서, 미뤄왔던 인앱 업데이트를 현재 사이드로 운영 중인 개인 앱에 적용해보고 구현시 주의할 점에 대해서 정리를 해보도록 하겠다.
velog.io
https://jaeryo2357.tistory.com/144#%EC%9A%B0%EC%84%A0%EC%88%9C%EC%9C%84%20%EC%84%A4%EC%A0%95-1
[Android] InAppUpdate 기능 알아보기
InAppUpdate 인 앱 업데이트 기능은 앱 내에서 사용자의 업데이트를 유도하는 기능으로 2019년 Google IO에서 처음 소개되었습니다! 안드로이드 5.0(Api level 21) 이상부터 사용 가능하고 Play Core Library 버
jaeryo2357.tistory.com
[Android] 플레이 스토어 버전 업데이트 관리하기
플레이 스토어에 출시된 앱의 버전 업데이트를 관리 할 때에는 몇 가지 방식이 있다.1. 강제 업데이트2. 선택적 업데이트3. 인앱 업데이트 우선 우리 회사의 경우 사용자 경험에 있어 최대한 강
grusie.tistory.com
안드로이드 인앱 업데이트 즉시 업데이트
지난번 글에서 안드로이드 앱 업데이트에 필요한 버전 정보와 인앱 업데이트 기능에 대해 알아보았다. 이번 글에서는 인앱 업데이트 중 즉시 업데이트에 대해서 알아보자. 즉시 업데이트에 대
velog.io
'Project > Ear Alarm' 카테고리의 다른 글
Attempting to launch an unregistered ActivityResultLauncher with contract (0) | 2025.05.24 |
---|---|
Restoring the Navigation back stack failed (1) | 2025.05.22 |
Google Play In-App Review API 연결하기 (1) | 2025.05.19 |
타이머 서비스 분석 및 알람 로직 개선 (0) | 2025.03.31 |