관리 메뉴

정상에서 IT를 외치다

[in-app updates] 안드로이드 인 앱 업데이트 사용기 본문

안드로이드

[in-app updates] 안드로이드 인 앱 업데이트 사용기

Black-Jin 2019. 6. 21. 11:24
728x90
반응형

안녕하세요. 블랙진입니다.


2019 구글 IO에서 발표한 내용중 인 앱 업데이트에 대한 사용기를 소개해 볼려고 합니다.


In-App Update


새로운 버전의 앱을 구글 스토어에 등록했을 때 알림을 통해 사용자에게 업데이트를 권하는 방법은 개발자 마다 한번은 고민했을 겁니다. 이에 대해 2019 구글 IO에서는 그 뱡향을 제시해 줬는데요. 다음은 2019 구글 블로그의 내용 중 일부입니다.


today we’re also moving in-app updates from beta to stable. The ability to dynamically update apps is something you’ve been requesting for a long time. Let’s say you have a crucial bug in your app, and you need to push it out right away; you don’t want to wait until users discover an update in the Play Store. Now you can.


내용을 보시면 beta 버전에서 stable 버전을 이번 2019년에 배포 했다고 하는데 어디 한번 써볼까요?



Flexible vs Immediate


Flexible(권장 업데이트)



출처


유연한(권장) 업데이트입니다. 앱을 실행시키면 업데이트 여부를 파악해 다이얼로그를 사용자에게 보여줍니다. 업데이트를 클릭하게 되면 백그라운드에서 업데이트가 진행되며 업데이트가 끝나면 사용자에게 완료 메시지를 보여줍니다. 여기서 사용자가 앱을 다시 시작할지 말지를 선택하게 되는데요. 이렇게 플레이 스토어로 이동하지 않고 업데이트 여부를 사용자가 선택할 수 있게하여 앱에 대한 사용자 경험을 계속 유지시켜 줄 수 있습니다.




Immediate(필수 업데이트)



출처


즉시(필수) 업데이트입니다. 앱을 실행시키면 업데이트를 권하는 전체화면이 나옵니다. 물론 이 화면에서 뒤로가기를 통해 이전화면으로 넘어 갈 수 있지만 flexible 업데이트 보다는 사용자에게 업데이트를 더 요구하는 방법입니다. 역시 플레이 스토어로 이동하지 않고 해당 화면에서 진행되며 업데이트가 완료 되면 앱이 재실행됩니다. 





사용방법


1.  Play Core Library 설치


인 앱 업데이트는 안드로이드 5.0(Api level 21) 이상부터 사용 가능합니다. 그리고 Play Core Library 1.5.0 이상을 안드로이드 스튜디오에 추가해 주셔야 됩니다.


//play store core
implementation 'com.google.android.play:core:1.5.0'


2. AppUpdateManager 초기화

appUpdateManager = AppUpdateManagerFactory.create(this)

appUpdateManager?.let {
it.appUpdateInfo.addOnSuccessListener { appUpdateInfo ->

if (appUpdateInfo.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE
&& appUpdateInfo.isUpdateTypeAllowed(AppUpdateType.IMMEDIATE)) {
// or AppUpdateType.FLEXIBLE
appUpdateManager?.startUpdateFlowForResult(
appUpdateInfo,
AppUpdateType.IMMEDIATE, // or AppUpdateType.FLEXIBLE
this,
REQUEST_CODE_UPDATE
)
}
}
}


AppUpdateManagerFactory를 통해 초기화를 해줍니다. 그리고 앱이 업데이트 가능한 상태인지?(UpdateAvailability.UPDATE_AVAILABLE), 해당 업데이트 타입을 적용할 수 있는지?(isUpdateTypeAllowed(AppUpdateType.IMMEDIATE)) 체크 후 업데이트를 진행합니다.



3. 업데이트 취소


AppUpdateManager 의 startUpdateFlowForResult 는 startActivityForResult 와 같은 동작을 합니다. 따라서 onActivityResult 를 통해 취소 결과를 반환해 줍니다.

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)

if (requestCode == REQUEST_CODE_UPDATE) {
if (resultCode != Activity.RESULT_OK) {
toast("업데이트가 취소 되었습니다.")
}
}
}



4. Flexible 업데이트


1) 리스너를 추가해 줍니다.(생명주기에 맞춰 해제 코드도 추가해 주세요)

