인앱 업데이트 지원(Kotlin 또는 Java)

이 가이드에서는 Kotlin이나 자바를 사용하여 앱에서 인앱 업데이트를 지원하는 방법을 설명합니다. 네이티브 코드(C/C++)를 사용하는 구현과 Unity를 사용하는 구현의 경우 별도의 가이드를 참고하세요.

개발 환경 설정

Play In-App Update 라이브러리는 Google Play Core 라이브러리의 일부입니다. Play In-App Update 라이브러리를 통합하려면 다음 Gradle 종속 항목을 포함하세요.

Groovy

// In your app’s build.gradle file:
...
dependencies {
    // This dependency is downloaded from the Google’s Maven repository.
    // So, make sure you also include that repository in your project's build.gradle file.
    implementation 'com.google.android.play:app-update:2.1.0'

    // For Kotlin users also add the Kotlin extensions library for Play In-App Update:
    implementation 'com.google.android.play:app-update-ktx:2.1.0'
    ...
}

Kotlin

// In your app’s build.gradle.kts file:
...
dependencies {
    // This dependency is downloaded from the Google’s Maven repository.
    // So, make sure you also include that repository in your project's build.gradle file.
    implementation("com.google.android.play:app-update:2.1.0")

    // For Kotlin users also import the Kotlin extensions library for Play In-App Update:
    implementation("com.google.android.play:app-update-ktx:2.1.0")
    ...
}

업데이트 사용 가능 여부 확인

업데이트를 요청하기 전에 앱에 사용할 수 있는 업데이트가 있는지 확인합니다. 업데이트가 있는지 AppUpdateManager를 사용하여 확인하세요.

Kotlin

val appUpdateManager = AppUpdateManagerFactory.create(context)

// 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.
    }
}

Java

AppUpdateManager appUpdateManager = AppUpdateManagerFactory.create(context);

// Returns an intent object that you use to check for an update.
Task<AppUpdateInfo> appUpdateInfoTask = appUpdateManager.getAppUpdateInfo();

// 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.
    }
});

반환된 AppUpdateInfo 인스턴스에 업데이트 사용 가능 상태가 포함되어 있습니다. 업데이트 상태에 따라 인스턴스에는 다음 항목도 포함되어 있습니다.

  • 사용 가능한 업데이트가 있고 업데이트가 허용되는 경우 반환되는 인스턴스에는 업데이트를 시작하기 위한 인텐트도 들어 있습니다.
  • 인앱 업데이트가 이미 진행 중이면 인스턴스는 진행 중인 업데이트의 상태도 보고합니다.

업데이트 비활성화 확인

업데이트 사용 가능 여부를 확인하는 것 외에 Play 스토어를 통해 사용자에게 업데이트 알림이 전송된 후 얼마나 시간이 지났는지 확인하는 것도 좋습니다. 그렇게 하면 유연한 업데이트를 시작할지 아니면 즉시 업데이트를 시작할지 판단하는 데 도움이 될 수 있습니다. 예를 들어 며칠을 기다렸다가 사용자에게 유연한 업데이트를 알린 다음 며칠 기다렸다가 즉시 업데이트를 요청할 수도 있습니다.

clientVersionStalenessDays()를 사용하여 Play 스토어에 업데이트가 제공된 이후의 일 수를 확인할 수 있습니다.

Kotlin

val appUpdateManager = AppUpdateManagerFactory.create(context)

// Returns an intent object that you use to check for an update.
val appUpdateInfoTask = appUpdateManager.appUpdateInfo

// Checks whether the platform allows the specified type of update,
// and current version staleness.
appUpdateInfoTask.addOnSuccessListener { appUpdateInfo ->
    if (appUpdateInfo.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE
          && (appUpdateInfo.clientVersionStalenessDays() ?: -1) >= DAYS_FOR_FLEXIBLE_UPDATE
          && appUpdateInfo.isUpdateTypeAllowed(AppUpdateType.FLEXIBLE)) {
              // Request the update.
    }
}

Java

AppUpdateManager appUpdateManager = AppUpdateManagerFactory.create(context);

// Returns an intent object that you use to check for an update.
Task<AppUpdateInfo> appUpdateInfoTask = appUpdateManager.getAppUpdateInfo();

// Checks whether the platform allows the specified type of update,
// and current version staleness.
appUpdateInfoTask.addOnSuccessListener(appUpdateInfo -> {
    if (appUpdateInfo.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE
          && appUpdateInfo.clientVersionStalenessDays() != null
          && appUpdateInfo.clientVersionStalenessDays() >= DAYS_FOR_FLEXIBLE_UPDATE
          && appUpdateInfo.isUpdateTypeAllowed(AppUpdateType.FLEXIBLE)) {
              // Request the update.
    }
});

