본문 바로가기
[Developer]/Android

나의 Android 앱에 Firebase Analytics 적용하기

by 해피빈이 2026. 6. 10.
반응형

사진: Unsplash 의 Lukas Blazek

 

앱을 만들다 보면 자연스럽게 이런 질문이 생긴다.

 

사용자는 어떤 화면을 자주 볼까?
검색 기능은 실제로 쓰이고 있을까?
특정 버튼은 눌리고 있을까?
앱링크나 외부 링크는 얼마나 활용되고 있을까?


이런 질문에 답하려면 앱 안에서 발생하는 사용자 행동을 기록할 수 있어야 한다. Android 앱에서는 Firebase Analytics를 이용하면 비교적 쉽게 이벤트를 수집할 수 있다.

다만 Firebase Analytics 적용은 단순히 SDK를 추가하는 것으로 끝나지 않는다. 앱 곳곳에서 FirebaseAnalytics.logEvent()를 직접 호출하기 시작하면 화면, ViewModel, Navigation 코드가 Firebase SDK에 강하게 묶이게 된다.

이번 글에서는 Firebase Analytics를 적용하되, 앱 내부에서는 Firebase를 직접 알지 않도록 AnalyticsLogger라는 추상화 계층을 만들고, Hilt로 주입해서 사용하는 구조를 정리해본다.

사용한 환경은 다음과 같다.(구현을 위해서 실제로는 아래와 동일할 필요는 없다. 자신의 환경에 맞추어 유연하게 사용하면 된다.)

- Android
- Kotlin
- Jetpack Compose
- Hilt
- Firebase Analytics
- Gradle Kotlin DSL
- Version Catalog


---

1. Firebase Console에서 준비하기

