구독 이벤트 트래킹 구현 가이드

구독 이벤트 트래킹 구현 가이드

Singular의 SDK, server-to-server 연동 또는 서드파티 파트너를 사용하여 모든 플랫폼에서 구독 매출, 유저 리텐션, 캠페인 성과를 측정할 수 있도록 종합적인 구독 트래킹을 구현하세요.

Singular는 신규 구독, 갱신, 체험, 취소, 환불 등 구독 이벤트를 유저를 유입시킨 마케팅 캠페인에 정확하게 어트리뷰션하여 구독 라이프사이클과 매출 발생에 대한 완전한 가시성을 제공합니다.


구현 옵션

앱 아키텍처와 비즈니스 요구사항에 가장 적합한 구현 방법을 선택하세요.

고려 사항 SDK 연동 Server-to-Server (S2S) 서드파티 연동
구현 Singular SDK의 커스텀 매출 메서드를 통해 구독 이벤트 전송 필수 모바일 디바이스 속성과 함께 백엔드에서 Singular의 REST API로 이벤트 전송 RevenueCat, Adapty, Superwall, Xsolla 또는 기타 구독 플랫폼과의 연동 구성
앱 활동 의존성 SDK가 이벤트를 전송하려면 앱이 실행되어 있어야 함 앱 상태와 무관하게 실시간으로 이벤트 전송 파트너 플랫폼에서 실시간으로 이벤트 전송
이벤트 타이밍 앱 실행 시점에 따라 이벤트 시간이 실제 구독 시간과 다를 수 있음 백엔드에서 처리되는 대로 실시간 이벤트 트래킹 파트너 처리 후 거의 실시간 트래킹
복잡성 간단한 클라이언트 측 구현 보안이 확보된 백엔드 인프라 필요 파트너 계정 및 구성 필요

중요: 구독 이벤트에 S2S 또는 서드파티 연동을 사용하는 대부분의 고객은 완전한 어트리뷰션 트래킹을 위해 세션 및 구독 외 이벤트를 관리하도록 Singular SDK를 함께 연동해야 합니다.


Android: Google Play Billing 연동

Google Play Billing Library 8.0.0을 연동하여 구독 정보를 조회하고 Singular SDK 연동을 위해 디바이스에서 직접 구독 상태를 관리하세요.

사전 요구사항

Google Play Console 구성

앱에서 결제를 구현하기 전에 Google Play Console 에서 인앱 상품과 구독 항목을 설정하세요.

  1. 구독 상품 생성: Play Console에서 구독 등급, 결제 주기, 가격을 정의합니다
  2. 혜택 구성: 기본 요금제, 혜택 토큰, 프로모션 가격을 설정합니다
  3. 설정 테스트: 테스트 계정을 생성하고 상품 이용 가능 여부를 확인합니다

Billing Library 종속성 추가

build.gradle 업데이트

코루틴 지원을 위한 Kotlin 확장과 함께 앱의 종속성에 Google Play Billing Library 8.0.0을 추가하세요.

Kotlin DSL
dependencies {
    val billingVersion = "8.0.0"

    // Google Play Billing Library
    implementation("com.android.billingclient:billing:$billingVersion")

    // Kotlin extensions and coroutines support
    implementation("com.android.billingclient:billing-ktx:$billingVersion")
}

버전 8.0.0 기능: 이 버전은 enableAutoServiceReconnection() 를 통한 자동 서비스 재연결, 개선된 구매 조회 API, 향상된 보류 중 거래 처리 기능을 도입합니다.


BillingClient 초기화

Billing Manager 생성

모든 구독 작업을 처리하고 이벤트 트래킹을 위해 Singular SDK와 연동할 billing manager 클래스를 설정하세요.

Kotlin
class SubscriptionManager(private val context: Context) {

