비자발적 이탈이라는 문제

모든 취소가 떠나고 싶어서 떠난 고객은 아닙니다. 상당수는 그저 결제 실패입니다. 만료된 카드, 잔액 부족, 은행 거절이 원인이죠. 사용자는 취소를 선택한 적이 없는데, 회복 장치가 없으면 조용히 접근 권한을 잃고 여러분은 매출을 잃습니다.

RevenueCat의 State of Subscription Apps 2026에 따르면 Google Play 구독 취소의 약 3분의 1이 비자발적 청구 실패였습니다(App Store의 비율보다 대략 두 배 높습니다). 보고서는 이렇게 표현합니다. 청구 문제를 고치는 것이야말로 가장 확실한 성장 지렛대 중 하나라고요. 이 코드랩에서 그 방법을 보여 드립니다.

이번에 할 일은 다음과 같습니다.

  • App Store Connect와 Google Play에서 청구 유예 기간을 켜서, 스토어가 카드를 재시도하는 동안 접근 권한이 이어지게 합니다.
  • RevenueCat으로 청구 재시도 중인 활성 구독을 감지합니다(iOS, Android, React Native).
  • Customer Center 또는 스토어의 managementURL로 사용자가 결제 수단을 고치게 합니다.
  • BILLING_ISSUE 웹훅에 대응해 앱 밖에서 사용자의 재참여를 유도합니다.
대부분 설정, 코드는 조금뿐입니다. 여기서 가장 큰 성과는 스토어와 대시보드 설정에서 나옵니다. 앱 안 코드는 짧은 감지 확인과 바로 붙이는 Customer Center 정도입니다.

자발적 이탈과 비자발적 이탈

전혀 다른 두 사건이 똑같이 "구독이 끝났다"로 나타납니다. 둘을 다르게 다뤄야 합니다.

유형 원인 회복 방법
자발적 사용자가 취소를 누름 취소 흐름 제안, 윈백, 가치 리마인더
비자발적 갱신 시점에 결제 실패 유예 기간, 청구 재시도, 결제 수정 유도

이 코드랩이 겨냥하는 것은 비자발적 이탈입니다. 회복 사슬은 이렇습니다. 스토어가 일정 기간 동안 카드를 계속 재시도하고(이 기간이 유예 기간입니다), 사용자는 그동안 접근 권한을 유지하며, 여러분의 앱은 기간이 끝나기 전에 결제 수단을 업데이트하도록 사용자를 살짝 밀어 줍니다.

RevenueCat이 둘을 대신 구분해 줍니다. 자발적 취소는 entitlement에 unsubscribeDetectedAt을 설정하고, 비자발적 실패는 billingIssueDetectedAt을 설정합니다. 후자는 6단계에서 사용합니다. (자발적 이탈은 별도의 Customer Center 코드랩에서 다룹니다.)

App Store Connect에서 유예 기간 켜기

청구 유예 기간을 켜면 Apple이 실패한 갱신을 재시도하는 동안 구독자가 접근 권한을 유지합니다. 그래서 Apple이 제때 결제를 회복하면 서비스나 매출에 끊김이 없습니다.

  1. App Store Connect에서 앱을 열고 사이드바의 Subscriptions를 클릭하세요.
  2. Billing Grace Period 섹션에서 Set Up Billing Grace Period를 클릭하세요.
  3. 기간을 고르세요. 3일, 16일, 28일 중 하나이며(앱의 모든 구독에 일괄 적용됩니다).
  4. 갱신 유형(전체 갱신, 또는 유료에서 유료로 이어지는 갱신만)과 환경(샌드박스만, 또는 프로덕션과 샌드박스)을 고르세요.
  5. Confirm을 클릭하세요.
주간 구독은 상한이 있습니다. 16일이나 28일을 골라도 주간 구독은 최대 6일까지만 유예 기간을 받습니다. 유예 기간이 구독 자체보다 길어지지 않게 하기 위해서죠. 이 작업에는 Account Holder, Admin, App Manager 역할이 필요합니다.
iOS 16.4 이상은 자동으로 돕습니다. 유예 기간이 켜져 있으면 iOS가 재시도 기간 동안 직접 고객에게 청구 문제를 바로잡도록 안내할 수 있습니다. 여러분이 앱 안에서 만드는 것에 더해서요.

Google Play에서 유예 기간과 계정 보류 켜기

Google Play의 회복 단계는 두 가지입니다. 유예 기간 동안에는 Google이 카드를 재시도하는 사이에도 사용자가 접근 권한을 유지합니다. 이마저 실패하면 구독은 계정 보류로 넘어가는데, 이때 사용자는 접근 권한을 잃는 반면 Google은 계속 재시도합니다. 두 단계 중 어느 쪽에서든 회복하면 구독은 끊김 없이 이어집니다.

  1. Google Play Console에서 Monetize with Play → Products → Subscriptions로 이동하세요.
  2. 구독을 열고 자동 갱신되는 기본 요금제(base plan)를 선택하세요. 유예 기간과 계정 보류는 기본 요금제 단위로 설정합니다.
  3. 유예 기간을 설정하세요(갱신 결제가 해결되지 않는 동안 사용자가 접근 권한을 유지하는 기간).
  4. 계정 보류 기간을 설정하세요(접근이 중단된 뒤에도 Google이 재시도를 이어 가는 기간).