업데이트 우선순위 확인

Google Play Developer API를 사용하여 각 업데이트의 우선순위를 설정할 수 있습니다. 이렇게 하면 앱에서 사용자에게 업데이트를 얼마나 강력하게 권장할지 결정할 수 있습니다. 업데이트 우선순위를 설정하는 다음 전략을 예로 들어 보겠습니다.

  • 사소한 UI 개선: 낮은 우선순위 업데이트. 유연한 업데이트와 즉시 업데이트 중 어떤 것도 요청하지 않습니다. 사용자가 앱과 상호작용하지 않을 때만 업데이트합니다.
  • 성능 개선: 중간 우선순위 업데이트. 유연한 업데이트를 요청합니다.
  • 중요 보안 업데이트: 높은 우선순위 업데이트. 즉시 업데이트를 요청합니다.

우선순위를 결정하기 위해 Google Play는 0에서 5 사이의 정숫값을 사용하며 0은 기본값, 5는 가장 높은 우선순위를 나타냅니다. 업데이트 우선순위를 설정하려면 Google Play Developer API의 Edits.tracks.releases 아래에 있는 inAppUpdatePriority 필드를 사용하세요. 출시에 새로 추가된 모든 버전은 출시와 동일한 우선순위로 간주됩니다. 우선순위는 새 버전을 출시할 때만 설정할 수 있으며 나중에 변경할 수 없습니다.

Play Developer API 문서에 설명된 대로 Google Play Developer API를 사용하여 우선순위를 설정합니다. 인앱 업데이트 우선순위는 Edit.tracks: update 메서드에 전달된 Edit.tracks 리소스에서 지정해야 합니다. 다음 예는 버전 코드가 88이고 inAppUpdatePriority가 5인 APK 출시를 보여 줍니다.

{
  "releases": [{
      "versionCodes": ["88"],
      "inAppUpdatePriority": 5,
      "status": "completed"
  }]
}

앱 코드에서 updatePriority()를 사용하여 특정 업데이트의 우선순위를 확인할 수 있습니다. 반환된 우선순위는 출시 트랙과 관계없이 설치된 버전과 사용 가능한 최신 버전 간의 모든 앱 버전 코드에 관한 inAppUpdatePriority를 고려합니다. 예를 들어 다음 시나리오를 고려해 보세요.

  • 우선순위 없이 버전 1을 프로덕션 트랙에 출시합니다.
  • 우선순위 5로 버전 2를 내부 테스트 트랙에 출시합니다.
  • 우선순위 없이 버전 3을 프로덕션 트랙에 출시합니다.

프로덕션 사용자가 버전 1에서 버전 3으로 업데이트하면 버전 2가 다른 트랙에 게시된 경우에도 우선순위 5를 받게 됩니다.

Kotlin

val appUpdateManager = AppUpdateManagerFactory.create(context)

// Returns an intent object that you use to check for an update.
val appUpdateInfoTask = appUpdateManager.appUpdateInfo

// Checks whether the platform allows the specified type of update,
// and checks the update priority.
appUpdateInfoTask.addOnSuccessListener { appUpdateInfo ->
    if (appUpdateInfo.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE
          && appUpdateInfo.updatePriority() >= 4 /* high priority */
          && appUpdateInfo.isUpdateTypeAllowed(AppUpdateType.IMMEDIATE)) {
              // Request an immediate update.
    }
}

Java

AppUpdateManager appUpdateManager = AppUpdateManagerFactory.create(context);

// Returns an intent object that you use to check for an update.
Task<AppUpdateInfo> appUpdateInfoTask = appUpdateManager.getAppUpdateInfo();

// Checks whether the platform allows the specified type of update,
// and checks the update priority.
appUpdateInfoTask.addOnSuccessListener(appUpdateInfo -> {
    if (appUpdateInfo.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE
          && appUpdateInfo.updatePriority() >= 4 /* high priority */
          && appUpdateInfo.isUpdateTypeAllowed(AppUpdateType.IMMEDIATE)) {
              // Request an immediate update.
    }
});

업데이트 시작

업데이트가 있다는 것을 확인한 후 AppUpdateManager.startUpdateFlowForResult()를 사용하여 업데이트를 요청할 수 있습니다.

Kotlin

appUpdateManager.startUpdateFlowForResult(
    // Pass the intent that is returned by 'getAppUpdateInfo()'.
    appUpdateInfo,
    // an activity result launcher registered via registerForActivityResult
    activityResultLauncher,
    // Or pass 'AppUpdateType.FLEXIBLE' to newBuilder() for
    // flexible updates.
    AppUpdateOptions.newBuilder(AppUpdateType.IMMEDIATE).build())