    private val purchasesUpdatedListener = PurchasesUpdatedListener { billingResult, purchases ->
        when (billingResult.responseCode) {
            BillingResponseCode.OK -> {
                purchases?.forEach { purchase ->
                    handlePurchaseUpdate(purchase)
                }
            }
            BillingResponseCode.USER_CANCELED -> {
                Log.d(TAG, "User canceled the purchase")
            }
            else -> {
                Log.e(TAG, "Purchase error: ${billingResult.debugMessage}")
            }
        }
    }

    private val billingClient = BillingClient.newBuilder(context)
        .setListener(purchasesUpdatedListener)
        .enablePendingPurchases() // Required for all purchases
        .enableAutoServiceReconnection() // NEW in 8.0.0 - handles reconnection automatically
        .build()

    fun initialize() {
        billingClient.startConnection(object : BillingClientStateListener {
            override fun onBillingSetupFinished(billingResult: BillingResult) {
                if (billingResult.responseCode == BillingResponseCode.OK) {
                    Log.d(TAG, "Billing client ready")
                    // Query existing subscriptions
                    queryExistingSubscriptions()
                } else {
                    Log.e(TAG, "Billing setup failed: ${billingResult.debugMessage}")
                }
            }

            override fun onBillingServiceDisconnected() {
                // With enableAutoServiceReconnection(), SDK handles reconnection
                // Manual retry is no longer required
                Log.d(TAG, "Billing service disconnected - auto-reconnection enabled")
            }
        })
    }

    companion object {
        private const val TAG = "SubscriptionManager"
    }
}

주요 기능:

  • 자동 재연결: 버전 8.0.0의 enableAutoServiceReconnection() 는 필요할 때 자동으로 연결을 다시 설정합니다
  • 구매 리스너: 갱신 및 보류 중 거래를 포함한 모든 구매 업데이트에 대한 콜백을 수신합니다
  • 오류 처리: 유저 취소 및 결제 오류에 대한 포괄적인 처리

구독 정보 조회

활성 구독 조회

갱신 및 앱 외부에서 구매된 구독을 처리하기 위해 queryPurchasesAsync() 를 사용하여 기존 구독을 조회하세요.

Kotlin
private suspend fun queryExistingSubscriptions() {
    val params = QueryPurchasesParams.newBuilder()
        .setProductType(BillingClient.ProductType.SUBS)
        .build()

    withContext(Dispatchers.IO) {
        val result = billingClient.queryPurchasesAsync(params)

        if (result.billingResult.responseCode == BillingResponseCode.OK) {
            result.purchasesList.forEach { purchase ->
                when (purchase.purchaseState) {
                    Purchase.PurchaseState.PURCHASED -> {
                        // Active subscription - process it
                        handleSubscriptionPurchase(purchase)
                    }
                    Purchase.PurchaseState.PENDING -> {
                        // Pending payment - notify user
                        Log.d(TAG, "Subscription pending: ${purchase.products}")
                    }
                }
            }
        } else {
            Log.e(TAG, "Query failed: ${result.billingResult.debugMessage}")
        }
    }
}

모범 사례: 앱이 백그라운드에 있거나 종료된 동안 발생한 구독 갱신을 포착하려면 onResume() 에서 queryPurchasesAsync() 를 호출하세요.


상품 세부 정보 조회

유저에게 표시하기 전에 가격, 결제 주기, 혜택 토큰을 포함한 구독 상품 세부 정보를 조회하세요.

