모바일 크래시 분석: Sentry vs Firebase Crashlytics 실전 비교

모바일 개발

크래시 분석SentryFirebase Crashlytics모바일 QAANR

이 글은 누구를 위한 것인가

  • 앱 크래시가 발생해도 어디서 났는지 모르는 팀
  • Sentry와 Crashlytics 중 어떤 것을 쓸지 결정 못 한 팀
  • 크래시율을 측정하고 개선하는 프로세스가 없는 개발팀

들어가며

"앱이 갑자기 꺼진다"는 1스타 리뷰는 이미 늦었다. 크래시 분석 도구 없이 앱을 운영하면, 재현 불가능한 버그를 사용자 리뷰로 알게 된다.

Sentry와 Firebase Crashlytics 모두 무료로 시작할 수 있지만, 제공하는 맥락의 깊이가 다르다. 상황에 따라 둘 다 쓰는 팀도 많다.

이 글은 bluefoxdev.kr의 모바일 오류 모니터링 가이드 를 참고하고, 실전 크래시 분석 설정 관점에서 확장하여 작성했습니다.


1. 도구 비교

┌─────────────────┬──────────────────┬──────────────────┐
│ 항목             │ Sentry           │ Crashlytics      │
├─────────────────┼──────────────────┼──────────────────┤
│ 비용             │ 무료 5K 이벤트/월 │ 무료 (무제한)    │
│ 크래시 추적      │ ✅               │ ✅               │
│ 비치명적 오류    │ ✅               │ ✅               │
│ ANR 분석        │ ✅ (Android)     │ ✅               │
│ 성능 모니터링    │ ✅ (Tracing)     │ 제한적           │
│ 사용자 피드백    │ ✅               │ ❌               │
│ 소스맵/dSYM     │ ✅ 자동 업로드   │ ✅               │
│ 알림 통합        │ Slack, PagerDuty │ Firebase Alert   │
│ 검색/필터       │ 강력            │ 기본적           │
│ 세션 리플레이    │ ✅ (모바일 beta) │ ❌               │
│ 자체 호스팅      │ ✅ (오픈소스)   │ ❌               │
└─────────────────┴──────────────────┴──────────────────┘

권장:
  Crashlytics: 크래시 전용, 비용 제약, Firebase 이미 사용 중
  Sentry: 크래시 + 성능 + 에러 통합 모니터링이 필요할 때
  둘 다: 크래시율은 Crashlytics, 상세 디버깅은 Sentry

2. Sentry iOS 설정

// Package.swift 또는 SPM
// https://github.com/getsentry/sentry-cocoa

import Sentry

@main
struct MyApp: App {
    init() {
        SentrySDK.start { options in
            options.dsn = "https://examplePublicKey@sentry.io/projectId"
            options.environment = ProcessInfo.processInfo.environment["APP_ENV"] ?? "production"
            options.release = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String
            
            // 성능 모니터링
            options.tracesSampleRate = 0.1  // 10% 샘플링
            
            // 프로파일링 (iOS 16+)
            options.profilesSampleRate = 0.1
            
            // 민감 정보 제거
            options.beforeSend = { event in
                // 개인정보 포함 필드 제거
                event.user?.email = nil
                event.user?.ipAddress = nil
                return event
            }
        }
    }
}

// 비치명적 오류 캡처
func handleAPIError(_ error: Error, endpoint: String) {
    SentrySDK.capture(error: error) { scope in
        scope.setTag(value: endpoint, key: "api.endpoint")
        scope.setLevel(.warning)
        scope.setContext(
            value: ["endpoint": endpoint, "timestamp": Date().timeIntervalSince1970],
            key: "api_context"
        )
    }
}

// 커스텀 이벤트
func trackPurchaseFailed(orderId: String, reason: String) {
    let event = Event(level: .error)
    event.message = SentryMessage(formatted: "Purchase failed")
    event.extra = ["order_id": orderId, "reason": reason]
    event.tags = ["feature": "purchase"]
    SentrySDK.capture(event: event)
}