계정 보류는 이제 자동 계산됩니다. 2025년 12월 1일부터 Google은 계정 보류 기간을 고정 30일이 아니라 기본값으로 60일에서 유예 기간을 뺀 값으로 계산합니다. 유예 기간과 계정 보류를 합치면 최소 30일은 되어야 합니다. 값을 코드에 박아 두지 말고 Console에서 현재 값을 읽으세요.
접근 규칙: 유예 기간 동안에는 접근을 유지하고, 계정 보류 동안에는 접근을 막습니다. 이 처리는 RevenueCat이 대신 해 줍니다. 유예 기간에는 entitlement가 활성 상태로 유지되고, 보류에 들어가면 비활성이 됩니다(자세한 내용은 다음 단계에서).

RevenueCat이 청구 문제를 드러내는 방식

이것이 감지를 단순하게 만드는 핵심 통찰입니다. 구독이 유예 기간에 있는 동안 RevenueCat은 entitlement를 isActive: true로 유지하고(사용자는 여전히 접근 권한이 있습니다), 실패가 감지된 시각으로 billingIssueDetectedAt을 설정합니다.

그래서 "결제 중인 고객인데 방금 카드가 실패했다"라는 신호는 다음과 같습니다.

text
entitlement.isActive == true  AND  entitlement.billingIssueDetectedAt != null
        =>  active subscriber, in billing retry, nudge them to fix payment

entitlement의 관련 필드는 다음과 같습니다(이름은 플랫폼 전반에서 동일합니다).

  • isActive: 고객이 지금 접근 권한이 있는지. 유예 기간 동안 true로 유지됩니다.
  • billingIssueDetectedAt: 날짜/시각 값. 결제 실패가 감지되면 설정되고, 결제가 회복되면 지워집니다. 문제가 없으면 null입니다.
  • willRenew: 구독이 갱신되도록 설정되어 있는지.
  • unsubscribeDetectedAt: 자발적 취소일 때 설정됩니다(청구 문제와 혼동하지 마세요).
CustomerInfo에서 유예와 보류의 차이. App Store나 Google Play 유예 기간 동안에는 entitlement가 활성이며 billingIssueDetectedAt이 설정됩니다. Google이 계정 보류로 넘어가면 entitlement는 더 이상 활성이 아니라서, 평소의 entitlement 확인만으로도 접근이 이미 막힙니다.

앱 안에서 청구 재시도 감지하기

현재 CustomerInfo를 읽고 pro entitlement에서 청구 문제 신호를 확인하세요.

iOS (Swift)

swift
let info = try await Purchases.shared.customerInfo()

if let pro = info.entitlements["pro"], pro.isActive {
    if pro.billingIssueDetectedAt != nil {
        // Active subscriber whose payment is failing: show a "fix payment" banner.
        showBillingIssueBanner()
    }
}

Android (Kotlin)

kotlin
val info = Purchases.sharedInstance.awaitCustomerInfo()
val pro = info.entitlements["pro"]

if (pro?.isActive == true && pro.billingIssueDetectedAt != null) {
    // Active subscriber in billing retry.
    showBillingIssueBanner()
}

React Native (TypeScript)

tsx
const info = await Purchases.getCustomerInfo();
const pro = info.entitlements.active['pro'];

if (pro && pro.billingIssueDetectedAt != null) {
  // Active subscriber in billing retry.
  showBillingIssueBanner();
}
항상 최신으로 유지하세요. 앱이 포그라운드로 올라올 때 확인하고, 모바일에서는 CustomerInfo 업데이트도 구독해서 결제가 회복되는 순간 배너가 사라지게 하세요. (React Native 패턴은 CustomerInfo 리스너 가이드를 참고하세요.)

Customer Center로 사용자가 결제 고치게 하기

문제를 감지했다면, 사용자가 한 번의 탭으로 고칠 수 있게 해 주세요. 방법은 두 가지입니다.

방법 A: Customer Center (권장)

Customer Center는 RevenueCat이 제공하는, 바로 붙여 쓰는 셀프서비스 구독 관리 UI입니다. 사용자를 구독 관리와 결제 업데이트로 안내하고, 복원과 취소, 리텐션 제안까지 처리합니다.

swift
// iOS (SwiftUI) - RevenueCatUI
import RevenueCatUI

struct SettingsView: View {
    @State private var showCustomerCenter = false

    var body: some View {
        Button("Manage Subscription") { showCustomerCenter = true }
            .sheet(isPresented: $showCustomerCenter) {
                CustomerCenterView()
            }
    }
}
tsx
// React Native - react-native-purchases-ui
import RevenueCatUI from 'react-native-purchases-ui';

await RevenueCatUI.presentCustomerCenter();

Android에서는 purchases-uiCustomerCenter Composable을 렌더링하세요.

Customer Center는 Pro 또는 Enterprise 요금제가 필요하며, 대시보드의 Project Settings → Monetization Tools → Customer Center에서 설정합니다.

