앱 업데이트 유도 기능 추가하기

업데이트 유도 실행화면
4.2.0 버전에 추가된 앱 업데이트 유도 기능

원티드 안드로이드앱 4.2.0 버전에는 앱 업데이트 유도 기능이 추가되었습니다. 대부분의 사용자가 경험 해봤을 법한 단순한 다이얼로그에 최신 버전 유무 안내. 그리고 스토어로 이동되는 UI가 추가되었습니다.

원티드 제품팀이 커지면서 앱의 경우 앱개발 서클로 나뉘어서 개발을 진행하고 있습니다. 6월초 Crashlytics로 Crash-Free 수치가 많이 떨어졌고, 이 후에 어떻게 이 부분을 빠르게 대응할 수 있을 지 방법을 논의하다가 나온 대안중 하나가 업데이트 유도 입니다.

업데이트 유도 기능
Crash-free 수치는 앱 서클의 KPI입니다.

업데이트 유도 화면은 서비스 안정성을 유지하는데 필요한 기능 중 하나라고 생각합니다. 비록 사용자들은 불편한 UX를 경험할 수 있는 요소기는 하지만, Crash를 경험하는것과 비교해봤을 때는 좀 더 낫지 않나 생각합니다. 그리고 서비스 운영 정책이 변경되거나, 더 이상 특정 버전 이하를 지원할 수 없는 상태가 되었을 때는 빠르게 최신 버전으로 업데이트를 유도할 수 있는 장치가 필요하다고 생각했습니다.

어떻게 만들 것인가?

단순하게 앱 초기 실행 시 업데이트 유도 팝업을 보여주는 것에서 시작하였습니다. 위에 추가된 화면처럼 단순한 형태입니다. 대략적으로 어떻게 실행될 지 정리해보면 아래와 같습니다.

업데이트 유도 기능
업데이트 유도 기능 PRD. 단순합니다.

최신 버전을 체크하는 방법은 업데이트 유도 기능 제공에서 가장 중요한 로직입니다. 다양한 방법으로 해결이 가능하지만, 일반적으로 스토어의 페이지를 파싱해서 등록된 버전정보를 가져와 현재 앱버전과 다를경우 업데이트 화면으로 이동시키는 방법을 이용합니다. 이 방법을 사용할 경우 1) 버전업 주기가 짧은 서비스는 업데이트 유도 팝업이 자주 노출될 수 있다. 2) 해당 페이지 컨텐츠가 변경되면 파싱 로직도 수정이 필요하다 등이 잠재적인 이슈가 될 수 있습니다.

업데이트 유도 기능을 우리가 제어할 수 있고, 확장성을 고려하며, 최소한의 비용으로 원하는 기능을 추가할 수 있는 방법을 고민하였습니다. 그러다 Firebase Firestore라는 것을 알게 되었습니다. 이 서비스를 선택한 이유는 RestAPI 형태로 사용이 가능하며, NoSQL 기반 데이터베이스이므로 기능 확장 등에도 용이 하다고 생각합니다. RemoteConfig 를 이용할 수도 있지만, 저희는 앱 서비스의 용량이 커지는 것을 원하지 않았습니다.

Firestore 설정

Firebase Console에 있는 메뉴 중 개발 - Database - Cloud Firebase를 선택합니다.

문서의 기본 데이터 구성은 {컬렉션-도큐먼트-컬렉션-도큐먼트-…} 형식으로 구성됩니다. Rest API로 사용하기 문서를 확인하여 원하는 형태의 API로 앱에서 사용하려는 데이터들을 정의합니다.

데이터를 구성하였으면 Rest API를 문서에서 정의되어있는 URL에 한번 접근해 봅니다. 만약 최신 버전정보만 세팅해 두었다고 한다면, 아래와 같은 JSON을 확인할 수 있습니다.