Kotlin
private suspend fun querySubscriptionProducts() {
    val productList = listOf(
        QueryProductDetailsParams.Product.newBuilder()
            .setProductId("premium_monthly")
            .setProductType(BillingClient.ProductType.SUBS)
            .build(),
        QueryProductDetailsParams.Product.newBuilder()
            .setProductId("premium_yearly")
            .setProductType(BillingClient.ProductType.SUBS)
            .build()
    )

    val params = QueryProductDetailsParams.newBuilder()
        .setProductList(productList)
        .build()

    withContext(Dispatchers.IO) {
        val productDetailsResult = billingClient.queryProductDetails(params)

        if (productDetailsResult.billingResult.responseCode == BillingResponseCode.OK) {
            // Process successfully retrieved product details
            productDetailsResult.productDetailsList?.forEach { productDetails ->
                // Extract subscription offers
                productDetails.subscriptionOfferDetails?.forEach { offer ->
                    val price = offer.pricingPhases.pricingPhaseList.firstOrNull()
                    Log.d(TAG, "Product: ${productDetails.productId}, Price: ${price?.formattedPrice}")
                }
            }

            // Handle unfetched products
            productDetailsResult.unfetchedProductList?.forEach { unfetchedProduct ->
                Log.w(TAG, "Unfetched: ${unfetchedProduct.productId}")
            }
        }
    }
}

구매 업데이트 처리

구독 이벤트 처리

구매 상태 변경을 처리하고 구독 라이프사이클에 따라 Singular에 적절한 이벤트를 전송하세요.

Kotlin
private fun handlePurchaseUpdate(purchase: Purchase) {
    when (purchase.purchaseState) {
        Purchase.PurchaseState.PURCHASED -> {
            if (!purchase.isAcknowledged) {
                // Verify purchase on your backend first
                verifyPurchaseOnBackend(purchase) { isValid ->
                    if (isValid) {
                        // Send subscription event to Singular
                        trackSubscriptionToSingular(purchase)

                        // Grant entitlement to user
                        grantSubscriptionAccess(purchase)

                        // Acknowledge purchase to Google
                        acknowledgePurchase(purchase)
                    }
                }
            }
        }
        Purchase.PurchaseState.PENDING -> {
            // Notify user that payment is pending
            Log.d(TAG, "Purchase pending approval: ${purchase.products}")
            notifyUserPendingPayment(purchase)
        }
    }
}

private fun trackSubscriptionToSingular(purchase: Purchase) {
    // Get cached product details for pricing information
    val productDetails = getCachedProductDetails(purchase.products.first())

    productDetails?.let { details ->
        val subscriptionOffer = details.subscriptionOfferDetails?.firstOrNull()
        val pricingPhase = subscriptionOffer?.pricingPhases?.pricingPhaseList?.firstOrNull()

        val price = pricingPhase?.priceAmountMicros?.div(1_000_000.0) ?: 0.0
        val currency = pricingPhase?.priceCurrencyCode ?: "USD"

        // Determine event name based on purchase type
        val eventName = if (isNewSubscription(purchase)) {
            "sng_subscribe"
        } else {
            "subscription_renewed"
        }

        // Send to Singular WITHOUT receipt
        Singular.customRevenue(
            eventName,
            currency,
            price,
            mapOf(
                "subscription_id" to purchase.products.first(),
                "order_id" to (purchase.orderId ?: ""),
                "purchase_time" to purchase.purchaseTime.toString()
            )
        )

        Log.d(TAG, "Tracked $eventName to Singular: $price $currency")
    }
}

private fun acknowledgePurchase(purchase: Purchase) {
    val acknowledgePurchaseParams = AcknowledgePurchaseParams.newBuilder()
        .setPurchaseToken(purchase.purchaseToken)
        .build()

    lifecycleScope.launch {
        val ackResult = withContext(Dispatchers.IO) {
            billingClient.acknowledgePurchase(acknowledgePurchaseParams)
        }

        if (ackResult.responseCode == BillingResponseCode.OK) {
            Log.d(TAG, "Purchase acknowledged successfully")
        } else {
            Log.e(TAG, "Acknowledgement failed: ${ackResult.debugMessage}")
        }
    }
}

필수: 구독 이벤트를 트래킹할 때 Google Play 영수증을 Singular에 전송하지 마세요. customRevenue() 메서드를 영수증 검증 없이 사용하세요. 구매는 3일 이내에 승인(acknowledge)하지 않으면 자동으로 환불됩니다.


iOS: StoreKit 연동

Apple의 StoreKit 프레임워크를 연동하여 구독을 관리하고 iOS 앱에 대해 Singular로 이벤트를 전송하세요.

