본문 바로가기
Android

특명! 업데이트를 사용자에게 신속하게 전달하라!

by boiledeggishere 2025. 6. 28.

들어가면서,

안녕하세요! 터닝 팀의 안드로이드 개발자 이석준입니다.

 

앱을 개발하다 보면 잦은 버그 수정이나 기능 개선, 대규모 업데이트 등 다양한 이유로 새로운 버전을 지속적으로 출시하게 됩니다. 

 

업데이트가 이뤄진 이후에도 이전 버전을 사용하는 사용자가 있을 수도 있는데요. 이 경우, 최신 버전에 맞춰 개선된 서버나 기능과의 불일치로 인해 앱이 제대로 동작하지 않거나 아예 종료되는 문제가 발생할 수 있습니다.

 

이런 문제는 결국 사용자 이탈로 이어질 가능성이 높기 때문에, 빠르게 업데이트를 안내하는 방식이 중요합니다!

 

많은 서비스들이 이 문제를 해결하기 위해 앱 실행 시 ‘업데이트 안내 팝업’을 띄우는 방식을 사용하고 있습니다. 사용자에게 빠르게 업데이트 소식을 전달할 수 있고, 운영자가 의도한 흐름대로 서비스를 이용하게 만들 수 있는 장점이 있죠.

 

 

터닝에서는 이 문제를 단순히 ‘업데이트 안내를 하자’는 수준이 아니라, '사용자 경험을 해치지 않으면서도 새로운 버전을 신속하게 알릴 수 있는 방법은 무엇일까?'라는 질문에서 출발해 깊이 고민했습니다.

 

그 결과, 적절한 타이밍과 UX 흐름을 고려한 업데이트 안내 팝업 기능을 직접 구현하게 되었고, 지금은 사용자에게 자연스럽게 업데이트를 유도하면서도 앱의 안정성을 유지할 수 있는 기반이 마련되었습니다.

 

이번 글에서는 터닝 팀이 어떻게 업데이트 팝업을 구현했는지, 그리고 그 과정에서 어떤 고민과 선택들이 있었는지에 대해 소개해보려고 합니다.


Semantic Versioning

앱 실행 시 무조건 업데이트 팝업을 띄우는 방식은 피로감을 유발할 수 있기 때문에 지양해야 합니다.
중요한 건, 사용자가 현재 사용하는 앱 버전이 최신 버전보다 낮은 경우에만 안내를 제공해야 한다는 점입니다.
그러려면 당연히 앱 버전 간 비교 로직이 필요하고, 이를 위해선 모두가 이해하고 따를 수 있는 명확한 버전 관리 규칙을 세워야 합니다.

 

터닝은 여기서 Semantic Versioning(시맨틱 버저닝)이라는 개념을 도입하게 됩니다.
시맨틱 버저닝은 소프트웨어 버전을 표준화한 방식으로 관리하기 위한 규칙으로, 보통 MAJOR.MINOR.PATCH 형식으로 버전을 구성합니다.

 

각 버전 요소가 의미하는 바는 다음과 같습니다:

  • MAJOR: 하위 호환이 깨지는, 즉 기존 앱이 더 이상 정상적으로 동작하지 않을 수 있는 수준의 변화가 있을 때 증가합니다. 예를 들어 서버 API 구조가 변경되어 구버전 앱에서 요청을 처리하지 못하게 되는 경우가 이에 해당하죠.
  • MINOR: 새로운 기능이 추가되었지만 기존 기능은 그대로 유지되는 경우 증가합니다.
    하위 호환성을 유지하면서 앱의 기능 범위를 확장하는 경우입니다.
  • PATCH: 눈에 띄는 기능 추가는 없지만, 작은 버그를 수정하거나 성능 개선이 이루어진 경우에 증가합니다. 가장 자주 이루어지는 변경이기도 합니다.

 

이러한 규칙을 기반으로 버전을 관리하면, 사용자에게 꼭 필요한 시점에만 업데이트 안내를 제공할 수 있고, 개발팀 역시 의미 있는 버전 비교를 통해 안정적인 서비스 운영이 가능해집니다.

 

업데이트 권유하기 vs 업데이트 강제하기

앱 버전이 최신이 아니라는 이유만으로 매번 사용자의 이용을 차단한다면, 사용자 경험은 빠르게 무너집니다. 특히 강제 업데이트는 사용자의 현재 상황을 고려하지 못한 채 앱 접근 자체를 막기 때문에, 불편함과 피로도가 누적될 수밖에 없습니다.

 

반면, 단순히 업데이트를 안내만 하고 사용자가 이를 무시할 수 있게 둔다면, 심각한 버그나 서비스 오류를 방치하게 될 위험이 있습니다. 사용자와 운영팀 모두에게 좋지 않은 결과로 이어질 수 있죠.

 