먼저 [Firebase Console](https://console.firebase.google.com/)에서 프로젝트를 생성한다.


프로젝트를 만든 뒤 Android 앱을 추가한다. 이때 입력하는 Android package name은 실제 앱의 `applicationId`와 일치해야 한다.

 

android {
    defaultConfig {
        applicationId = "com.example.myapp"
    }
}



Android 앱 등록이 끝나면 `google-services.json` 파일을 다운로드할 수 있다.

이 파일은 Android 앱과 Firebase 프로젝트를 연결하는 설정 파일이다. 일반적으로 아래 위치에 둔다.

app/google-services.json

 

Firebase Android 앱 등록 화면

 

google-services.json 다운로드 화면


여기까지 하면 Firebase Console에서의 기본 준비는 끝난다.

 

 

2. Firebase 의존성 추가

이제 Android 프로젝트에 Firebase 의존성을 추가한다.

 

나는 Version Catalog를 사용하고 있기 때문에 libs.versions.toml에 Firebase 관련 버전과 라이브러리를 정의했다.

# gradle/libs.versions.toml

[versions]
googleServices = "4.4.2"
firebaseBom = "33.7.0"

[libraries]
firebase-bom = { module = "com.google.firebase:firebase-bom", version.ref = "firebaseBom" }
firebase-analytics = { module = "com.google.firebase:firebase-analytics" }

[plugins]
google-services = { id = "cohttp://m.google.gms.google-services", version.ref = "googleServices" }

 

프로젝트 레벨 `build.gradle.kts`에는 Google Services 플러그인을 추가한다.

// build.gradle.kts

plugins {
    alias(libs.plugins.google.services) apply false
}

 

앱 모듈의 `build.gradle.kts`에는 플러그인과 의존성을 추가한다.

 

// app/build.gradle.kts

plugins {
    id("com.android.application")
    id("org.jetbrains.kotlin.android")
    alias(libs.plugins.google.services)
}

dependencies {
    implementation(platform(libs.firebase.bom))
    implementation(libs.firebase.analytics)
}

 

Firebase는 BOM(Bill of Materials)을 제공한다. BOM을 사용하면 Firebase 라이브러리들의 호환 버전을 직접 하나씩 맞추지 않아도 된다.

그래서 firebase-analytics에는 별도 버전을 적지 않고, Firebase BOM이 버전을 관리하도록 둔다.

3. 쉬운 Analytics 적용을 위한 추상화 계층 생성

 

Firebase Analytics를 가장 단순하게 사용하는 방법은 필요한 곳에서 바로 FirebaseAnalytics를 호출하는 것이다.

 

firebaseAnalytics.logEvent("search", bundle)

 


하지만 앱이 커지면 이 방식은 금방 불편해진다.

화면, ViewModel, Navigation 코드 곳곳이 Firebase SDK에 직접 의존하게 되고, Preview나 테스트에서도 Firebase 초기화가 신경 쓰이기 시작한다.

그래서 앱 내부에서는 Firebase를 직접 호출하지 않고, AnalyticsLogger라는 인터페이스를 통해 이벤트를 기록하도록 만들었다.

먼저 이벤트 이름과 파라미터 이름을 한곳에 모아둔다.

object AnalyticsEvent {
    const val NAVIGATE = "navigate"
    const val SEARCH = "search"
    const val SELECT_ITEM = "select_item"
    const val SELECT_FILTER = "select_filter"
    const val REFRESH_LIST = "refresh_list"
    const val OPEN_EXTERNAL_LINK = "open_external_link"
}

object AnalyticsParam {
    const val DESTINATION = "destination"
    const val ITEM_TYPE = "item_type"
    const val ITEM_ID = "item_id"
    const val QUERY_LENGTH = "query_length"
    const val FILTER_NAME = "filter_name"
    const val URL_HOST = "url_host"
}



이렇게 상수로 관리하면 이벤트 이름에 오타가 생기는 것을 줄일 수 있고, 어떤 이벤트와 파라미터를 사용하고 있는지도 한눈에 볼 수 있다.

다음으로 로거 인터페이스를 만든다.

interface AnalyticsLogger {
    fun logScreen(
        screenName: String,
        params: Map<String, Any?> = emptyMap()
    )

    fun logEvent(
        eventName: String,
        params: Map<String, Any?> = emptyMap()
    )

    object Empty : AnalyticsLogger {
        override fun logScreen(
            screenName: String,
            params: Map<String, Any?>
        ) = Unit

        override fun logEvent(
            eventName: String,
            params: Map<String, Any?>
        ) = Unit
    }
}



여기서 Empty 구현체를 함께 둔 이유는 Compose Preview나 테스트에서 사용하기 위해서다.

Preview에서는 실제 Firebase 이벤트를 보낼 필요가 없다. 이럴 때 AnalyticsLogger.Empty를 넘겨주면 아무 동작도 하지 않기 때문에 편하게 사용할 수 있다.

이제 Firebase Analytics를 실제로 호출하는 구현체를 만든다.

 

class FirebaseAnalyticsLogger @Inject constructor(
    private val firebaseAnalytics: FirebaseAnalytics
) : AnalyticsLogger {

    override fun logScreen(
        screenName: String,
        params: Map<String, Any?>
    ) {
        val bundle = params.toBundle().apply {
            putString(FirebaseAnalytics.Param.SCREEN_NAME, screenName)
            putString(FirebaseAnalytics.Param.SCREEN_CLASS, screenName)
        }

        firebaseAnalytics.logEvent(
            FirebaseAnalytics.Event.SCREEN_VIEW,
            bundle
        )
    }

    override fun logEvent(
        eventName: String,
        params: Map<String, Any?>
    ) {
        firebaseAnalytics.logEvent(eventName, params.toBundle())
    }

    private fun Map<String, Any?>.toBundle(): Bundle {
        return Bundle().apply {
            forEach { (key, value) ->
                when (value) {
                    null -> Unit
                    is String -> putString(key, value)
                    is Int -> putLong(key, value.toLong())
                    is Long -> putLong(key, value)
                    is Float -> putDouble(key, value.toDouble())
                    is Double -> putDouble(key, value)
                    is Boolean -> putString(key, value.toString())
                    else -> putString(key, value.toString())
                }
            }
        }
    }
}



앱에서는 Map<String, Any?> 형태로 파라미터를 넘기고, Firebase 구현체 내부에서 Bundle로 변환한다.

덕분에 이벤트를 기록하는 쪽에서는 Firebase의 Bundle을 직접 다루지 않아도 된다.

 

4. Hilt로 DI 주입

이제 AnalyticsLogger를 앱 어디서든 주입받을 수 있도록 Hilt 모듈을 만든다.

@Module
@InstallIn(SingletonComponent::class)
abstract class AnalyticsModule {

    @Binds
    @Singleton
    abstract fun bindAnalyticsLogger(
        logger: FirebaseAnalyticsLogger
    ): AnalyticsLogger

    companion object {
        @Provides
        @Singleton
        fun provideFirebaseAnalytics(
            @ApplicationContext context: Context
        ): FirebaseAnalytics {
            return FirebaseAnalytics.getInstance(context)
        }
    }
}

 


이 구조의 핵심은 ViewModel이나 화면 코드가 FirebaseAnalyticsLogger를 직접 알지 않는다는 점이다.

앱 내부 코드는 AnalyticsLogger 인터페이스에만 의존한다. 실제 구현체가 Firebase인지, 테스트용 Fake인지, 아무것도 하지 않는 Empty 구현체인지는 외부에서 결정한다.

전체 흐름은 다음과 같다.




이렇게 해두면 Analytics 도구를 바꾸거나, 테스트용 구현체를 추가하거나, Preview에서 로깅을 비활성화하기가 쉬워진다.

 

5. Compose에서 적용 시 특이사항

Compose에서 Analytics를 적용할 때 가장 조심해야 하는 부분은 recomposition이다.

 

Composable 함수의 본문은 여러 번 다시 실행될 수 있다. 따라서 본문에서 바로 이벤트를 기록하면 같은 이벤트가 의도치 않게 여러 번 전송될 수 있다.

@Composable
fun BadExample(
    analyticsLogger: AnalyticsLogger
) {
    analyticsLogger.logEvent("some_event")
}


이런 코드는 피하는 것이 좋다.

화면 진입처럼 특정 상태 변화에 반응해야 하는 이벤트는 LaunchedEffect 안에서 처리한다.

@Composable
fun MainScreen(
    currentRoute: String,
    analyticsLogger: AnalyticsLogger
) {
    LaunchedEffect(currentRoute) {
        analyticsLogger.logScreen(
            screenName = currentRoute,
            params = mapOf(
                AnalyticsParam.DESTINATION to currentRoute
            )
        )
    }

    // Screen content
}

 


버튼 클릭, 검색 제출, 필터 선택처럼 사용자의 명확한 액션으로 발생하는 이벤트는 해당 콜백 안에서 기록한다.

@Composable
fun HomeButton(
    analyticsLogger: AnalyticsLogger,
    onNavigate: () -> Unit
) {
    Button(
        onClick = {
            analyticsLogger.logEvent(
                AnalyticsEvent.NAVIGATE,
                mapOf(
                    AnalyticsParam.DESTINATION to "list"
                )
            )

            onNavigate()
        }
    ) {
        Text("목록으로 이동")
    }
}



Preview에서는 AnalyticsLogger.Empty를 사용하면 된다.

@Preview
@Composable
fun HomeButtonPreview() {
    HomeButton(
        analyticsLogger = AnalyticsLogger.Empty,
        onNavigate = {}
    )
}

 


정리하면 Compose에서 Analytics를 적용할 때는 다음을 신경 쓰면 좋다.

- 화면 진입 로깅은 LaunchedEffect에서 처리한다.
- 클릭, 검색, 필터 선택은 사용자 액션 콜백에서 처리한다.
- Composable 본문에서 직접 이벤트를 기록하지 않는다.
- Preview에서는 AnalyticsLogger.Empty를 사용한다.
- UI 컴포저블이 Firebase 구현체를 직접 알지 않도록 한다.

 

6. 각 화면 및 이벤트에 적용

 

이제 실제 앱에서 자주 쓰는 이벤트를 기록해보자.

 

(1) 검색 이벤트

검색 이벤트는 ViewModel에서 처리하기 좋다.

@HiltViewModel
class ListViewModel @Inject constructor(
    private val analyticsLogger: AnalyticsLogger
) : ViewModel() {

    fun search(query: String) {
        analyticsLogger.logEvent(
            AnalyticsEvent.SEARCH,
            mapOf(
                AnalyticsParam.ITEM_TYPE to "article",
                AnalyticsParam.QUERY_LENGTH to query.length
            )
        )

        // 실제 검색 로직 실행
    }
}

 


여기서 실제 검색어를 기록하지 않고 query.length만 기록했다.

Analytics에서는 사용자 입력값을 그대로 남기는 일을 조심해야 한다. 검색어 원문은 민감한 정보가 될 수 있다. 꼭 필요한 경우가 아니라면 원문 대신 길이, 검색 여부, 카테고리 정도만 기록하는 편이 안전하다.

 

(2) 필터 선택 이벤트

fun selectFilter(filterName: String?) {
    analyticsLogger.logEvent(
        AnalyticsEvent.SELECT_FILTER,
        mapOf(
            AnalyticsParam.ITEM_TYPE to "article",
            AnalyticsParam.FILTER_NAME to (filterName ?: "all")
        )
    )

    // 필터 적용
}


필터가 해제된 경우에는 all처럼 명확한 값을 넣어두면 나중에 분석하기 편하다.

 

(3) 리스트 아이템 선택 이벤트

fun selectItem(itemId: String) {
    analyticsLogger.logEvent(
        AnalyticsEvent.SELECT_ITEM,
        mapOf(
            AnalyticsParam.ITEM_TYPE to "article",
            AnalyticsParam.ITEM_ID to itemId
        )
    )

    // 상세 화면 이동
}

 

이 이벤트를 기록하면 사용자가 어떤 종류의 아이템을 자주 선택하는지 확인할 수 있다.

(4) 새로고침 이벤트

fun refreshList() {
    analyticsLogger.logEvent(
        AnalyticsEvent.REFRESH_LIST,
        mapOf(
            AnalyticsParam.ITEM_TYPE to "article"
        )
    )

    // 목록 새로고침
}


Pull to refresh 같은 기능이 실제로 사용되는지도 이벤트로 확인할 수 있다.

(5) 외부 링크 열기 이벤트

fun openExternalLink(url: String) {
    val host = Uri.parse(url).host

    analyticsLogger.logEvent(
        AnalyticsEvent.OPEN_EXTERNAL_LINK,
        mapOf(
            AnalyticsParam.URL_HOST to host
        )
    )

    // Custom Tabs 또는 Browser 실행
}


외부 링크의 전체 URL을 기록하기보다 host 정도만 기록하면 충분한 경우가 많다.

예를 들어 `https://example.com/some/path?id=123` 전체를 남기지 않고, `example.com`만 기록하는 식이다.

7. 로깅 결과물 확인

이벤트를 추가한 뒤에는 Firebase Console에서 실제로 이벤트가 들어오는지 확인해야 한다.

개발 중에는 Firebase Console의 DebugView를 사용하는 것이 좋다.

확인할 항목은 다음과 같다.

- 화면 진입 시 screen_view가 기록되는가
- 버튼 클릭 시 의도한 custom event가 기록되는가
- 이벤트 파라미터가 누락되지 않았는가
- 이벤트 이름에 오타가 없는가
- 검색어 원문처럼 불필요한 값이 기록되고 있지 않은가

 

Firebase DebugView 이벤트 확인

 

Analytics 이벤트 상세 파라미터 확인

 

Firebase 개요의 일부

 


DebugView에서는 앱에서 발생한 이벤트를 거의 실시간으로 확인할 수 있다. 다만 일반 Analytics 대시보드에 반영되는 데에는 시간이 걸릴 수 있다.

그래서 개발 중에는 DebugView로 먼저 확인하고, 배포 후에는 Analytics 대시보드에서 전체 사용 흐름을 보는 방식으로 활용하면 좋다.

8. 마무리하며

Firebase Analytics 적용은 SDK를 추가하는 것만으로 끝나지 않는다.

정말 중요한 부분은 앱에서 어떤 행동을 이벤트로 볼 것인지 정하고, 그 이벤트를 일관된 방식으로 기록할 수 있는 구조를 만드는 것이다.

이번 글에서는 Firebase SDK를 직접 화면 코드에 흩뿌리지 않고, AnalyticsLogger라는 추상화 계층을 만든 뒤 Hilt로 주입하는 방식을 사용했다.

이렇게 해두면 Compose 화면, ViewModel, Navigation 계층 어디서든 쉽게 이벤트를 남길 수 있고, Preview나 테스트 환경에서도 부담 없이 대체 구현을 사용할 수 있다.

작은 앱이라도 처음부터 이런 구조를 잡아두면 이후 이벤트가 늘어날 때 훨씬 관리하기 쉬워진다.

Firebase Analytics를 처음 적용한다면 SDK 설정에서 멈추지 말고, 내 앱의 사용자 행동을 어떤 이벤트로 바라볼지 함께 설계해보는 것을 추천한다.

반응형

댓글