사전 요구사항

App Store Connect 구성

앱에서 StoreKit을 구현하기 전에 App Store Connect 에서 구독 상품과 구독 그룹을 설정하세요.

  1. 구독 그룹 생성: 액세스 등급에 따라 구독을 그룹으로 구성합니다
  2. 구독 상품 정의: 구독 등급, 기간, 가격을 설정합니다
  3. 혜택 구성: 소개 혜택, 프로모션 혜택, 구독 코드를 생성합니다
  4. 테스트 환경: 개발을 위한 샌드박스 테스트 계정을 설정합니다

StoreKit 구현

Transaction Observer 설정

구독 구매 알림과 갱신을 수신하려면 SKPaymentTransactionObserver 를 구현하세요.

Swift
import StoreKit

class SubscriptionManager: NSObject, SKPaymentTransactionObserver {

    static let shared = SubscriptionManager()

    private override init() {
        super.init()
        // Add transaction observer
        SKPaymentQueue.default().add(self)
    }

    deinit {
        SKPaymentQueue.default().remove(self)
    }

    // MARK: - SKPaymentTransactionObserver

    func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
        for transaction in transactions {
            switch transaction.transactionState {
            case .purchased:
                // New subscription or renewal
                handlePurchasedTransaction(transaction)

            case .restored:
                // Subscription restored from another device
                handleRestoredTransaction(transaction)

            case .failed:
                // Purchase failed
                handleFailedTransaction(transaction)

            case .deferred:
                // Purchase awaiting approval (family sharing)
                print("Transaction deferred: \(transaction.payment.productIdentifier)")

            case .purchasing:
                // Transaction in progress
                break

            @unknown default:
                break
            }
        }
    }

    private func handlePurchasedTransaction(_ transaction: SKPaymentTransaction) {
        // Verify receipt with your backend
        verifyReceipt { isValid in
            if isValid {
                // Send to Singular
                self.trackSubscriptionToSingular(transaction)

                // Grant entitlement
                self.grantSubscriptionAccess(transaction)

                // Finish transaction
                SKPaymentQueue.default().finishTransaction(transaction)
            }
        }
    }

    private func trackSubscriptionToSingular(_ transaction: SKPaymentTransaction) {
        // Get product details for pricing
        let productId = transaction.payment.productIdentifier

        // You should cache product details from SKProductsRequest
        if let product = getCachedProduct(productId) {
            let price = product.price.doubleValue
            let currency = product.priceLocale.currencyCode ?? "USD"

            // Determine event name
            let eventName = isNewSubscription(transaction) ? "sng_subscribe" : "subscription_renewed"

            // Send to Singular WITHOUT receipt
            Singular.customRevenue(
                eventName,
                currency: currency,
                amount: price,
                withAttributes: [
                    "subscription_id": productId,
                    "transaction_id": transaction.transactionIdentifier ?? "",
                    "transaction_date": transaction.transactionDate?.timeIntervalSince1970 ?? 0
                ]
            )
        }
    }
}

구독 상태 조회

영수증 검증을 사용하여 현재 구독 상태를 확인하고 갱신을 처리하세요.

Swift
func refreshSubscriptionStatus() {
    let request = SKReceiptRefreshRequest()
    request.delegate = self
    request.start()
}

extension SubscriptionManager: SKRequestDelegate {
    func requestDidFinish(_ request: SKRequest) {
        // Receipt refreshed successfully
        if let appStoreReceiptURL = Bundle.main.appStoreReceiptURL,
           FileManager.default.fileExists(atPath: appStoreReceiptURL.path) {

            // Send receipt to your backend for validation
            validateReceiptOnServer()
        }
    }

    func request(_ request: SKRequest, didFailWithError error: Error) {
        print("Receipt refresh failed: \(error.localizedDescription)")
    }
}

SDK 연동 방법