Java

appUpdateManager.startUpdateFlowForResult(
    // Pass the intent that is returned by 'getAppUpdateInfo()'.
    appUpdateInfo,
    // an activity result launcher registered via registerForActivityResult
    activityResultLauncher,
    // Or pass 'AppUpdateType.FLEXIBLE' to newBuilder() for
    // flexible updates.
    AppUpdateOptions.newBuilder(AppUpdateType.IMMEDIATE).build());

업데이트는 각 AppUpdateInfo 인스턴스를 사용하여 한 번만 시작할 수 있습니다. 실패한 경우 업데이트를 다시 시도하려면 새 AppUpdateInfo를 요청하고 업데이트가 사용 가능하고 허용되는지 다시 확인합니다.

내장된 ActivityResultContracts.StartIntentSenderForResult 계약을 사용하여 활동 결과 런처를 등록할 수 있습니다. 업데이트 상태 콜백 가져오기 섹션을 확인하세요.

다음 단계는 유연한 업데이트즉시 업데이트 중 무엇을 요청하는지에 따라 다릅니다.

AppUpdateOptions로 업데이트 구성

AppUpdateOptions에는 기기 저장소가 제한된 경우 업데이트가 애셋 팩을 지울 수 있는지 정의하는 AllowAssetPackDeletion 필드가 포함됩니다. 이 필드는 기본적으로 false로 설정되지만 setAllowAssetPackDeletion() 메서드를 사용하여 true로 대신 설정할 수 있습니다.

Kotlin

appUpdateManager.startUpdateFlowForResult(
    // Pass the intent that is returned by 'getAppUpdateInfo()'.
    appUpdateInfo,
    // an activity result launcher registered via registerForActivityResult
    activityResultLauncher,
    // Or pass 'AppUpdateType.FLEXIBLE' to newBuilder() for
    // flexible updates.
    AppUpdateOptions.newBuilder(AppUpdateType.IMMEDIATE)
        .setAllowAssetPackDeletion(true)
        .build())

Java

appUpdateManager.startUpdateFlowForResult(
    // Pass the intent that is returned by 'getAppUpdateInfo()'.
    appUpdateInfo,
    // an activity result launcher registered via registerForActivityResult
    activityResultLauncher,
    // Or pass 'AppUpdateType.FLEXIBLE' to newBuilder() for
    // flexible updates.
    AppUpdateOptions.newBuilder(AppUpdateType.IMMEDIATE)
        .setAllowAssetPackDeletion(true)
        .build());

업데이트 상태에 관한 콜백 가져오기

업데이트를 시작한 후 등록된 활동 결과 런처 콜백은 다음과 같이 확인 대화상자 결과를 가져옵니다.

Kotlin

registerForActivityResult(StartIntentSenderForResult()) { result: ActivityResult ->
    // handle callback
    if (result.resultCode != RESULT_OK) {
        log("Update flow failed! Result code: " + result.resultCode);
        // If the update is canceled or fails,
        // you can request to start the update again.
    }
}

Java

registerForActivityResult(
    new ActivityResultContracts.StartIntentSenderForResult(),
    new ActivityResultCallback<ActivityResult>() {
        @Override
        public void onActivityResult(ActivityResult result) {
            // handle callback
            if (result.getResultCode() != RESULT_OK) {
                log("Update flow failed! Result code: " + result.getResultCode());
                // If the update is canceled or fails,
                // you can request to start the update again.
            }
        }
    });

onActivityResult() 콜백에서 수신할 수 있는 몇 가지 값은 다음과 같습니다.

  • RESULT_OK: 사용자가 업데이트를 수락했습니다. 즉시 업데이트인 경우 앱에 업데이트 제어 권한이 다시 주어졌을 때는 이미 업데이트가 완료된 상태여야 하기 때문에 개발자는 이 콜백을 수신하지 못할 수 있습니다.
  • RESULT_CANCELED: 사용자가 업데이트를 거부하거나 취소했습니다.
  • ActivityResult.RESULT_IN_APP_UPDATE_FAILED: 기타 오류로 인해 사용자가 동의하지 못했거나 업데이트가 진행되지 못했습니다.

유연한 업데이트 처리

유연한 업데이트를 시작하면 사용자에게 동의를 요청하는 대화상자가 먼저 표시됩니다. 사용자가 동의하면 백그라운드에서 다운로드가 시작되고 사용자는 앱과 상호작용을 계속할 수 있습니다. 이 섹션에서는 유연한 인앱 업데이트를 모니터링하고 완료하는 방법을 설명합니다.