listener = InstallStateUpdatedListener {
// Handle install state
if (it.installStatus() == InstallStatus.DOWNLOADED) {
popupSnackbarForCompleteUpdate()
}
}

appUpdateManager?.registerListener(listener)


리스너를 통해 백그라운드에서 앱 설치가 완료되면 재시작 요청을 사용자에게 보여줍니다.


2) 설치/재시작 요청 보여주기

private fun popupSnackbarForCompleteUpdate() {

val snackbar = Snackbar.make(findViewById(R.id.clActivityMain), "업데이트 버전 다운로드 완료", 5000)
.setAction("설치/재시작") {
appUpdateManager?.completeUpdate()
}

snackbar.show()
}


사용자가 직접 completeUpdate() 함수를 실행 시켜 주어야만 업데이트가 적용됩니다. 그전에는 앱을 아무리 다시 시작해도 적용이 되지 않습니다. 이 때 AppUpdateManager 상태를 봐야 되는데 그전에 AppUpdateManager에는 어떤 상태가 있는지 확인해 보겠습니다.


appUpdateManager.appUpdateInfo

AppUpdateManager 에서 getAppUpdateInfo 를 통해 두 가지 상태를 알아올 수 있습니다.



UpdateAvailablity(업데이트 가능 여부)

public @interface UpdateAvailability {
int UNKNOWN = 0;
int UPDATE_NOT_AVAILABLE = 1;
int UPDATE_AVAILABLE = 2;
int DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS = 3;
}


InstallStatus(설치가 얼마나 진행 되었는지)

public @interface InstallStatus {
int UNKNOWN = 0;
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;
}


그럼 다시 본론으로 들어가서 2) 설치/재시작 요청 보여주기 단계에서는 어떤 상태 일까요?


UpdateAvailability : 3

InstallStatus : 11


업데이트가 이미 진행된 상태에서는 UpdateAvailablity 는 DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS 이며 InstallStatus 는 DOWNLOADED 상태가 표시됩니다. (만약 백그라운드에서 아직 설치가 진행 중인 상태에서는 UpdateAvailablity : 3 , InstallStatus : 2 이 나옵니다. 설치/재시작을 누르면  UpdateAvailablity : 3 , InstallStatus : 3 가 되며 설치/재시작이 모두 완료 되면 UpdateAvailablity : 1 , InstallStatus : 0  상태가 됩니다.) 


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


위 코드는 구글 개발 페이지에서 가져온 코드 입니다. onResume 에서 상태 처리를 하지만 위와 같이 한 경우 모든 app entry point 에서 확인을 해줘야 된다고 하죠? 필자는 적절한 방법이라 생각되지 않아 다르게 처리했습니다. 이 부분에 관해서는 개발자 분들의 판단에 맞춰 InstallStatus.DOWNLOADED 를 어디서 주기적으로 확인하면 좋을지 결정해서 작업하시면 되겠습니다.



5. Immediate 업데이트



즉시 업데이트에서 버튼을 클릭하게 되면 오른쪽 다운로드 화면으로 넘어가게 됩니다. 근데 저 화면에서 뒤로가기가 적용됩니다.... 다운로드 중에 뒤로가기 혹은 백그라운드로 이동하게 되면 이에 맞춰 추가 코드를 작성해 주어야합니다.  왜냐하면 위 화면에서 벗어나게 되면 백그라운드에서 업데이트가 완료 되어도 앱에 적용되지 않습니다. AppUpdateManager의 상태를 확인해 startUpdateFlowForeResult를 재동작 시켜주어야 합니다. 그러면 오른쪽 화면의 상황에서는 AppUpdateManager 는 무슨 상태일까요?


UpdateAvailablity(업데이트 가능 여부)

public @interface UpdateAvailability {
int UNKNOWN = 0;
int UPDATE_NOT_AVAILABLE = 1;
int UPDATE_AVAILABLE = 2;
int DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS = 3;
}


InstallStatus(설치가 얼마나 진행 되었는지)

public @interface InstallStatus {
int UNKNOWN = 0;
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;
}


UpdateAvailability : 3

InstallStatus : 2


업데이트가 이미 진행된 상태에서는 UpdateAvailablity 는 DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS 이며 설치 상태는 두가지로 나뉩니다. 설치 중이면  InstallStatus : 2 설치가 완료된 상태면 InstallStatus : 11 로 나옵니다. 이에 관해서 구글에서는 어떻게 처리했을까요? 역시 구글 개발 페이지 코드를 보겠습니다.