영수증 검증 없이 Singular SDK의 커스텀 매출 메서드를 사용하여 앱에서 구독 이벤트를 전송하세요.

플랫폼별 SDK 메서드

구독 이벤트를 트래킹하려면 플랫폼에 맞는 SDK 메서드를 사용하세요.

Android SDK

Kotlin
// Track subscription without receipt
Singular.customRevenue(
    "sng_subscribe",  // Event name
    "USD",            // Currency
    9.99,             // Amount
    mapOf(            // Additional attributes
        "subscription_id" to "premium_monthly",
        "billing_period" to "monthly"
    )
)

문서: Android SDK 매출 트래킹


iOS SDK

Swift
// Track subscription without receipt
Singular.customRevenue(
    "sng_subscribe",  // Event name
    currency: "USD",  // Currency
    amount: 9.99,     // Amount
    withAttributes: [ // Additional attributes
        "subscription_id": "premium_monthly",
        "billing_period": "monthly"
    ]
)

문서: iOS SDK 매출 트래킹


React Native SDK

JavaScript
// Track subscription without receipt
Singular.customRevenueWithArgs(
    "sng_subscribe",  // Event name
    "USD",            // Currency
    9.99,             // Amount
    {                 // Additional attributes
        subscription_id: "premium_monthly",
        billing_period: "monthly"
    }
);

문서: React Native SDK 매출 트래킹


Unity SDK

C#
// Track subscription without receipt
SingularSDK.CustomRevenue(
    "sng_subscribe",  // Event name
    "USD",            // Currency
    9.99,             // Amount
    new Dictionary<string, object> { // Additional attributes
        { "subscription_id", "premium_monthly" },
        { "billing_period", "monthly" }
    }
);

문서: Unity SDK 매출 트래킹


Flutter SDK

Dart
// Track subscription without receipt
Singular.customRevenueWithAttributes(
    "sng_subscribe",  // Event name
    "USD",            // Currency
    9.99,             // Amount
    {                 // Additional attributes
        "subscription_id": "premium_monthly",
        "billing_period": "monthly"
    }
);

문서: Flutter SDK 매출 트래킹


Cordova SDK

JavaScript
// Track subscription without receipt
cordova.plugins.SingularCordovaSdk.customRevenueWithArgs(
    "sng_subscribe",  // Event name
    "USD",            // Currency
    9.99,             // Amount
    {                 // Additional attributes
        subscription_id: "premium_monthly",
        billing_period: "monthly"
    }
);

문서: Cordova SDK 매출 트래킹

필수: 구독 트래킹을 위해 IAP(인앱 구매) 메서드를 사용하거나 영수증 값을 Singular에 전송하지 마세요. 영수증 없이 customRevenue() 메서드만 사용하세요.


Server-to-Server 연동

앱 상태와 무관하게 즉시 트래킹할 수 있도록 백엔드에서 Singular의 REST API로 구독 이벤트를 실시간으로 전송하세요.

구현 요구사항

Singular의 Event Endpoint를 사용하여 보안이 확보된 백엔드에서 매출 파라미터와 함께 구독 이벤트를 전송하세요.

Event Endpoint

구독 이벤트 데이터와 필수 모바일 디바이스 속성을 함께 담아 Singular의 Event API로 POST 요청을 전송하세요.

Endpoint:

POST https://api.singular.net/api/v1/evt

필수 파라미터:

  • SDK Key: Singular SDK 키 ( a 파라미터)
  • Event Name: 구독 이벤트 이름 ( n 파라미터)
  • Revenue: 구독 금액 ( amt 파라미터)
  • Currency: ISO 4217 통화 코드 ( cur 파라미터)
  • Device Identifiers: 어트리뷰션을 위한 IDFA (iOS) 또는 GAID (Android)
  • Platform: 운영체제 ( p 파라미터)

문서: Event Endpoint 레퍼런스 매출 파라미터


예시 요청