유연한 업데이트 상태 모니터링

유연한 업데이트를 위한 다운로드가 시작되면 앱은 업데이트 설치 가능 시점을 확인하고 앱의 UI에 진행 상황을 표시하기 위해 업데이트 상태를 모니터링해야 합니다.

설치 상태 업데이트 리스너를 등록하여 진행 중인 업데이트의 상태를 모니터링할 수 있습니다. 앱의 UI에 진행률 표시줄을 제공하여 다운로드 진행 상태를 사용자에게 알릴 수도 있습니다.

Kotlin

// Create a listener to track request state updates.
val listener = InstallStateUpdatedListener { state ->
    // (Optional) Provide a download progress bar.
    if (state.installStatus() == InstallStatus.DOWNLOADING) {
      val bytesDownloaded = state.bytesDownloaded()
      val totalBytesToDownload = state.totalBytesToDownload()
      // Show update progress bar.
    }
    // Log state or install the update.
}

// Before starting an update, register a listener for updates.
appUpdateManager.registerListener(listener)

// Start an update.

// When status updates are no longer needed, unregister the listener.
appUpdateManager.unregisterListener(listener)

Java

// Create a listener to track request state updates.
InstallStateUpdatedListener listener = state -> {
  // (Optional) Provide a download progress bar.
  if (state.installStatus() == InstallStatus.DOWNLOADING) {
      long bytesDownloaded = state.bytesDownloaded();
      long totalBytesToDownload = state.totalBytesToDownload();
      // Implement progress bar.
  }
  // Log state or install the update.
};

// Before starting an update, register a listener for updates.
appUpdateManager.registerListener(listener);

// Start an update.

// When status updates are no longer needed, unregister the listener.
appUpdateManager.unregisterListener(listener);

유연한 업데이트 설치

InstallStatus.DOWNLOADED 상태를 감지한 경우 앱을 다시 시작하여 업데이트를 설치해야 합니다.

즉시 업데이트와 달리 Google Play는 유연한 업데이트를 위해 앱 재시작을 자동으로 트리거하지 않습니다. 이는 유연한 업데이트에서는 사용자가 업데이트 설치 여부를 결정할 때까지 앱과 계속 상호작용하기를 바라기 때문입니다.

사용자에게 업데이트 설치가 준비되었음을 알리고 앱 재시작 전에 확인을 요청하는 알림(또는 기타 UI 표시)을 제공하는 것이 좋습니다.

다음 예에서는 앱 재시작을 위해 사용자의 확인을 요청하는 머티리얼 디자인 스낵바를 구현하는 방법을 보여줍니다.

Kotlin

val listener = { state ->
    if (state.installStatus() == InstallStatus.DOWNLOADED) {
        // After the update is downloaded, show a notification
        // and request user confirmation to restart the app.
        popupSnackbarForCompleteUpdate()
    }
    ...
}

// Displays the snackbar notification and call to action.
fun popupSnackbarForCompleteUpdate() {
    Snackbar.make(
        findViewById(R.id.activity_main_layout),
        "An update has just been downloaded.",
        Snackbar.LENGTH_INDEFINITE
    ).apply {
        setAction("RESTART") { appUpdateManager.completeUpdate() }
        setActionTextColor(resources.getColor(R.color.snackbar_action_text_color))
        show()
    }
}

Java

InstallStateUpdatedListener listener = state -> {
    if (state.installStatus() == InstallStatus.DOWNLOADED) {
        // After the update is downloaded, show a notification
        // and request user confirmation to restart the app.
        popupSnackbarForCompleteUpdate();
    }
    ...
};

// Displays the snackbar notification and call to action.
private void popupSnackbarForCompleteUpdate() {
  Snackbar snackbar =
      Snackbar.make(
          findViewById(R.id.activity_main_layout),
          "An update has just been downloaded.",
          Snackbar.LENGTH_INDEFINITE);
  snackbar.setAction("RESTART", view -> appUpdateManager.completeUpdate());
  snackbar.setActionTextColor(
      getResources().getColor(R.color.snackbar_action_text_color));
  snackbar.show();
}

포그라운드에서 appUpdateManager.completeUpdate()를 호출하면 백그라운드에서 앱을 다시 시작하는 전체 화면 UI가 플랫폼에 표시됩니다. 플랫폼이 업데이트 설치를 끝내면 앱은 기본 활동으로 다시 시작됩니다.