// 트랜잭션 (성능 추적)
func loadProductDetail(productId: String) async throws -> Product {
    let transaction = SentrySDK.startTransaction(
        name: "product.detail.load",
        operation: "http"
    )
    defer { transaction.finish() }
    
    let apiSpan = transaction.startChild(operation: "http.client", description: "GET /products/\(productId)")
    defer { apiSpan.finish() }
    
    return try await api.getProduct(id: productId)
}

3. Firebase Crashlytics Android 설정

// build.gradle (app)
// implementation("com.google.firebase:firebase-crashlytics-ktx")
// implementation("com.google.firebase:firebase-analytics-ktx")

import com.google.firebase.crashlytics.FirebaseCrashlytics
import com.google.firebase.crashlytics.ktx.crashlytics
import com.google.firebase.ktx.Firebase

class MyApplication : Application() {
    override fun onCreate() {
        super.onCreate()
        
        // 개발 환경에서는 크래시 수집 비활성화
        Firebase.crashlytics.setCrashlyticsCollectionEnabled(
            BuildConfig.DEBUG.not()
        )
    }
}

// 사용자 식별 (크래시 추적용)
fun setUser(userId: String) {
    Firebase.crashlytics.setUserId(userId)
}

// 커스텀 키 설정 (크래시 컨텍스트)
fun logPurchaseAttempt(productId: String, price: Double) {
    Firebase.crashlytics.apply {
        setCustomKey("last_product_id", productId)
        setCustomKey("last_price", price)
        setCustomKey("screen", "checkout")
        log("Purchase attempt: $productId")
    }
}

// 비치명적 예외 기록
fun handleNetworkError(error: Exception, url: String) {
    Firebase.crashlytics.apply {
        setCustomKey("failed_url", url)
        recordException(error)
    }
}

// ANR 방지 (메인 스레드 작업 감지)
class ANRWatchdog(private val timeout: Long = 5000L) : Thread() {
    @Volatile
    private var tick = 0L
    
    private val mainHandler = Handler(Looper.getMainLooper())
    
    override fun run() {
        while (!isInterrupted) {
            val lastTick = tick
            mainHandler.post { tick = System.currentTimeMillis() }
            sleep(timeout)
            
            if (tick == lastTick) {
                // 메인 스레드가 응답 없음 → ANR 위험
                val exception = ANRException("ANR detected: main thread blocked for ${timeout}ms")
                Firebase.crashlytics.recordException(exception)
            }
        }
    }
}

4. 크래시 우선순위 분류

[크래시 심각도 분류 매트릭스]

P0 (즉시 대응 — 1시간 내):
  ✗ 앱 실행 시 즉시 크래시 (Crash-free session rate < 90%)
  ✗ 결제/가입 플로우 크래시
  ✗ 단일 버전에서 급증 (전날 대비 300% 이상)

P1 (당일 대응):
  ✗ 핵심 기능 크래시 (메인 탭, 검색)
  ✗ 특정 기기/OS 버전 집중 크래시
  ✗ Crash-free session rate < 99%

P2 (주간 대응):
  ✗ 보조 기능 비치명적 오류
  ✗ 엣지 케이스 크래시 (영향 사용자 < 0.1%)

[크래시율 목표]
  Crash-free session rate: 99.5% 이상 (업계 표준)
  Crash-free user rate: 99.9% 이상 (우수)
  ANR rate: 0.47% 이하 (Google Play 기준)

5. 릴리스 전 크래시 체크리스트

□ 최근 7일 크래시율 기준치 이하
□ 새 기기/OS 버전에서 테스트 완료
□ dSYM/mapping.txt 업로드 확인 (심볼리케이션)
□ 테스트 크래시 발생 → 대시보드 수신 확인
□ 이전 버전 미해결 P0/P1 크래시 없음
□ Firebase Test Lab 또는 Sentry 실기기 테스트
□ 알림 채널 설정 (Slack #crash-alert 연결)

마무리

크래시율은 앱 품질의 가장 기본적인 지표다. 두 도구 모두 무료로 시작할 수 있으므로, 일단 하나라도 먼저 붙여라. Crashlytics는 설정이 5분이면 된다.

크래시 대응의 핵심은 발생 속도보다 해결 속도다. 알림을 Slack에 연결하고, P0 크래시는 핫픽스 프로세스를 미리 정의해두면 사용자 영향을 최소화할 수 있다.