이 딜레마를 해결하기 위해 터닝 팀에서는 다음과 같은 버전 구분 기반의 업데이트 정책을 고안했습니다:

  • PATCH 또는 MINOR 버전 업데이트가 이루어진 경우에는, 사용자가 업데이트 여부를 선택할 수 있도록 권유형 팝업을 띄웁니다. 이 경우, 구버전에서도 앱이 정상적으로 동작할 수 있다는 것이 전제입니다.
  • 반면, MAJOR 버전 업데이트가 이루어진 경우에는, 구버전에서 앱이 정상적으로 작동하지 않을 가능성이 높기 때문에, 강제 업데이트 팝업을 띄워 사용자에게 반드시 업데이트하도록 안내합니다. 업데이트를 하지 않으면 앱을 더 이상 사용할 수 없습니다.

좌측은 권유형 팝업, 우측은 강제 업데이트 팝업

 

 

이처럼 사용자 경험을 해치지 않으면서도 서비스 안정성을 확보하기 위해, 업데이트의 중요도에 따라 팝업의 방식과 강도를 구분하는 전략을 선택했습니다. (멋있는 디자인은 덤입니다!)


 

지금부터 안드로이드에서 위 기능을 어떻게 구현했는지 소개하겠습니다! 

자세한 구현 코드는 글 중간에 첨부된 깃허브 링크에서 확인하실 수 있으니, 궁금하신 분들은 방문해 주세요!

📌 최신 앱 버전 정보 가져오기

앱의 최신 버전 정보를 관리할 때, 사용자의 기기에 설치된 앱을 직접 업데이트하지 않아도 버전 정보를 실시간으로 변경할 수 있어야 합니다. 이를 위해 Firebase의 Remote Config를 사용했습니다.

다음은 구글 파이어베이스 공식 문서에 따르면 Remote Config는 다음과 같이 설명되어 있습니다:

Firebase Remote Config은 사용자가 앱 업데이트를 다운로드할 필요 없이 클라이언트 앱 또는 서버의 동작과 모양을 변경할 수 있는 클라우드 서비스입니다

 

 

앱이 실행되면 다음과 같은 순서로 동작합니다:

  1. Firebase Remote Config에서 최신 앱 버전 정보를 가져옵니다.
  2. 현재 기기에 설치된 앱 버전과 비교합니다.
  3. 버전이 낮을 경우, 팝업을 띄워 업데이트를 유도합니다.

 

버전을 비교하는 로직은 앱 실행 시점에 자동으로 수행되어 설치된 앱이 최신 버전인지 여부를 확인할 수 있습니다.

 

단순히 버전 숫자만 비교하는 데 그치지 않고, 팝업 내에 표시되는 메시지 역시 Remote Config에서 제어합니다.버전이 업데이트될 때마다 새로운 기능 안내나 중요 공지를 함께 보여줄 수 있어 사용자 경험 측면에서도 유용합니다.

 

Remote Config 적용은 공식문서 설명을 참고하시면 어렵지 않게 구현하실 수 있으실겁니다.  

공식문서 - Firebase Remote Config 시작하기

 

터닝에서 RemoteConfig를 연결한 방법은 다음 링크를 참고하세요!

터닝의 RemoteConfig 클래스 보러가기

 

RemoteConfig의 fetch 과정에 대하여...

터닝 앱에서는 Firebase Remote Config를 통해 앱 버전 정보를 내려받는 기능을 전용 클래스인 TerningRemoteConfig에 위임하여, Remote Config 관련 책임을 앱의 다른 영역과 명확히 분리하였습니다.

 

앱이 실행되면 TerningRemoteConfig 인스턴스가 생성되고, 이 클래스의 addOnVersionFetchCompleteListener 메소드를 통해 RemoteConfig 서버에서 데이터를 가져오는 로직이 실행됩니다. 

 

addOnVersionFetchCompleteListener는 내부적으로 FirebaseRemoteConfig.fetchAndActivate() 메서드를 호출합니다. 이 메서드는 비동기 작업을 나타내는 Task<Boolean> 객체를 반환하며, 이 객체의 리스너를 통해 콜백으로 전달됩니다. 

 

Remote Config의 fetch 작업은 비동기이기 때문에 리스너 내부 코드는 나중에 실행되고, 함수는 값을 받기 전에 먼저 리턴을 해버리게 됩니다. 즉, 다음과 같은 코드는 원하는 대로 동작하지 않습니다.

fun addOnVersionFetchCompleteListener(): String {
    var data: String = ""
    remoteConfig.fetchAndActivate().addOnCompleteListener {
        data = remoteConfig.getString("latest_version")
    }
    return version // 이 시점엔 아직 fetch가 끝나지 않음
}

 

 

위와 같은 문제를 해결하기 위해 addOnVersionFetchCompleteListener는 콜백 함수를 매개변수로 받도록 설계했습니다. 

데이터를 성공적으로 받아오면, 리스너 내부에서 콜백을 호출하여 결과를 호출한 쪽으로 전달하는 구조입니다:

