무엇이 바뀌고 왜 지금인가

Google은 모든 Android 앱이 청구 코드를 최신 상태로 유지하도록 요구합니다. 2026년 8월 31일까지 모든 새 앱과 기존 앱의 모든 업데이트는 Google Play Billing Library v8 이상을 사용해야 합니다 (2026년 5월에 출시된 v9도 조건을 충족합니다). Play Console에서 2026년 11월 1일까지 일회성 연장을 요청할 수 있습니다. 이는 곧 게시 관문입니다. 이미 설치된 앱은 계속 동작하지만, v8 이상이 되기 전까지는 어떤 업데이트도 배포할 수 없습니다.

문제는 이 일이 매년 되풀이된다는 점입니다. v8은 2026년의 최소 기준이고, v9은 2027년, 그 뒤로도 이어집니다. 버전이 올라갈 때마다 API가 삭제되고 코드 수정을 강제합니다. Google 자체 Play Billing 코드랩도 아직 v5에 머물러 있고 "deprecated" 배너가 붙어 있는데, 이는 이 흐름이 얼마나 빠른지를 보여 줍니다.

이 코드랩은 오래가는 해법을 보여 줍니다. RevenueCat으로 이전하면 RevenueCat이 Billing Library를 대신 관리하므로 다시는 버전을 쫓을 필요가 없습니다. 이 과정에서 다음을 합니다.

  • v8과 v9이 정확히 무엇을 삭제했는지 확인합니다 (그래서 BillingClient를 직접 패치하더라도 이 페이지가 도움이 됩니다).
  • RevenueCat을 추가하고 기존 launchBillingFlow 구매 코드를 교체합니다.
  • 기존 구독자를 옮겨서 결제 중인 사용자가 계속 접근 권한을 유지하게 합니다.
먼저 이것부터: RevenueCat을 Google Play에 연결하세요. 이 코드랩은 Google Play Store에 연결된 RevenueCat 프로젝트가 이미 있고, 서비스 계정 자격 증명이 설정되어 있으며, 상품을 가져온 상태라고 가정합니다. 아직 설정하지 않았다면 먼저 RevenueCat Google Play 연동 코드랩을 완료한 뒤 여기로 돌아오세요.
대상 독자: 오늘날 원시 Google Play Billing Library(BillingClient)를 직접 연동하고 있는 Android 앱입니다. 예제는 모두 Kotlin입니다.

두 가지 이전 경로

마감을 넘기는 방법은 두 가지입니다. 둘 다 유효하지만, 이 코드랩은 두 번째를 권장합니다.

경로 해야 할 일 내년
BillingClient를 직접 v8으로 패치 삭제된 API(3단계)를 자체 청구 코드에서 다시 작성 v9, v10에서 같은 일을 반복...
RevenueCat으로 이전 청구 코드를 한 번만 교체하면 RevenueCat이 Billing Library를 책임집니다 RevenueCat SDK만 업데이트하고 BillingClient 재작성은 없음

단일 상품만 판매했고 앞으로도 페이월, 실험, 크로스 플랫폼 지원을 추가할 계획이 전혀 없다면 직접 패치해도 괜찮습니다. 하지만 대부분의 앱에서는 첫 번째 경로에 매년 드는 비용이 두 번째를 택할 이유가 됩니다.

한 번에 다 할 필요는 없습니다. RevenueCat은 단계적 이전(observer mode)을 지원합니다. 이 방식에서는 RevenueCat이 먼저 기존 청구 코드와 나란히 동작합니다. 자세한 내용은 9단계에서 다룹니다.

v8과 v9이 깨뜨린 것

참고로, 원시 Billing Library가 v8(2025년 6월 30일 출시)에서 삭제한 내용은 다음과 같습니다. 직접 패치한다면 이것이 작업 목록입니다. RevenueCat으로 이전한다면 이 코드는 삭제하면 됩니다.