cURL
curl -X POST 'https://api.singular.net/api/v1/evt' \
  -H 'Content-Type: application/json' \
  -d '{
    "a": "YOUR_SDK_KEY",
    "n": "sng_subscribe",
    "r": 9.99,
    "pcc": "USD",
    "idfa": "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
    "p": "iOS",
    "install_time": 1697000000000,
    "extra": {
      "subscription_id": "premium_monthly",
      "order_id": "GPA.1234.5678.9012"
    }
  }'

SDK 불필요: 구독 이벤트에 S2S를 사용하는 경우 이러한 이벤트를 SDK에서 전송할 필요가 없습니다. 다만 대부분의 앱은 세션 트래킹과 구독 외 이벤트를 위해 SDK를 함께 연동해야 합니다.


서드파티 연동

구독 인프라를 관리하고 이벤트를 Singular로 직접 전송하는 서드파티 플랫폼을 통해 구독 트래킹을 구성하세요.

지원 파트너

Singular 지원 기능이 내장된 구독 관리 플랫폼과 연동하세요.

RevenueCat 연동

RevenueCat는 구독 인프라를 처리하고 구독 이벤트를 Singular의 REST API로 자동 전송합니다.

설정 단계:

  1. RevenueCat 계정 생성: RevenueCat 대시보드에서 앱을 설정합니다
  2. Singular 연동 구성: RevenueCat 연동 설정에서 Singular SDK 키를 추가합니다
  3. 이벤트 매핑: 어떤 RevenueCat 이벤트를 Singular로 전송할지 구성합니다
  4. 연동 테스트: 이벤트가 Singular 대시보드에 표시되는지 확인합니다

문서: RevenueCat Singular 연동


Adapty 연동

Adapty는 구독 분석을 제공하며 자체 연동을 통해 이벤트를 Singular로 전송합니다.

설정 단계:

  1. Adapty 계정 생성: Adapty에 앱을 등록하고 구성합니다
  2. Singular 연동 활성화: 연동 메뉴로 이동하여 Singular 자격 증명을 추가합니다
  3. 이벤트 매핑 구성: 전달할 구독 이벤트를 선택합니다
  4. 데이터 흐름 확인: 이벤트가 Singular에 정상적으로 도달하는지 확인합니다

문서: Adapty Singular 연동


Superwall 연동

Superwall 구독 라이프사이클 및 매출 이벤트를 수신하도록 Singular를 연결하며, 더 강력한 어트리뷰션을 위한 SDID 지원을 함께 사용할 수 있습니다.

설정 단계:

  1. Singular SDK 연동: 세션 및 이벤트 트래킹을 위해 앱에 Singular SDK를 추가합니다
  2. Singular SDID 설정(선택): Singular SDID를 설정하고 더 강력한 어트리뷰션을 위해 앱에서 Superwall 유저 속성을 SDID로 업데이트합니다
  3. Superwall에서 Singular 연동 추가: Superwall 대시보드에서 Singular 연동을 활성화하고 구성합니다

문서: Superwall Singular 연동


Xsolla 연동

Xsolla는 Web Shop에서의 구매 정보를 모바일 인앱 이벤트로 Singular에 전송하며, Singular는 이를 모바일 애플리케이션 설치, 유저 획득, 리인게이지먼트 캠페인에 어트리뷰션합니다.

설정 단계:

  1. 모바일 앱에 Singular 연동: 모바일 앱에 Singular SDK를 추가합니다
  2. 인앱 이벤트 구성: 예를 들어 로그인 등 CUID를 포함하는 인앱 이벤트를 Singular로 전송합니다
  3. Singular 파트너 구성 설정: Xsolla용 Singular 파트너 구성을 설정합니다
  4. Xsolla 계정에 Singular 추가: Xsolla 계정에서 Singular를 활성화합니다

문서: Xsolla Singular 연동

참고: 서드파티 연동은 파트너 플랫폼에서 관리됩니다. 구성은 각 파트너의 대시보드에서 완료해야 합니다. 세션 및 구독 외 이벤트 트래킹을 위해 Singular SDK를 함께 연동해야 합니다.