// 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.
                manager
.startUpdateFlowForResult(
                    appUpdateInfo
,
                    IMMEDIATE
,
                   
this,
                    MY_REQUEST_CODE
);
           
}
         
});
}


OnResume 에서 UpdateAvailablity 상태가 DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS 인 경우 startUpdateFlowForResult 를 실행 시켜 줬네요. 이 코드는 다운로드 화면에서 뒤로간 경우로 onResume이 적용되어 다시 다운로드 화면으로 강제 이동됩니다. 이 부분 역시 업데이트 상태에 따라 개발자 분들의 판단에 맞춰 예외 처리를 해주시면 됩니다. 필자는 즉시 업데이트 시에는 구글의 위 방식이 적절한 것 같습니다.




테스트 하기



필자는 구글 콘솔의 내부 테스트 트랙에 앱을 업데이트하여 테스트 했습니다. 내부 테스트 트랙은 앱을 올리면 빠르게(1분 미만 - 필자가 테스트 했을 때 기준) 스토어에 적용되므로 빠르게 인 앱 업데이트를 테스트 해 볼 수 있습니다. 


추가) FakeAppUpdateManager를 사용해서 테스트를 진행할 수 있다는 내용을 댓글(서영락님)을 통해 알게 되었습니다. 사용법은 구글문서를 확인해주세요



문제 이슈


in-app updates 문서의 맨 아래를 보면 아래와 같이 나와 있습니다.


내용을 정리하면


1. 테스트 하기전 구글 스토어에서 다운 받은 앱을 사용하세요.

2. 구글 스토어에 있는 어플리케이션 ID와 인증키가 동일해야 합니다.

3. 낮은 버전에서 높은 버전으로 테스트를 진행하세요.

4. 구글 플레이 캐시로 인해 업데이트가 안될 수 있습니다. 구글플레이 스토어를 완전히 종료 후 실행한 후 "나의 앱/게임 탭"에서 업데이트 가능 여부를 확인해 주세요.


필자는 처음에 스토어에 업데이트 되었지만 동작이 안되 코드가 잘못 된줄 알았습니다. 하지만 구글 스토어를 완전히 종료 해주고 재 실행한 후 구글 스토어 네비게이션 드로워에 있는 "내 앱/게임" 에서 "업데이트 대기 중 목록"에 나와 있는지 확인 후 실행해주면 잘 적용됩니다.



마무리


Immediate 업데이트 방식을 아이웨딩 앱에서 어떻게 동작되는지 GIF를 준비했습니다.


SplashActivity -> MainActivity (업데이트 코드 실행)



깔끔하게 업데이트 되는 모습! 이거 정말 좋아요! 두번 쓰세요! 세번 쓰세요!



필자가 진행한 상태 테스트

/**
* 업데이트가 없는 경우
* UpdateAvailability : 1
* installStatus : 0
*
*
* 권장 업데이트 테스트
*
* 업데이트가 있는 경우
* UpdateAvailability : 2
* installStatus : 0
*
* 업데이트 확인 후 진행 중
* UpdateAvailability : 3
* installStatus : 2
*
* 업데이트 다운로드 완료
* UpdateAvailability : 3
* installStatus : 11
*
* 업데이트 설치/재시작 진행 중
* (뒤로 가기 및 화면 종료를 해도 설치가 완료되면 앱을 자동 재실행 합니다.)
* UpdateAvailability : 3
* installStatus : 3
*
* 업데이트 설치 및 재시작 완료
* UpdateAvailability : 1
* installStatus : 0
*
*
* 즉시 업데이트 테스트
*
* 업데이트가 있는 경우
* UpdateAvailability : 2
* installStatus : 0
*
* 업데이트 확인 후 진행 중(확인 클릭 후 뒤로가기)
* UpdateAvailability : 3
* installStatus : 2
*
* 업데이트 다운로드 완료
* (설치 화면을 벗어나면 자동으로 재실행 되지 않습니다.)
* UpdateAvailability : 3
* installStatus : 11
*
* 업데이트 설치 및 재시작 완료
* UpdateAvailability : 1
* installStatus : 0
*
*/