fun addOnVersionFetchCompleteListener(callback: (String) -> Unit) {
    remoteConfig.fetchAndActivate().addOnCompleteListener { task ->
        if (task.isSuccessful) {
            callback(remoteConfig.getString("latest_version"))
        }
    }
}

 

 

📌  기기 앱 버전 정보 가져오기

안드로이드 앱의 build.gradle 파일에선 버전에 대해 두 개의 값을 관리합니다. 

  • versionCode
    앱의 내부 버전 번호로, 정수값으로만 표현됩니다.

    구글 PlayStore에서는 이 값을 기준으로 업데이트 여부를 판단한다고 합니다.
  • versionName
    사용자에게 표시되는 외부용 버전 문자열입니다.

    보통 1.0.0 같은 버전 체계(semver)에 맞춰 표현됩니다.

 

두 값은 표현 방식은 다르지만, 의미적으로는 동일한 버전을 나타내야 합니다.
예를 들어 versionName이 1.0.0이라면, versionCode는 101000로 매핑될 수 있습니다.

 

터닝에서는 versionName 값을 Remote Config에서 내려받은 최신 버전 정보와 비교하여 사용자의 앱이 최신 버전인지 판단합니다.

이를 위해 앱 내에서 versionName을 가져오는 코드가 필요합니다. 다음은 versionName을 가져오는 과정입니다:

  1. PackManager 가져오기
    PackageManager는 앱의 패키지 정보에 접근할 수 있도록 해주는 시스템 서비스입니다.

    ApplicationContext를 통해 접근할 수 있습니다.
  2. 앱의 packageName 가져오기
    현재 앱의 고유한 패키지명을 의미하며, PackageManager에 정보를 요청할 때 필요합니다.

    이 정보도 ApplicationContext에 포함돼 있습니다.
  3. 패키지 정보 조회
    PackageManager.getPackageInfo(packageName, flags)를 호출하면 해당 앱의 다양한 정보를 포함한 PackageInfo 객체가 반환됩니다.

    만약 packageName이 잘못되었을 경우, PackageManager.NameNotFoundException이 발생할 수 있으므로 예외 처리가 필요합니다.
  4. versionName 추출
    성공적으로 반환된 PackageInfo 객체에서 versionName 필드를 통해 앱 버전을 확인할 수 있습니다.

위 과정을 하나의 ApplicationContext의 확장함수로 구현했습니다!

fun Context.getVersionName(): String? = runCatching {
    packageManager.getPackageInfo(packageName, 0).versionName
}.getOrNull()

 

 

확장함수를 구현해둔 덕에 어디서든 다음처럼 간단히 호출할 수 있습니다!

val versionName = context.getAppVersionName()

 

📌  버전 비교

터닝 앱은 앱 버전 비교를 위해 SemVer라는 써드파티 라이브러리를 사용하고 있습니다.
이 라이브러리는 Semantic Versioning(시맨틱 버저닝) 방식으로 정의된 문자열 버전을 SemVer 클래스로 파싱하여, 요소별 비교 및 정렬을 간편하게 처리할 수 있도록 도와줍니다.

swiftzer/semver 깃허브

 

또한, 터닝은 클린 아키텍처를 기반으로 설계되어 있기 때문에, 도메인 레이어는 Java 혹은 Kotlin으로만 구성되어야 합니다. SemVer는 순수 Kotlin으로 작성된 라이브러리이므로, 도메인 레이어에서도 의존성 부담 없이 안전하게 사용할 수 있습니다.

 

터닝에서는 이 SemVer 클래스를 통해 서버에서 내려받은 최신 앱 버전기기에 설치된 앱 버전을 비교합니다. 이후 요소 단위로 분석된 결과에 따라, 사전에 정의한 버전별 팝업 정책을 적용해 사용자에게 알맞은 업데이트 알림을 띄웁니다.

 

터닝의 버전 비교 로직이 궁금하시면 다음 링크를 참고하세요!

터닝의 버전 비교 유즈케이스 보러가기


👋 마무리

이렇게 터닝에서 정의한 업데이트 정책과, 이를 안드로이드 앱에서 어떻게 구현했는지 간단히 소개해드렸습니다.

 

터닝 앱은 클린 아키텍처 기반으로 설계되어 있어 전체 구조가 다소 복잡할 수 있습니다. 그래서 이번 글에서는 핵심 컨셉 위주로 정리해보았습니다.

 

더 자세한 구현이 궁금하신 분들은 터닝 깃허브 링크를 참고해 소스코드를 직접 확인해보세요!

 

GitHub - teamterning/Terning-Android: 💚 지금이 안드의 터닝포인트~ 💚

💚 지금이 안드의 터닝포인트~ 💚. Contribute to teamterning/Terning-Android development by creating an account on GitHub.

github.com

 

Android_이석준

캘린더를 맡은 Android 개발자 이석준입니다.

깃허브

'Android' 카테고리의 다른 글

앱을 3가지 방식으로 연다고? (딥링크 구조 도입기)  (1) 2025.06.15