구독 이벤트 유형

다양한 구독 라이프사이클 이벤트를 트래킹하여 전체 구독 여정에 걸쳐 유저 인게이지먼트, 리텐션, 매출을 측정하세요.

표준 이벤트 이름

플랫폼 전반에서 일관된 리포팅을 위해 Singular의 표준 이벤트 이름을 사용하거나, 트래킹 요구사항에 따라 커스텀 이벤트 이름을 정의하세요.

구독 상태 이벤트 이름 SDK 메서드 S2S 연동
신규 구독 sng_subscribe 매출 금액과 함께 customRevenue() (영수증 없음) 매출 파라미터가 포함된 Event endpoint
체험 시작 sng_start_trial 매출 없이 event() 메서드 매출 없는 표준 Event endpoint
체험 종료 sng_end_trial 매출 없이 event() 메서드 매출 없는 표준 Event endpoint
구독 갱신 subscription_renewed 매출 금액과 함께 customRevenue() (영수증 없음) 매출 파라미터가 포함된 Event endpoint
취소 subscription_cancelled 매출 없이 event() 메서드 매출 없는 표준 Event endpoint
환불 subscription_refunded 음수 금액과 함께 customRevenue() (선택) 또는 매출 없는 event() 음수 매출이 포함된 Event endpoint (선택) 또는 표준 이벤트

중요: customRevenue()를 통해 구독 이벤트를 전송할 때는 isRestored false 인 이벤트만 전송하세요. 복원된 구매는 신규 매출이 아니라 기존 구독을 나타냅니다.


이벤트 구현 예시

신규 구독

유저가 처음으로 신규 구독을 구매하는 시점을 트래킹합니다.

Kotlin
// New subscription purchase
Singular.customRevenue(
    "sng_subscribe",
    "USD",
    9.99,
    mapOf(
        "subscription_tier" to "premium",
        "billing_period" to "monthly",
        "product_id" to "premium_monthly"
    )
)

체험 시작

유저가 매출 없이 무료 체험 기간을 시작하는 시점을 트래킹합니다.

Kotlin
// Trial start (no revenue)
Singular.event(
    "sng_start_trial",
    mapOf(
        "trial_duration" to "7_days",
        "subscription_tier" to "premium"
    )
)

구독 갱신

매출 금액과 함께 자동 구독 갱신을 트래킹합니다.

Kotlin
// Subscription renewal
Singular.customRevenue(
    "subscription_renewed",
    "USD",
    9.99,
    mapOf(
        "subscription_tier" to "premium",
        "renewal_count" to 3,
        "product_id" to "premium_monthly"
    )
)

취소

유저가 구독을 취소하는 시점을 트래킹합니다(매출 이벤트 없음).

Kotlin
// Subscription cancelled (no revenue)
Singular.event(
    "subscription_cancelled",
    mapOf(
        "cancellation_reason" to "too_expensive",
        "days_active" to 45,
        "subscription_tier" to "premium"
    )
)

SKAdNetwork 측정

iOS의 개인정보 보호를 준수하는 어트리뷰션을 위해 Apple의 SKAdNetwork를 통해 구독 이벤트를 측정하세요.

SKAN 구독 지원

Singular는 모든 연동 방법(SDK, S2S, 서드파티)을 통해 구독 이벤트 데이터를 측정할 수 있습니다. 이벤트 측정은 SKAN 버전에 따라 SKAN postback 타이밍 윈도우로 인해 제한될 수 있습니다.

하이브리드 SKAN 구현

라이프사이클 이벤트에는 Singular SDK를 사용하고 구독에는 S2S/서드파티 연동을 사용하는 앱의 경우, 두 데이터 소스를 결합하도록 하이브리드 SKAN을 활성화하세요.

지원팀 문의: 앱에 하이브리드 SKAN을 활성화하려면 고객 성공 매니저(CSM)에게 문의하세요. 이렇게 하면 백엔드 소스의 구독 이벤트가 SKAN conversion value에 올바르게 어트리뷰션됩니다.