{
  "name": "{Your firestore collections path}",
  "fields": {
    "{Defined field name, eg. version }": {
      "stringValue": "{target app version for update, eg. 4.2.0}"
    }
  },
  "createTime": "{timestamp}",
  "updateTime": "{timestamp}"
}

버전 정보는 해당 버전과 비교하여 설치된 앱이 더 작은 버전일 경우 업데이트 유도 기능이 활성화 된다는 의미입니다. 이제 앱에서 버전 체크 기능을 추가하면 개발이 끝날 것 같습니다.

API 사용 로직 추가하기

API는 Retrofit라이브러리를 이용하고 있습니다. Retrofit 객체 생성 부분은 공식 문서를 참조하면 되며, API 를 다음과 같이 추가하였습니다.

interface FirebaseService {

    @GET("/v1beta1/projects/wanted-mo/databases/(default)/documents/{Your firestore collections path}")
    Observable<FirebaseContract.FirebaseEntity<FirebaseData.AppVersionEntity>> getAppVersion();

}

API Response 클래스는 아래와 같습니다.

class FirebaseContract {

    data class FirebaseEntity<T> (
        val name : String = "",
        val createTime : String = "",
        val updteTime : String = "",
        var fields : T
    )

    data class AppVersionEntity (
        val version : StringValue = StringValue("")
    )

    data class StringValue(
        val stringValue : String
    )
}

Firestore의 기본 응답 구조는 실제 원하는 값 (fields)를 generic으로 설정하고, 각 응답 모델에 맞춰서 사용할 수 있도록 하였습니다. 콘솔에서 설정한 실제 앱에서 사용할 데이터는 AppVersionEntity 구성해 두었고, fields 객체로부터 접근 가능합니다.

Firestore에서 제공하는 데이터 타입에 따라 몇 가지 추가 구성이 더 필요할 수 있습니다. 만약, Boolean 타입으로 데이터를 정의해 두었다면 fields 하위에 아래와 같은 object로 전달됩니다.

"{Defined field name}" : {
    "boolValue" : "{true/false}"
}

데이터타입에 맞는 형식을 Respose에서 사용할 수 있도록 잘 구성하면 됩니다.

최신 버전 여부 체크

이제 앱 버전정보를 표시하기 위하여 두 개의 버전 정보를 가진 String을 비교하여 최신버전 여부를 체크해야 합니다. Comparable 을 구현한 클래스를 하나 만들었습니다.

class StringVersion(private val version: String) : Comparable<StringVersion> {

    init {
        if(!version.matches(Regex("[0-9]+(\\.[0-9]+)*")))
            throw IllegalArgumentException("Invalid version format [$version]")
    }

    override fun compareTo(other: StringVersion): Int {
        other ?: return 1
        val thisParts = version.split(".")
        val thatParts = other.version.split(".")
        val length = Math.max(thisParts.size, thatParts.size)
        for(i in 0 until length) {
            val thisPart = if( i < thisParts.size ) thisParts[i].toInt() else 0
            val thatPart = if( i < thatParts.size ) thatParts[i].toInt() else 0
            if( thisPart < thatPart ) return -1
            if( thisPart > thatPart ) return 1
        }
        return 0
    }

    override fun equals(other: Any?): Boolean = other?.let{
        if( other is StringVersion ) {
            compareTo(other) == 0
        } else {
            false
        }
    } ?: false

}

현재 버전명은 PackageInfo로 확인이 가능하고, 업데이트가 필요한지 체크는 API로 확인이 가능하니 이제 업데이트 유도 기능을 유저에게 제공이 가능합니다.

response.fields.version.stringValue?.let {
    if( StringVersion(currentVersion).compareTo(StringVersion(it)) < 0 ) {
        // Show update dialog
    } else {
        // Do
    }
}

필요에 따라 업데이트 내용 안내 표시, 강제/선택적 업데이트 유도 등의 부가적인 기능 추가도 가능합니다. API사용 시 발생되는 비용 정책은 여기를 참조하면 됩니다.


참조