구독 이벤트 트래킹 구현 가이드
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 에서 인앱 상품과 구독 항목을 설정하세요.
- 구독 상품 생성: Play Console에서 구독 등급, 결제 주기, 가격을 정의합니다
- 혜택 구성: 기본 요금제, 혜택 토큰, 프로모션 가격을 설정합니다
- 설정 테스트: 테스트 계정을 생성하고 상품 이용 가능 여부를 확인합니다
Billing Library 종속성 추가
build.gradle 업데이트
코루틴 지원을 위한 Kotlin 확장과 함께 앱의 종속성에 Google Play Billing Library 8.0.0을 추가하세요.
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 클래스를 설정하세요.
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()
를 사용하여 기존 구독을 조회하세요.
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()
를 호출하세요.
상품 세부 정보 조회
유저에게 표시하기 전에 가격, 결제 주기, 혜택 토큰을 포함한 구독 상품 세부 정보를 조회하세요.
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에 적절한 이벤트를 전송하세요.
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 에서 구독 상품과 구독 그룹을 설정하세요.
- 구독 그룹 생성: 액세스 등급에 따라 구독을 그룹으로 구성합니다
- 구독 상품 정의: 구독 등급, 기간, 가격을 설정합니다
- 혜택 구성: 소개 혜택, 프로모션 혜택, 구독 코드를 생성합니다
- 테스트 환경: 개발을 위한 샌드박스 테스트 계정을 설정합니다
StoreKit 구현
Transaction Observer 설정
구독 구매 알림과 갱신을 수신하려면
SKPaymentTransactionObserver
를 구현하세요.
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
]
)
}
}
}
구독 상태 조회
영수증 검증을 사용하여 현재 구독 상태를 확인하고 갱신을 처리하세요.
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
// 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"
)
)
iOS SDK
// 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
// Track subscription without receipt
Singular.customRevenueWithArgs(
"sng_subscribe", // Event name
"USD", // Currency
9.99, // Amount
{ // Additional attributes
subscription_id: "premium_monthly",
billing_period: "monthly"
}
);
Unity SDK
// 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
// Track subscription without receipt
Singular.customRevenueWithAttributes(
"sng_subscribe", // Event name
"USD", // Currency
9.99, // Amount
{ // Additional attributes
"subscription_id": "premium_monthly",
"billing_period": "monthly"
}
);
Cordova SDK
// 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"
}
);
필수:
구독 트래킹을 위해 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 -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로 자동 전송합니다.
설정 단계:
- RevenueCat 계정 생성: RevenueCat 대시보드에서 앱을 설정합니다
- Singular 연동 구성: RevenueCat 연동 설정에서 Singular SDK 키를 추가합니다
- 이벤트 매핑: 어떤 RevenueCat 이벤트를 Singular로 전송할지 구성합니다
- 연동 테스트: 이벤트가 Singular 대시보드에 표시되는지 확인합니다
Adapty 연동
Adapty는 구독 분석을 제공하며 자체 연동을 통해 이벤트를 Singular로 전송합니다.
설정 단계:
- Adapty 계정 생성: Adapty에 앱을 등록하고 구성합니다
- Singular 연동 활성화: 연동 메뉴로 이동하여 Singular 자격 증명을 추가합니다
- 이벤트 매핑 구성: 전달할 구독 이벤트를 선택합니다
- 데이터 흐름 확인: 이벤트가 Singular에 정상적으로 도달하는지 확인합니다
Superwall 연동
Superwall 구독 라이프사이클 및 매출 이벤트를 수신하도록 Singular를 연결하며, 더 강력한 어트리뷰션을 위한 SDID 지원을 함께 사용할 수 있습니다.
설정 단계:
- Singular SDK 연동: 세션 및 이벤트 트래킹을 위해 앱에 Singular SDK를 추가합니다
- Singular SDID 설정(선택): Singular SDID를 설정하고 더 강력한 어트리뷰션을 위해 앱에서 Superwall 유저 속성을 SDID로 업데이트합니다
- Superwall에서 Singular 연동 추가: Superwall 대시보드에서 Singular 연동을 활성화하고 구성합니다
Xsolla 연동
Xsolla는 Web Shop에서의 구매 정보를 모바일 인앱 이벤트로 Singular에 전송하며, Singular는 이를 모바일 애플리케이션 설치, 유저 획득, 리인게이지먼트 캠페인에 어트리뷰션합니다.
설정 단계:
- 모바일 앱에 Singular 연동: 모바일 앱에 Singular SDK를 추가합니다
- 인앱 이벤트 구성: 예를 들어 로그인 등 CUID를 포함하는 인앱 이벤트를 Singular로 전송합니다
- Singular 파트너 구성 설정: 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
인 이벤트만 전송하세요. 복원된 구매는 신규 매출이 아니라 기존 구독을 나타냅니다.
이벤트 구현 예시
신규 구독
유저가 처음으로 신규 구독을 구매하는 시점을 트래킹합니다.
// New subscription purchase
Singular.customRevenue(
"sng_subscribe",
"USD",
9.99,
mapOf(
"subscription_tier" to "premium",
"billing_period" to "monthly",
"product_id" to "premium_monthly"
)
)
체험 시작
유저가 매출 없이 무료 체험 기간을 시작하는 시점을 트래킹합니다.
// Trial start (no revenue)
Singular.event(
"sng_start_trial",
mapOf(
"trial_duration" to "7_days",
"subscription_tier" to "premium"
)
)
구독 갱신
매출 금액과 함께 자동 구독 갱신을 트래킹합니다.
// Subscription renewal
Singular.customRevenue(
"subscription_renewed",
"USD",
9.99,
mapOf(
"subscription_tier" to "premium",
"renewal_count" to 3,
"product_id" to "premium_monthly"
)
)
취소
유저가 구독을 취소하는 시점을 트래킹합니다(매출 이벤트 없음).
// 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 같은 속성을 추가하세요
테스트 및 검증
테스트 체크리스트
- 테스트 환경 설정: Google Play와 App Store 모두에 대해 샌드박스/테스트 계정을 사용합니다
- 신규 구독: sng_subscribe 이벤트가 올바른 금액 및 통화와 함께 정상적으로 전송되는지 확인합니다
- 체험 플로우: 매출 없이 체험 시작 및 체험 종료 이벤트를 테스트합니다
- 갱신 처리: 갱신 시 올바른 매출과 함께 subscription_renewed 이벤트가 트리거되는지 확인합니다
- 취소 트래킹: 유저가 구독을 취소할 때 취소 이벤트가 발생하는지 확인합니다
- 복원 필터링: 복원된 구매가 중복 이벤트를 전송하지 않도록 합니다
- Singular 대시보드: 이벤트가 올바른 어트리뷰션과 함께 Singular에 표시되는지 확인합니다
- 크로스 디바이스: 여러 디바이스 간 구독 동기화를 테스트합니다
일반적인 문제
- 누락된 이벤트: billing client 연결이 활성 상태이고 앱 resume 시 queryPurchasesAsync()가 호출되는지 확인하세요
- 중복 이벤트: 복원된 구매가 isRestored 확인으로 올바르게 필터링되는지 확인하세요
- 잘못된 매출: ProductDetails에서 가격을 추출할 때 micros를 1,000,000으로 올바르게 나누는지 확인하세요
- 어트리뷰션 문제: 디바이스 식별자(IDFA/GAID)가 S2S 요청에 올바르게 포함되는지 확인하세요
- 승인 실패: 승인(acknowledge) 전에 백엔드 영수증 검증이 완료되는지 확인하세요
지원 리소스: 문제 해결 지원이 필요하면 SDK 버전, 플랫폼 세부 정보, 샘플 이벤트 페이로드, 그리고 문제를 보여주는 Singular 대시보드 스크린샷과 함께 Singular 지원팀에 문의하세요.