개요

0:01:00

구매 복원은 구독이나 인앱 구매를 판매하는 iOS 앱이라면 반드시 갖춰야 할 기본 기능입니다. Apple의 App Store Review Guidelines는 인앱 구매를 제공하는 모든 앱에 사용자가 구매를 복원할 수 있는 수단을 포함하도록 요구합니다. 이 수단이 없으면 심사 과정에서 앱이 거절될 수 있습니다.

사용자가 구매를 복원해야 하는 대표적인 상황은 다음과 같습니다.

  • 앱을 삭제한 뒤 다시 설치할 때
  • 새 iPhone이나 iPad를 설정할 때
  • 같은 Apple ID로 로그인한 두 번째 기기에 앱을 내려받을 때
  • 로컬 구매 상태를 잃어버린 앱 업데이트 이후
App Store 요구 사항: Apple의 가이드라인(3.1.1 항)은 인앱 구매가 있는 모든 앱에 사용자가 쉽게 찾을 수 있는 "구매 복원" 수단을 포함하도록 규정합니다. 이 수단이 없는 앱은 거절될 수 있습니다.

사전 준비

  • RevenueCat iOS SDK가 설치되어 있습니다(Swift Package Manager 또는 CocoaPods 사용)
  • App 진입점에서 API 키로 SDK를 구성해 두었습니다
  • RevenueCat 대시보드에 Entitlement가 하나 이상 구성되어 있습니다

restorePurchases() API

0:01:30

RevenueCat의 restorePurchases() 메서드는 다음 작업을 순서대로 수행합니다.

  1. 기기에서 현재 App Store 영수증을 가져옵니다
  2. Apple 검증을 위해 영수증을 RevenueCat 서버로 보냅니다
  3. 활성 구독이나 구매를 현재 고객 레코드에 동기화합니다
  4. 모든 활성 Entitlement를 담아 갱신된 CustomerInfo 객체를 반환합니다

Swift의 최신 async/await 문법으로 기본 사용법을 보면 다음과 같습니다.

swift
// Basic restore using async/await
do {
    let customerInfo = try await Purchases.shared.restorePurchases()
    if customerInfo.entitlements["premium"]?.isActive == true {
        // User has premium access
        print("Purchases restored successfully")
    } else {
        print("No active purchases found to restore")
    }
} catch {
    print("Error restoring purchases: \(error.localizedDescription)")
}

반환되는 CustomerInfo 객체는 복원 이후 사용자의 현재 구독 상태를 반영합니다. Entitlement는 상품 ID가 아니라 항상 식별자(예: "premium")로 확인하세요. 이렇게 하면 기능 잠금 로직을 특정 상품에서 분리해 둘 수 있습니다.


SwiftUI 구현

0:02:30

아래는 그 자체로 완결된, 프로덕션에 바로 쓸 수 있는 RestorePurchasesButton SwiftUI 뷰입니다. 로딩 상태를 처리하고, 결과를 알림으로 보여 주며, 중복 탭을 막기 위해 비동기 작업이 진행되는 동안 버튼을 비활성화합니다.

swift
import SwiftUI
import RevenueCat

struct RestorePurchasesButton: View {
    @State private var isRestoring = false
    @State private var showAlert = false
    @State private var alertMessage = ""

    var body: some View {
        Button(action: restorePurchases) {
            if isRestoring {
                ProgressView()
                    .progressViewStyle(.circular)
            } else {
                Text("Restore Purchases")
            }
        }
        .disabled(isRestoring)
        .alert("Restore Purchases", isPresented: $showAlert) {
            Button("OK") { }
        } message: {
            Text(alertMessage)
        }
    }

    private func restorePurchases() {
        isRestoring = true
        Task {
            do {
                let customerInfo = try await Purchases.shared.restorePurchases()
                if customerInfo.entitlements.active.isEmpty {
                    alertMessage = "No active purchases found. If you believe this is an error, please contact support."
                } else {
                    alertMessage = "Your purchases have been successfully restored!"
                }
            } catch {
                alertMessage = "Failed to restore purchases: \(error.localizedDescription)"
            }
            isRestoring = false
            showAlert = true
        }
    }
}

이 버튼을 페이월이나 설정 화면에 넣으세요.

swift
struct PaywallView: View {
    var body: some View {
        VStack(spacing: 16) {
            // ... your pricing options ...

            RestorePurchasesButton()
                .font(.footnote)
                .foregroundColor(.secondary)
        }
    }
}

UIKit 구현

0:02:00

아직 SwiftUI로 전환하지 않은 앱이라면, 클로저 기반 콜백 API를 사용하는 UIKit 구현은 다음과 같습니다.

swift
@IBAction func restorePurchasesTapped(_ sender: UIButton) {
    sender.isEnabled = false

    Purchases.shared.restorePurchases { customerInfo, error in
        sender.isEnabled = true

        if let error = error {
            self.showAlert(title: "Error", message: error.localizedDescription)
            return
        }

        if customerInfo?.entitlements["premium"]?.isActive == true {
            self.showAlert(title: "Success", message: "Purchases restored successfully!")
            self.updateUIForPremium()
        } else {
            self.showAlert(
                title: "Not Found",
                message: "No active purchases found. Contact support if you believe this is an error."
            )
        }
    }
}

private func showAlert(title: String, message: String) {
    let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
    alert.addAction(UIAlertAction(title: "OK", style: .default))
    present(alert, animated: true)
}

내장 복원 버튼

0:01:30

RevenueCat의 Paywalls(RevenueCatUIPaywallView 또는 PaywallViewController)를 사용한다면 "구매 복원" 버튼이 자동으로 포함됩니다. 직접 추가할 필요가 없습니다.

이 내장 버튼은 모든 RevenueCat 페이월 템플릿 하단에 표시되며, 위에서 만든 수동 구현과 똑같이 동작합니다. 페이월 UI를 완전히 직접 만든다면 위에서 소개한 RestorePurchasesButton 컴포넌트를 사용하세요.

swift
import RevenueCatUI

// RevenueCat's PaywallView includes a restore button automatically
struct AppPaywall: View {
    var body: some View {
        // The restore purchases button is built in — no extra code needed
        PaywallView()
    }
}

중요한 참고 사항

0:01:30
App Store 요구 사항: Apple은 복원 버튼을 사용자가 쉽게 접근할 수 있게 두도록 요구합니다. 페이월 화면이나 앱의 설정/계정 섹션에 배치하세요. 여러 번 탭해야 나오는 곳에 숨기지 마세요.
익명 사용자: 앱이 RevenueCat의 익명 사용자 ID를 사용하는 상태에서 활성 구매가 없는 사용자가 restorePurchases()를 호출하면, RevenueCat이 새 익명 ID를 만들 수 있습니다. 앱에 자체 인증 시스템이 있다면, 복원을 호출하기 전에 Purchases.shared.logIn(appUserID:)로 사용자를 로그인시켜 구매가 올바른 계정에 연결되도록 하세요.
실행 시 자동 복원 금지: restorePurchases()는 사용자가 복원 버튼을 명시적으로 탭했을 때만 호출하세요. 앱을 실행할 때마다 자동으로 호출하는 것은 불필요하고, App Store 네트워크 요청을 유발하며, 앱 시작 속도를 느리게 할 수 있습니다.

관련 가이드