SKAN Postback 윈도우:

  • SKAN 4.0: 세 개의 postback 윈도우(0-2일, 3-7일, 8-35일)를 통해 초기 구독 이벤트와 단기 갱신을 측정할 수 있습니다
  • SKAN 3.0: Singular 24시간 postback 윈도우로 인해 즉시 이루어진 구독만 측정할 수 있습니다
  • Conversion Value: 고가치 이벤트를 우선하도록 SKAN conversion value 스키마에서 구독 이벤트를 구성하세요

모범 사례 및 검증

구현 모범 사례를 따르고 프로덕션 배포 전에 구독 트래킹이 올바르게 작동하는지 검증하세요.

구현 모범 사례

주요 가이드라인

  • 영수증 검증 금지: 구독을 트래킹할 때 플랫폼 영수증(Google Play, App Store)을 Singular에 절대 전송하지 마세요. 영수증 없이 customRevenue()를 사용하세요
  • 복원된 구매 필터링: 중복 매출 리포팅을 방지하기 위해 isRestored가 false인 이벤트만 전송하세요
  • 백엔드 검증: Singular에 이벤트를 전송하기 전에 항상 보안이 확보된 백엔드에서 구매를 검증하세요
  • 적시 승인: 자동 환불을 방지하기 위해 Google Play 구매를 3일 이내에 승인(acknowledge)하세요
  • Resume 시 조회: 앱이 종료된 동안 발생한 갱신을 포착하려면 onResume()에서 queryPurchasesAsync()를 호출하세요
  • 일관된 이벤트 이름: 연동 리포팅을 위해 표준 Singular 이벤트 이름(sng_subscribe, subscription_renewed)을 사용하세요
  • 메타데이터 포함: 상세 분석을 위해 subscription_tier, billing_period, product_id 같은 속성을 추가하세요

테스트 및 검증

테스트 체크리스트

  1. 테스트 환경 설정: Google Play와 App Store 모두에 대해 샌드박스/테스트 계정을 사용합니다
  2. 신규 구독: sng_subscribe 이벤트가 올바른 금액 및 통화와 함께 정상적으로 전송되는지 확인합니다
  3. 체험 플로우: 매출 없이 체험 시작 및 체험 종료 이벤트를 테스트합니다
  4. 갱신 처리: 갱신 시 올바른 매출과 함께 subscription_renewed 이벤트가 트리거되는지 확인합니다
  5. 취소 트래킹: 유저가 구독을 취소할 때 취소 이벤트가 발생하는지 확인합니다
  6. 복원 필터링: 복원된 구매가 중복 이벤트를 전송하지 않도록 합니다
  7. Singular 대시보드: 이벤트가 올바른 어트리뷰션과 함께 Singular에 표시되는지 확인합니다
  8. 크로스 디바이스: 여러 디바이스 간 구독 동기화를 테스트합니다

일반적인 문제

  • 누락된 이벤트: billing client 연결이 활성 상태이고 앱 resume 시 queryPurchasesAsync()가 호출되는지 확인하세요
  • 중복 이벤트: 복원된 구매가 isRestored 확인으로 올바르게 필터링되는지 확인하세요
  • 잘못된 매출: ProductDetails에서 가격을 추출할 때 micros를 1,000,000으로 올바르게 나누는지 확인하세요
  • 어트리뷰션 문제: 디바이스 식별자(IDFA/GAID)가 S2S 요청에 올바르게 포함되는지 확인하세요
  • 승인 실패: 승인(acknowledge) 전에 백엔드 영수증 검증이 완료되는지 확인하세요

지원 리소스: 문제 해결 지원이 필요하면 SDK 버전, 플랫폼 세부 정보, 샘플 이벤트 페이로드, 그리고 문제를 보여주는 Singular 대시보드 스크린샷과 함께 Singular 지원팀에 문의하세요.