앱이 백그라운드에 있을 때 completeUpdate()를 대신 호출하면 기기 UI를 가리지 않는 상태로 업데이트가 자동으로 설치됩니다.

사용자가 앱을 포그라운드로 가져올 때마다 앱에 설치 대기 중인 업데이트가 있는지 확인하세요. 앱의 업데이트가 DOWNLOADED 상태이면 사용자에게 업데이트를 설치하라는 메시지를 표시합니다. 그러지 않으면 업데이트 데이터가 사용자의 기기 저장용량을 계속 차지합니다.

Kotlin

// Checks that the update is not stalled during 'onResume()'.
// However, you should execute this check at all app entry points.
override fun onResume() {
    super.onResume()

    appUpdateManager
        .appUpdateInfo
        .addOnSuccessListener { appUpdateInfo ->
            ...
            // If the update is downloaded but not installed,
            // notify the user to complete the update.
            if (appUpdateInfo.installStatus() == InstallStatus.DOWNLOADED) {
                popupSnackbarForCompleteUpdate()
            }
        }
}

Java

// Checks that the update is not stalled during 'onResume()'.
// However, you should execute this check at all app entry points.
@Override
protected void onResume() {
  super.onResume();

  appUpdateManager
      .getAppUpdateInfo()
      .addOnSuccessListener(appUpdateInfo -> {
              ...
              // If the update is downloaded but not installed,
              // notify the user to complete the update.
              if (appUpdateInfo.installStatus() == InstallStatus.DOWNLOADED) {
                  popupSnackbarForCompleteUpdate();
              }
          });
}

즉시 업데이트 처리

즉시 업데이트를 실행할 때 사용자가 업데이트 시작에 동의하면 Google Play는 업데이트가 진행되는 내내 업데이트 진행 상황을 앱의 UI 상단에 표시합니다. 업데이트하는 동안 사용자가 앱을 닫거나 종료하더라도 추가 사용자 확인 없이 업데이트가 백그라운드에서 계속 다운로드되고 설치되어야 합니다.

하지만 앱이 포그라운드로 돌아오면 업데이트가 UpdateAvailability.DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS 상태로 중단되지 않았는지 확인해야 합니다. 업데이트가 이 상태로 중단된 경우 업데이트를 다시 시작합니다.

Kotlin

// Checks that the update is not stalled during 'onResume()'.
// However, you should execute this check at all entry points into the app.
override fun onResume() {
    super.onResume()

    appUpdateManager
        .appUpdateInfo
        .addOnSuccessListener { appUpdateInfo ->
            ...
            if (appUpdateInfo.updateAvailability()
                == UpdateAvailability.DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS
            ) {
                // If an in-app update is already running, resume the update.
                appUpdateManager.startUpdateFlowForResult(
                  appUpdateInfo,
                  activityResultLauncher,
                  AppUpdateOptions.newBuilder(AppUpdateType.IMMEDIATE).build())
            }
        }
}

Java

// Checks that the update is not stalled during 'onResume()'.
// However, you should execute this check at all entry points into the app.
@Override
protected void onResume() {
  super.onResume();

  appUpdateManager
      .getAppUpdateInfo()
      .addOnSuccessListener(
          appUpdateInfo -> {
            ...
            if (appUpdateInfo.updateAvailability()
                == UpdateAvailability.DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS) {
                // If an in-app update is already running, resume the update.
                appUpdateManager.startUpdateFlowForResult(
                  appUpdateInfo,
                  activityResultLauncher,
                  AppUpdateOptions.newBuilder(AppUpdateType.IMMEDIATE).build());
            }
          });
}

startUpdateFlowForResult()의 참조 문서에 설명된 대로 업데이트 흐름에서 결과를 반환합니다. 특히 앱은 사용자가 업데이트를 거부하거나 다운로드를 취소하는 경우를 처리할 수 있어야 합니다. 사용자가 이러한 동작 중 하나를 진행하면 Google Play UI가 닫힙니다. 앱이 가장 적합한 진행 방법을 판단해야 합니다.

가능하면 사용자가 업데이트 없이 계속 진행하도록 허용하고 나중에 다시 메시지를 표시하는 것이 좋습니다. 앱이 업데이트 없이 작동할 수 없으면 업데이트 흐름을 다시 시작하거나 사용자에게 앱을 닫으라는 메시지를 표시하기 전에 정보 메시지를 표시하는 것이 좋습니다. 이렇게 하면 사용자가 업데이트를 다운로드할 준비가 되면 앱을 다시 실행할 수 있다는 것을 알게 됩니다.

다음 단계

앱의 인앱 업데이트를 테스트하여 통합이 올바르게 작동하는지 확인합니다.