v8에서 삭제됨 대체
querySkuDetailsAsync(), SkuDetails queryProductDetailsAsync(), ProductDetails, QueryProductDetailsParams
queryPurchaseHistoryAsync(), PurchaseHistoryRecord queryPurchasesAsync() (활성 구독과 소비되지 않은 일회성 구매만 반환하며, 무효화된 이력은 서버 Voided Purchases API가 필요함)
enablePendingPurchases() (인자 없음) enablePendingPurchases(PendingPurchasesParams)
ProrationMode setSubscriptionReplacementMode()를 통한 ReplacementMode
setSkuDetails() setProductDetailsParamsList()

v9(2026년 5월 19일)은 변경 폭이 더 작지만 여전히 코드에 영향을 줍니다. 차단된 Play Store 오류를 ERROR에서 BILLING_UNAVAILABLE로 바꿨고, 외부 결제 필드를 nullable로 만들었으며, 타깃 SDK를 35로 올렸습니다. (onProductDetailsResponse 리스너 시그니처 변경과 새 청구 하위 응답 코드는 v8에서 이미 도입되었습니다.) 최소 SDK는 v8.1부터 API 23(Android 6.0)으로 올라갔습니다.

이 패턴은 끝나지 않습니다. 이 작업 중 상당 부분이 앱과는 아무 상관 없는 배관 작업이라는 점에 눈여겨보세요. 바로 그 일을 RevenueCat이 흡수합니다.

RevenueCat이 쳇바퀴를 끝내는 방법

RevenueCat Android SDK는 Google Play Billing Library를 대신 번들하고 관리합니다. 현재 SDK는 Billing Library 8.3.0을 전이 의존성으로 함께 제공하므로, com.android.billingclient:billing을 직접 추가하지 않아도 되고, BillingClient를 전혀 호출하지 않습니다.

매년 돌아오는 마감이라는 관점에서 이것이 뜻하는 바는 다음과 같습니다.

  • 지금: RevenueCat SDK를 한 번만 도입합니다 (이 코드랩의 나머지 내용).
  • 내년, v9이나 v10이 필수가 될 때: RevenueCat SDK 버전만 올리면 됩니다. BillingClient 재작성도, 삭제된 API 찾기도 없습니다.

직접 만들었어야 할 것들도 함께 얻습니다. 단일 진실 공급원인 customerInfo.entitlements, 서버 측 영수증 검증, 웹훅, 그리고 이후 Paywalls, Experiments, Customer Center에 대한 접근까지 포함됩니다.

구매의 소유자는 하나입니다. RevenueCat이 Billing Library를 소유하므로 Google에 대한 구매 확인(acknowledge)도 대신 처리합니다. 같은 구매를 자체 코드에서 또 확인해서는 안 됩니다 (Google은 3일 이내에 확인되지 않은 구매를 자동으로 환불합니다). 두 연동을 동시에 돌리지 않는 방법은 9단계에서 다룹니다.

RevenueCat 추가와 설정

앱 모듈의 Gradle 파일에 SDK를 추가하세요 (최신 버전 사용):

kotlin
// app/build.gradle.kts
dependencies {
    implementation("com.revenuecat.purchases:purchases:10.10.0")
    // Optional: paywalls + Customer Center (requires minSdk 24)
    // implementation("com.revenuecat.purchases:purchases-ui:10.10.0")
}
// Remove your old: implementation("com.android.billingclient:billing:...")

SDK는 minSdk 23(Android 6.0)을 요구합니다. Application 클래스에서 한 번만 설정하세요.

kotlin
// MainApplication.kt
class MainApplication : Application() {
    override fun onCreate() {
        super.onCreate()

        Purchases.logLevel = LogLevel.DEBUG
        Purchases.configure(
            PurchasesConfiguration.Builder(this, "goog_YOUR_PUBLIC_SDK_KEY")
                // .purchasesAreCompletedBy(PurchasesAreCompletedBy.REVENUECAT) // default: full migration
                .build()
        )
    }
}
public 키를 사용하세요. Google Play public SDK 키는 goog_로 시작하며, RevenueCat 대시보드의 Project Settings > API keys에서 찾을 수 있습니다. secret 키를 절대 앱에 포함하지 마세요. 기본값인 PurchasesAreCompletedBy.REVENUECAT은 RevenueCat이 구매를 완료(확인)한다는 뜻입니다. MY_APP으로 바꾸는 것은 9단계의 단계적 경로에서만 하세요.

