안드로이드 FCM, GA 구현

손현수·2025년 1월 20일

FCM(푸시알림), GA(유저 동작 데이터)는 모두 파이어베이스 콘솔을 통해 관리할 수 있다.

디버그 sha-1 발급

매번 어떻게 발급받는지 기억이 안 나서 구글에 검색하는 게 시간이 너무 아깝다.
안드로이드 스튜디오 터미널에서 다음 코드를 입력하면 디버그 sha-1를 확인할 수 있다.

keytool -list -v -keystore ~/.android/debug.keystore -alias androiddebugkey -storepass android -keypass android

출력된 값 중에 SHA1 값을 파이어베이스 콘솔에서 프로젝트 생성할 때 넣어주면 된다.

google-services.json

프로젝트를 생성하고 google-services.json 파일을 안드로이드 프로젝트의 app 모듈 아래에 넣어줘야 한다.

FCM 관련 코드

Manifest

<service
	android:name=".FcmService"
    android:enabled="true"
    android:exported="true">
    	<intent-filter>
        	<action android:name="com.google.firebase.MESSAGING_EVENT" />
        </intent-filter>
</service>

매니페스트 파일에 위처럼 설정해준다. 이 프로젝트에서는 FcmService라는 클래스를 구현해서 알림을 전송한다.

FcmService

실제 알림을 보내는 로직은 FcmService에서 수행된다.

class FcmService: FirebaseMessagingService() {

    override fun onNewToken(token: String) {
        super.onNewToken(token)

        Timber.d("[FCM] FcmService -> token: $token")
    }

    override fun onMessageReceived(message: RemoteMessage) {
        super.onMessageReceived(message)

        Timber.d("[FCM] FcmService -> data: ${message.data}")

        val title = message.data["title"] ?: message.notification?.title
        val body = message.data["todolist"] ?: message.notification?.body

        if (message.data.isNotEmpty()) {
            sendNotification(title, body)
        } else {
            Timber.d("[FCM] FcmService -> empty data")
        }

    }

    private fun sendNotification(title: String?, body: String?) {

        val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager

        val notification = NotificationCompat.Builder(this, CHANNEL_ID)
            .setContentTitle(body)
            .setSmallIcon(R.drawable.ic_app)
            .setAutoCancel(true)
            .setGroup(GROUP_KEY)
            .build()

        val summaryNotification = NotificationCompat.Builder(this, CHANNEL_ID)
            .setContentTitle("일단")
            .setSmallIcon(R.drawable.ic_app)
            .setStyle(
                NotificationCompat.InboxStyle()
                    .addLine(body)
                    .setSummaryText("오늘 마감 할 일: ${notificationManager.activeNotifications.size}")
            )
            .setGroup(GROUP_KEY)
            .setGroupSummary(true)
            .build()

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            val channel = NotificationChannel(
                CHANNEL_ID,
                CHANNEL_NAME,
                NotificationManager.IMPORTANCE_HIGH
            )
            notificationManager.createNotificationChannel(channel)
        }

        notificationManager.notify(System.currentTimeMillis().toInt(), notification)
        notificationManager.notify(SUMMARY_ID, summaryNotification)
    }

    companion object {
        private const val GROUP_KEY = "TODO_GROUP"
        private const val CHANNEL_ID = "TODO_CHANNEL"
        private const val CHANNEL_NAME = "TODO"
        private const val SUMMARY_ID = 0
    }
}

Fcm Token 발급

FCM이 동작하기 위해서는 파이어베이스로부터 fcm token을 가져와서 서버에 전달해야 한다. 현재 프로젝트에서는 카카오로그인 과정에서 fcm token을 로그인 api에 함께 보내고 있다.

FirebaseMessaging.getInstance().token.addOnCompleteListener(OnCompleteListener { task ->
    if (!task. isSuccessful) {
        Timber.d("[FCM] login -> 실패: ${task.exception}")
        return@OnCompleteListener
    }

    val token = task.result
    if (token != null) {
        viewModel.getClientId(token)
    }
})

token을 가져오는 것에 성공하면 상태 변수에 token을 저장해둔다. 함수 네이밍은 수정해야 할 것 같다. 저장한 token은 이후에 로그인 api에 함께 넣어서 보내면 된다.