반응형
21 Comments
  • 프로필사진 남갯 2019.06.25 14:15 신고 gif를 통한 친절한 설명 감사합니다.
  • 프로필사진 Black-Jin 2019.06.26 09:42 신고 방문 감사합니다 :)
  • 프로필사진 김머 2019.07.02 15:44 Flexible / Immediate 설정은 콘솔에 APK 올릴때 설정하는 건가요~?

    이부분이 이해가 안됩니다 ㅡㅜ..
  • 프로필사진 Black-Jin 2019.07.02 16:02 신고 APK 올릴때 설정하지 않습니다. 자동으로 구글 스토어 버전과 설치 되어 있는 앱의 버전이 다르면 실행됩니다. 이때 앱에서 업데이트 코드 초기화시 AppUpdateType.IMMEDIATE 혹은 AppUpdateType.FLEXIBLE에 의해서만 구분되어 동작됩니다. 여기서는 운용하는 방법에 따라 선택하셔야 됩니다. 코드에 어떤 조건을 줘서 이 경우에는 IMMEDIATE 초기화 되게 혹은 FLEXIBLE이 초기화 되게 이런식으로 코드에서 처리하시면 됩니다.
  • 프로필사진 김머 2019.07.03 12:11 답변 감사합니다!

    한가지 더 궁금한게 있습니다.

    IMMEDIATE / FLEXIBLE 체크하는데 이슈? 가 있습니다.

    isUpdateTypeAllowed 이 메소드를 통해서 타입을 체크하는데
    isUpdateTypeAllowed(AppUpdateType.IMMEDIATE),isUpdateTypeAllowed(AppUpdateType.FLEXIBLE) 둘다 트루로 떨어집니다.

    원래 이런건가요?


    when {
    // 업데이트 아님
    appUpdateInfo.updateAvailability() == UpdateAvailability.UPDATE_NOT_AVAILABLE -> {
    next?.invoke()
    }
    // 권장 업데이트
    appUpdateInfo.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE && appUpdateInfo.isUpdateTypeAllowed(AppUpdateType.FLEXIBLE) -> {
    Toast.makeText(context, "FLEXIBLE", Toast.LENGTH_SHORT).show()
    appUpdateManager?.startUpdateFlowForResult(
    appUpdateInfo,
    AppUpdateType.FLEXIBLE,
    context as Activity,
    REQUEST_CODE)
    }
    // 강제 업데이트
    appUpdateInfo.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE && appUpdateInfo.isUpdateTypeAllowed(AppUpdateType.IMMEDIATE) -> {
    Toast.makeText(context, "IMMEDIATE", Toast.LENGTH_SHORT).show()
    appUpdateManager?.startUpdateFlowForResult(
    appUpdateInfo,
    AppUpdateType.IMMEDIATE,
    context as Activity,
    REQUEST_CODE)
    }
    }

    이러한 타입을 구분하는 로직을 만들어 타입값에 따라 업데이트를 진행시키려고 합니다.

    하지만 둘다 트루로 떨어지고 그렇다면 타입을 따로 설정할 수 있는 방법이 있는것 인가 의문이 들어 레퍼런스를 찾아보고 구글링 해봐도 딱히 설정하는 방법에

    대해 나오는것 같지 않습니다. 아무래도 이해를 잘못한 것 같아 질문드립니다 ㅠㅠ..
  • 프로필사진 Black-Jin 2019.07.03 16:03 신고 맞습니다. 저도 isUpdateTypeAllowed 통해 분류하면 될 줄 알았는데 몇번에 확인 결과 둘다 true가 나오는 것을 확인했습니다. 이 값은 현재 플레이 스토어 버전이 앱 버전보다 높아 업데이트가 가능한 상태면 그냥 true가 나오는것 같습니다...

    그래서 저는 서버에서 따로 보내주는 인자를 통해 IMMEDIATE, FLEXIBLE 를 분류해주고 있습니다. 한 StackOverFlow를 보니 구글에서 플레이 스토어에 등록할 때 이를 분류해 줄 수 있는 기능을 어서 제공해 주길 바래야 된다고 하네요 ㅠㅜ

    제가 본 StackOverFlow 링크 남기겠습니다.
    https://stackoverflow.com/questions/56377914/what-purpose-does-appupdateinfo-isupdatetypeallowedappupdatetype-immediate-ser
  • 프로필사진 서영락 2019.08.09 10:36 FakeAppUpdateManager를 이용해서 업데이트 테스트를 할 수 있는 방법이 있습니다.
    이미 아실 수도 있겠지만 다른분들도 참고하시면 좋겠네요.

    https://developer.android.com/reference/com/google/android/play/core/appupdate/testing/FakeAppUpdateManager.html?fbclid=IwAR3w2qtT-nkmiwG3CT1nD0BDcMmMCL1BeKZ1J_Bx1wnvuy6TT9plfzHGjmA
  • 프로필사진 Black-Jin 2019.08.09 10:50 신고 감사합니다! 이걸 모르고 스토어에 계속 업데이트를 했네요 :)
  • 프로필사진 덜지 2019.10.28 17:50 신고 스토어에 한번 올라간 앱이라면

    versionCode를 내리고 릴리즈 빌드만으로 계속 테스트 할 수 있습니다.
    APK Signing은 해야겠지만요.
  • 프로필사진 Black-Jin 2019.10.29 18:28 신고 오옷! 그렇게 해도 되는거네요:)
    감사합니다.
  • 프로필사진 조냑 2019.11.06 15:15 잘봤습니다 ㅎㅎㅎ
    git도 하시나요?!!?!?!
    보고싶어서요 ㅠㅠ
  • 프로필사진 Black-Jin 2019.11.06 17:49 신고 안녕하세요! 제 깃허브 주소입니다:)
    https://github.com/dlwls5201
  • 프로필사진 조냑 2019.11.07 09:47 깃허브 감사합니다 ㅎㅎㅎ
    in app update관련해서는 없네용
    그래도 다른거 참고 감사합니다~~
  • 프로필사진 익명 2019.11.13 11:56 비밀댓글입니다
  • 프로필사진 Black-Jin 2019.11.13 16:12 신고 안녕하세요. 왕초보개발자님:)
    완료가 안된다는게 업데이트 화면에서 멈추는건지? 아니면 업데이트는 다 되고 원래 화면으로 돌아왔는데 적용이 안되는건지? 궁금합니다. 만약 후자인 경우 appUpdateManager?.completeUpdate()
    를 호출해야지만 업데이트가 적용됩니다.
  • 프로필사진 익명 2019.11.14 19:11 비밀댓글입니다
  • 프로필사진 조냑 2019.11.27 12:16 update가 3/11 상태로 계속 무한업데이트 되는데 뭐가문젤까요 ㅠㅠ
  • 프로필사진 병쥐 2020.02.01 19:46 신고 안녕하세요 질문이 있어서 글을 남깁니다.
    만약 이전버전에 인앱업데이트 코드가 없고
    새로운 버전에 인앱업데이트 코드를 넣어서 릴리즈할 경우
    이전버전의 사용자가 앱을 켰을 때 새로운버전을 받으라고 뜨는 건가요?
    아니면 해당기능을 사용하려면 이전버전과 새로운버전 각각에 인앱업데이트 코드가 있어야하는건가요?
    감사합니다!
  • 프로필사진 임섬세 2020.02.27 13:06 공식 문서상 애매한 부분이 있는데요

    When you publish your app as an Android App Bundle, the maximum allowed compressed download size of an app using in-app updates is 150MB. In-app updates are not compatible with apps that use APK expansion files (.obb files).

    앱번들 파일은 지원하지 않는다고 하는데 이부분이 현재 앱번들로 플레이스토어에 배포되어있는 앱은 지원 불가능한 것일까요?
  • 프로필사진 MinwooP 2022.06.07 15:09 앱번들 파일로 올릴 때, 150MB 이하의 파일만 가능하다는 것이지, 앱번들 파일을 지원하지 않는 것은 아니라고 알고 있습니다 !
  • 프로필사진 고구마 2022.06.26 15:16 IMMEDIATE로 업데이트 코드를 작성하고 정상적으로 업데이트 화면까지 잘 뜨는 걸 확인했습니다.
    하지만 업데이트 버튼을 누르면 앱이 재시작되는데 여기서 앱 버전이 업데이트 이전 버전하고 동일하다고 나오는데 이건 무슨 문제일까요? 계속 UpdateAvailability가 int UPDATE_AVAILABLE = 2;로 뜹니다 ㅠㅠ 업데이트 무한 루프에 걸린 것처럼요
댓글쓰기 폼