청구 개념 매핑하기

RevenueCat은 하드코딩된 SKU와 자체 "프리미엄 여부" 로직을 작고 오래가는 모델로 대체합니다. 다음 항목을 대시보드에서 설정하세요.

원시 Billing Library RevenueCat
SKU / ProductDetails (예: premium_monthly) Product (RevenueCat에 가져온 스토어 상품)
(SKU 목록을 하드코딩함) Offering(사용자에게 보여 주는 것) 안의 Package(월간/연간 같은 역할)
자체 isPremium 불리언 Entitlement (예: pro), customerInfo로 확인

대시보드에서 기존 Google Play 상품을 가져오고, entitlement(예: pro)를 만든 뒤, 각 상품을 거기에 Attach하세요.

이것이 이전 작업의 절반입니다. 상품을 entitlement에 attach하면 그 상품을 이전에 구매한 모든 고객에게 해당 entitlement가 부여됩니다. 따라서 RevenueCat이 기존 구독자의 구매를 알게 되면(8단계), 그 사용자의 pro 접근 권한이 자동으로 복원됩니다.

구매 흐름 교체하기

기존 queryProductDetailsAsync / launchBillingFlow / 구매 확인 코드를 삭제하고 두 번의 호출로 대체하세요. 먼저 판매할 대상을 가져옵니다.

kotlin
// Coroutines: fetch the current offering and its packages
val offerings = Purchases.sharedInstance.awaitOfferings()
val packages = offerings.current?.availablePackages.orEmpty()
// Show `packages` in your UI (each pkg.product carries the localized price)

그런 다음 사용자가 선택한 package로 구매를 진행합니다.

kotlin
try {
    val result = Purchases.sharedInstance.awaitPurchase(
        PurchaseParams.Builder(activity, selectedPackage).build()
    )
    val customerInfo = result.customerInfo
    if (customerInfo.entitlements["pro"]?.isActive == true) {
        // Unlock pro. RevenueCat already acknowledged the purchase with Google.
    }
} catch (e: PurchasesTransactionException) {
    if (!e.userCancelled) {
        // Show a real error; userCancelled just means the user backed out.
    }
}

코루틴을 사용하지 않는다면 콜백 방식의 등가 API(getOfferingsWith, purchaseWith)도 있습니다.

구매 확인 코드는 이제 필요 없습니다. 기본 설정에서는 RevenueCat이 Google과의 트랜잭션을 대신 마무리합니다. 이중으로 확인하지 않도록 기존 acknowledgePurchase / consumeAsync 호출을 삭제하세요.

이쪽 SDK가 처음이라면 Android 코드랩상품과 가격 가져오기 가이드에서 더 깊이 다룹니다.

기존 구독자 이전하기

이 부분이 가장 중요합니다. 현재 결제 중인 사용자가 접근 권한을 잃어서는 안 됩니다. 두 가지 요소가 있고, 두 가지 모두 필요합니다.

1. 새 구매: 자동으로 추적하기

Google Play 계정이 이미 연결된 상태에서( Google Play 연동 코드랩 참고), Google Play 서버 알림을 설정하고 Track new purchases from server-to-server notifications를 켜세요. 그러면 RevenueCat이 모든 구매를 기록합니다. 아직 SDK가 포함되지 않은 앱 버전에서 발생한 구매도 기록됩니다.

2. 기존 구매: 한 번만 동기화하기

기존 BillingClient 코드로 이미 구독한 사용자에 대해서는, RevenueCat이 적용된 빌드를 처음 실행할 때 syncPurchases()한 번 호출하세요. OS 로그인 프롬프트 없이 사용자의 기존 Google Play 구매를 RevenueCat에 전송합니다. 6단계의 상품-entitlement attach와 결합하면 사용자의 pro 접근 권한이 자동으로 복원됩니다.