대략적인 FCM의 동작 과정

  1. 카카오 로그인 화면에서 fcm token을 파이어베이스로부터 가져온다.
  2. 로그인 api에 fcm token을 넣어서 서버에 전달한다.
  3. 서버는 전달받은 토큰을 활용해서 특정 조건에 따라 FCM 서버를 통해 클라이언트로 메시지를 보낸다.
  4. 안드로이드에서는 FcmService 클래스의 onMessageReceived 메서드가 메시지를 전달받고 sendNotification 메서드로 유저에게 푸시 알림이 전달된다.

GA 설정

이 프로젝트는 멀티 모듈이 적용된 프로젝트이고 공통 플러그인을 구현해서 사용하고 있기 때문에 버전 카탈로그에 GA와 관련된 의존성을 추가해주고 공통 플러그인에 GA를 추가해주었다.

CommonPlugins

    with(plugins) {
        apply("kotlin-parcelize")
        apply("com.google.gms.google-services") // GA
    }
    
    dependencies {
        // 생략
        "implementation"(platform(libs.findLibrary("firebase-bom").get()))
        "implementation"(libs.findLibrary("firebase-analytics").get())
    }

일반적으로 gradle 스크립트에서 상단에는 plugins 블록이 존재하고 하단에는 dependencies 블록이 존재한다. 대부분의 라이브러리는 dependencies 블록에만 의존성을 추가하면 되지만 GA는 Google Play Services 플러그인을 사용해야 하므로 plugins 블록에도 추가해줘야 한다.
이런 경우에 공통 플러그인에서는 위 코드의 with 블록처럼 plugins에 추가해줘야 하는 것을 넣어서 반복적인 코드를 감소시킬 수 있다.

이제 CommonPlugins를 적용한 모듈에서는 GA를 사용하는 것이 가능하다.

GA 의존성 주입

실제 GA로 이벤트를 기록하기 위해서는 FirebaseAnalytics 객체를 활용해야 한다. 효과적인 이벤트 기록과 리소스 소모를 줄이기 위해 싱글톤으로 FirebaseAnalytics 객체를 관리하고 Hilt를 사용하여 의존성을 주입할 수 있다.

AnalyticsModule

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

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

이제 Hilt 모듈이 생성되었으므로 적절한 위치에 의존성을 주입해서 이벤트를 기록하는 것이 가능하다.

전역 객체를 통해 이벤트를 기록하기

Hilt를 통해 뷰모델에 의존성을 주입하고 사용하는 것도 좋지만 모든 뷰모델에 의존성 주입 코드를 넣는 것이 귀찮기도 하다. 이를 해결하기 위해 AnalyticsManager라는 전역 객체를 만들어서 이벤트를 기록하는 것은 어떨까?

object AnalyticsManager {
    private var firebaseAnalytics: FirebaseAnalytics? = null

    fun initialize(context: Context) {
        if (firebaseAnalytics == null) {
            // 인터넷 권한 관련 에러는 무시해도 됨
            firebaseAnalytics = FirebaseAnalytics.getInstance(context)
        }
    }

    fun logEvent(eventName: String, params: Map<String, String> = emptyMap()) {
        val bundle = Bundle().apply {
            params.forEach { (key, value) ->
                putString(key, value)
            }
        }
        firebaseAnalytics?.logEvent(eventName, bundle)
    }
}

초기화는 Application 클래스에서 초기화 함수를 호출해주면 된다.

@HiltAndroidApp
class PoptatoApplication: Application() {
    override fun onCreate() {
        super.onCreate()

        val config = ClarityConfig(BuildConfig.CLARITY_ID)
		
        // 생략
        AnalyticsManager.initialize(this)
    }
}

이렇게 하면 뷰모델에 FirebaseAnalytics 객체를 주입하지 않아도 이벤트를 기록하는 것이 가능하다. 실제 사용 예시는 다음과 같다.

    private fun getBacklogList(categoryId: Long, page: Int, size: Int) {
        AnalyticsManager.logEvent(
            eventName = "get_backlog_list",
            params = mapOf("button_name" to "할 일 내비게이션바 버튼", "user_action" to "백로그 전체 조회")
        )
        viewModelScope.launch {
            getBacklogListUseCase(request = GetBacklogListRequestModel(categoryId = categoryId, page = page, size = size)).collect {
                resultResponse(it, ::onSuccessGetBacklogList)
            }
        }
    }
profile
안녕하세요.

1개의 댓글

comment-user-thumbnail
2025년 2월 9일

respect bro🔥

답글 달기