방법 B: 스토어의 관리 페이지를 바로 열기

Customer Center를 쓰지 않는다면, customerInfo.managementURL로 사용자를 스토어의 구독 관리 페이지로 곧장 보내세요. RevenueCat이 올바른 스토어에 맞게 이 값을 자동으로 채워 줍니다.

tsx
// React Native
import { Linking } from 'react-native';

const info = await Purchases.getCustomerInfo();
if (info.managementURL) {
  await Linking.openURL(info.managementURL);
}

iOS에서는 customerInfo.managementURLUIApplication.shared.open(_:)로 열고, Android에서는 ACTION_VIEW 인텐트로 엽니다. Manage Subscriptions 가이드를 참고하세요.

managementURL은 null일 수 있습니다. 활성 스토어 구독이 없을 때 그렇고, Amazon이나 Stripe는 지원하지 않습니다. 열기 전에 null을 확인하세요.

청구 문제 웹훅에 대응하기

앱 안 배너는 앱을 여는 사용자에게만 닿습니다. 나머지에게 닿으려면, 서버에서 RevenueCat 웹훅을 활용해 이메일이나 푸시 알림을 보내세요.

  • BILLING_ISSUE: 결제 실패가 감지되는 즉시 발생합니다. RevenueCat은 문제당 하나씩 보냅니다. 유예 기간이 설정되어 있으면, 회복 마감 시각인 grace_period_expiration_at_ms가 포함됩니다.
  • CANCELLATIONcancel_reason: BILLING_ERROR가 붙은 경우: 이 역시 실패가 감지될 때 발생합니다.
  • EXPIRATIONexpiration_reason: BILLING_ERROR가 붙은 경우: 유예 기간이 회복 없이 끝날 때만 발생합니다. 이때 비로소 접근 권한을 제거합니다.
  • RENEWAL: 결제가 회복될 때 발생하며, billingIssueDetectedAt이 지워집니다.
typescript
// Your webhook endpoint (Express-style)
app.post('/revenuecat/webhook', (req, res) => {
  const event = req.body.event;

  switch (event.type) {
    case 'BILLING_ISSUE':
      // Card failed but access continues. Email "update your payment method"
      // with the deadline from event.grace_period_expiration_at_ms.
      sendFixPaymentEmail(event.app_user_id, event.grace_period_expiration_at_ms);
      break;
    case 'RENEWAL':
      // Payment recovered. Stop the dunning sequence.
      clearDunning(event.app_user_id);
      break;
  }

  res.sendStatus(200);
});
타이밍은 유예 기간에 달려 있습니다. 유예 기간이 설정되어 있으면 EXPIRATION (BILLING_ERROR)이 유예가 끝날 때까지 미뤄져서, 독촉(dunning) 시퀀스가 작동할 시간을 벌어 줍니다. 유예 기간이 없으면 BILLING_ISSUE와 동시에 즉시 발생해서 접근 권한이 곧바로 사라집니다. 3단계와 4단계에서 설정한 유예 기간이 중요한 이유가 바로 이것입니다.

샌드박스에서 테스트하기

출시하기 전에 전체 루프를 검증하세요.

  1. App Store Connect에서 Sandbox(또는 Production과 Sandbox)에 유예 기간을 켜서, 샌드박스 테스터로 직접 돌려 볼 수 있게 하세요.
  2. Google Play에서는 closed testing 트랙의 라이선스 테스터로 구독을 유예와 보류 상태로 몰아넣으세요.
  3. 갱신 실패를 발생시킨 뒤, 앱에서 pro entitlement가 여전히 isActive이고 billingIssueDetectedAt이 설정되어 배너가 뜨는지 확인하세요.
  4. 서버가 grace_period_expiration_at_ms가 담긴 BILLING_ISSUE 웹훅을 받았는지 확인하세요.
  5. 결제를 회복시킨 뒤 배너가 사라지고 RENEWAL 웹훅이 도착하는지 확인하세요.
샌드박스 시간은 압축되어 있고 스토어가 통제합니다. 샌드박스의 갱신과 재시도 간격은 빨라져 있고, 정확한 타이밍은 스토어가 정합니다. 그러니 특정 일정을 가정하지 말고 실제로 동작을 검증하세요.

정리

비자발적 이탈을 되찾는 완전한 회복 흐름을 만들었습니다.

  • 두 스토어의 유예 기간이 카드를 재시도하는 동안 결제 중인 사용자의 접근 권한을 유지합니다.
  • RevenueCat이 그 상태를 활성 entitlement + billingIssueDetectedAt으로 드러내고, 여러분은 이를 앱 안에서 감지합니다.
  • Customer Center(또는 managementURL)가 사용자에게 한 번의 탭으로 결제를 업데이트할 길을 줍니다.
  • 웹훅이 앱 밖 독촉(dunning)을 이끌고, 유예 기간 덕분에 타이밍에 여유가 생깁니다.

이 작업은 만들 수 있는 변화 중 지렛대 효과가 가장 큰 축에 듭니다. 떠날 마음이 전혀 없던 고객에게서 매출을 되찾는 일이니까요.

더 나아가기