kotlin
// Run ONCE per subscriber, the first time they open the RevenueCat build.
if (!prefs.getBoolean("rc_migrated", false)) {
    Purchases.sharedInstance.syncPurchasesWith(
        onError = { /* retry later */ },
        onSuccess = { _ -> prefs.edit().putBoolean("rc_migrated", true).apply() }
    )
}
매 실행이 아니라 한 번만 동기화하세요. 모든 사용자에 대해 매 실행마다 syncPurchases를 호출하면 지연이 늘고, 의도치 않게 고객들이 서로 별칭으로 묶일 수 있습니다. 일회성 플래그 뒤에 두거나(또는 기존 시스템은 "구독 중"이라고 하는데 RevenueCat은 그렇지 않을 때만 호출), OS 로그인을 유발할 수 있는 restorePurchases()는 사용자가 직접 누르는 "복원" 버튼 뒤에서만 사용하세요.
안정적인 App User ID를 사용하세요. 앱에 자체 계정이 있다면 그 사용자 id로 RevenueCat을 설정하거나(appUserID), 동기화 전에 Purchases.sharedInstance.logIn(userId)를 호출하세요. 그러면 동기화된 구매가 익명 id가 아니라 올바른 고객에게 연결됩니다.
과거 데이터에는 한계가 있습니다. 현재 SDK에서 syncPurchases활성 구독과 소비되지 않은 일회성 구매만 동기화합니다. 만료된 구매와 과거 구매까지 채워 넣으려면(정확한 차트와 평생 이력을 위해) 대시보드에서 Google Historical Import를 실행하세요. 이 기능은 2023년 7월까지의 이력을 가져오지만, 90일보다 오래전에 만료된 구매 토큰에는 알려진 공백이 있습니다.

라이선스 테스터로 테스트하기

출시하기 전에, 실제 기기에서 요금이 청구되지 않는 상태로 이전을 검증하세요.

  1. Play Console에서 Setup > License testing 아래에 테스터를 추가하고, 빌드를 internal 또는 closed testing 트랙에 게시합니다.
  2. 기존 구독이 있는 테스터로 새 RevenueCat 빌드를 설치하고, 일회성 syncPurchases가 실행되어 pro entitlement가 활성화되는지 확인합니다.
  3. 테스터로 새 흐름을 통해 구매를 완료하고, entitlement가 잠금 해제되며 트랜잭션이 RevenueCat에 나타나는지 확인합니다.
  4. RevenueCat 대시보드에서 해당 고객을 열어 구매와 entitlement가 기록되었는지 살펴봅니다.

단계적 롤아웃을 원하나요? observer mode를 사용하세요

한 번의 릴리스로 청구 코드를 제거할 준비가 되지 않았다면, RevenueCat을 PurchasesAreCompletedBy.MY_APP으로 설정하세요. 그러면 기존 코드가 여전히 구매를 완료(확인)하는 동안 RevenueCat은 구매와 entitlement를 기록합니다. 기존 구매 코드를 제거한 뒤에는 기본값인 PurchasesAreCompletedBy.REVENUECAT으로 전환하세요.

양쪽이 함께 확인하게 두지 마세요. 구매를 완료하는 주체는 자체 코드(observer mode)이거나 RevenueCat(전체 이전) 중 하나여야 하며, 둘 다는 안 됩니다. 이중 확인과 이중 연동은 미묘하고 디버깅하기 어려운 버그를 일으킵니다.

정리와 얻는 것

Android 앱을 원시 Billing Library에서 RevenueCat으로 이전했습니다.

  • BillingClient(그리고 v8에서 삭제된 API로 인한 모든 소동)를 getOfferings + purchase로 교체했습니다.
  • 직접 짜맞춘 SKU 로직 대신 단일 pro entitlement로 접근 권한을 모델링했습니다.
  • 일회성 syncPurchases와 상품-entitlement attach로 기존 구독자를 옮겼고, Historical Import로 이력을 채웠습니다.

얻는 것: 2026년 8월 31일 마감을 넘겼고, v9, v10, 그 이후가 필수가 되더라도 RevenueCat SDK만 올리면 됩니다. 더 이상 매년 Billing Library를 이전할 일이 없습니다.

계속하기