모바일 분석: Firebase Analytics vs Amplitude 선택과 이벤트 설계

모바일 개발

Firebase AnalyticsAmplitude모바일 분석이벤트 택소노미퍼널 분석

이 글은 누구를 위한 것인가

  • 어떤 분석 도구를 써야 할지 모르는 팀
  • 이벤트를 어떻게 설계해야 나중에 분석이 잘 되는지 모르는 개발자
  • iOS 14 이후 ATT로 추적이 제한된 상황을 대응해야 하는 팀

들어가며

이벤트를 나중에 생각하면 너무 늦다. "구매 완료" 이벤트를 추가했는데 카테고리 정보가 없어서 카테고리별 분석을 못 한다. 이벤트 택소노미 설계가 먼저다.

이 글은 bluebutton.kr의 모바일 분석 설계 가이드 를 참고하여 작성했습니다.


1. 분석 도구 선택과 이벤트 설계

[Firebase Analytics vs Amplitude]

Firebase Analytics:
  비용: 무료 (BigQuery 수출 비용 별도)
  강점: Google 생태계, Crashlytics, FCM 연동
  약점: 실시간 분석 제한, 커스텀 퍼널 불편
  적합: 소규모 앱, Google 광고 운영

Amplitude:
  비용: 월 100만 이벤트 무료, 이후 유료
  강점: 퍼널/코호트/리텐션 분석, 실시간
  약점: 비용, Google 연동 별도 작업
  적합: 제품 분석 집중, B2C 앱

[이벤트 택소노미 설계 원칙]

네이밍: "명사_동사" 형식
  ❌ click_buy, buy_btn_pressed
  ✅ product_viewed, checkout_completed

공통 속성 (모든 이벤트):
  user_id, session_id
  platform (ios/android)
  app_version, os_version
  timestamp

이벤트별 속성:
  product_viewed: product_id, category, price
  checkout_completed: order_id, total, payment_method
  content_shared: content_id, share_method

[iOS ATT 대응]
  iOS 14.5+: 추적 권한 팝업 의무화
  거부 시: IDFA 사용 불가
  대안: first-party ID, SKAdNetwork
  허용 요청 전: 가치 설명 필수

2. Analytics 구현 (이중 전송 패턴)

// iOS Swift

import FirebaseAnalytics
import Amplitude

// 이벤트 택소노미 정의 (타입 안전)
enum AnalyticsEvent {
    case productViewed(productId: String, category: String, price: Double)
    case checkoutStarted(cartItems: Int, totalValue: Double)
    case checkoutCompleted(orderId: String, total: Double, paymentMethod: String)
    case contentShared(contentId: String, shareMethod: String)
    
    var name: String {
        switch self {
        case .productViewed: return "product_viewed"
        case .checkoutStarted: return "checkout_started"
        case .checkoutCompleted: return "checkout_completed"
        case .contentShared: return "content_shared"
        }
    }
    
    var properties: [String: Any] {
        switch self {
        case let .productViewed(id, category, price):
            return ["product_id": id, "category": category, "price": price]
        case let .checkoutStarted(items, total):
            return ["cart_items": items, "total_value": total]
        case let .checkoutCompleted(orderId, total, payment):
            return ["order_id": orderId, "total": total, "payment_method": payment]
        case let .contentShared(contentId, method):
            return ["content_id": contentId, "share_method": method]
        }
    }
}

class AnalyticsManager {
    static let shared = AnalyticsManager()
    
    private var commonProperties: [String: Any] = [:]
    
    func configure(userId: String) {
        // Firebase
        Analytics.setUserID(userId)
        
        // Amplitude
        Amplitude.instance().setUserId(userId)
        
        // 공통 속성
        commonProperties = [
            "platform": "ios",
            "app_version": Bundle.main.appVersion,
            "os_version": UIDevice.current.systemVersion,
        ]
        
        Analytics.setDefaultEventParameters(commonProperties)
    }
    
    func track(_ event: AnalyticsEvent) {
        var properties = commonProperties.merging(event.properties) { _, new in new }
        
        // Firebase
        Analytics.logEvent(event.name, parameters: properties as [String: Any])
        
        // Amplitude
        Amplitude.instance().logEvent(event.name, withEventProperties: properties)
        
        // 디버그 로그
        #if DEBUG
        print("[Analytics] \(event.name): \(properties)")
        #endif
    }
    
    func setUserProperty(key: String, value: String) {
        Analytics.setUserProperty(value, forName: key)
        
        let identity = AMPIdentify()
        identity.set(key, value: value as NSObject)
        Amplitude.instance().identify(identity)
    }
    
    func startSession() {
        commonProperties["session_id"] = UUID().uuidString
    }
    
    // 퍼널 추적 헬퍼
    func trackCheckoutFunnel(step: CheckoutStep) {
        switch step {
        case .cartViewed(let items):
            track(.checkoutStarted(cartItems: items, totalValue: 0))
        case .paymentEntered:
            Analytics.logEvent("payment_info_entered", parameters: nil)
        case .orderPlaced(let orderId, let total, let method):
            track(.checkoutCompleted(orderId: orderId, total: total, paymentMethod: method))
        }
    }
}

enum CheckoutStep {
    case cartViewed(items: Int)
    case paymentEntered
    case orderPlaced(orderId: String, total: Double, method: String)
}

extension Bundle {
    var appVersion: String {
        infoDictionary?["CFBundleShortVersionString"] as? String ?? "unknown"
    }
}

// ATT 권한 요청
func requestTrackingPermission() async -> Bool {
    if #available(iOS 14, *) {
        let status = await ATTrackingManager.requestTrackingAuthorization()
        return status == .authorized
    }
    return true
}
// Android Kotlin
import com.google.firebase.analytics.FirebaseAnalytics
import com.amplitude.api.Amplitude
import org.json.JSONObject

class AnalyticsManager(private val context: android.content.Context) {
    
    private val firebaseAnalytics = FirebaseAnalytics.getInstance(context)
    private val amplitude = Amplitude.getInstance()
    
    fun configure(userId: String) {
        firebaseAnalytics.setUserId(userId)
        amplitude.setUserId(userId)
    }
    
    fun track(eventName: String, properties: Map<String, Any> = emptyMap()) {
        // Firebase
        val bundle = android.os.Bundle().apply {
            val enriched = properties + mapOf(
                "platform" to "android",
                "app_version" to getAppVersion(),
            )
            enriched.forEach { (k, v) ->
                when (v) {
                    is String -> putString(k, v)
                    is Int -> putInt(k, v)
                    is Long -> putLong(k, v)
                    is Double -> putDouble(k, v)
                    is Boolean -> putBoolean(k, v)
                }
            }
        }
        firebaseAnalytics.logEvent(eventName, bundle)
        
        // Amplitude
        val jsonProps = JSONObject(properties)
        amplitude.logEvent(eventName, jsonProps)
    }
    
    private fun getAppVersion(): String {
        return try {
            context.packageManager.getPackageInfo(context.packageName, 0).versionName
        } catch (e: Exception) {
            "unknown"
        }
    }
}

마무리

이벤트 택소노미는 초기에 설계하고 문서화해야 한다. "이름_동사" 네이밍, 공통 속성 자동 첨부, 타입 안전한 이벤트 열거형으로 구현 오류를 방지하라. Firebase는 무료라 부담이 없지만, 제품 분석이 중요하면 Amplitude의 퍼널·코호트 분석이 훨씬 강력하다.