본문 바로가기
Develop/Android

[Android] FCM 푸시알림 커스텀 구현기 (딥링크 활용)

by bona.com 2025. 4. 28.

필자가 속해 있는 프로젝트 <terning>에서 FCM 푸시알림 기능을 맡았다. 이에 대해 알아보면서 안드로이드 알림에는 어떤 종류가 있는지, 어떻게 커스텀하면 되는지, 딥링크로 어떻게 화면 이동하는 지 등을 공부했고 이를 기록하고자 한다.

 

FCM 푸시알림

그럼 FCM 푸시알림이란?
Firebase Cloud Messaging의 약자로, Google 클라우드 메세징 서비스이다. 이를 통해 FCM 토큰으로 사용자를 식별하여 쉽게 푸시알림을 구현할 수 있다. 

서버랑 통신할 때는 위와 같은 흐름을 따른다. 
정리하면 다음과 같다.

1. Firebase에서 토큰을  발급하여 Client에게 보낸다.
2. Client는 발급받은 토큰을 Server에게 보낸다.
3. Server는 이 토큰을 이용해 Firebase에게 메시지 전송을 요청한다.
4. FirebaseClient에게 메시지를 전송한다.
5. Client는 리스너를 통해 메시지를 수신한다.

 
처음 보면 복잡해 보일 수 있지만 우리는 클라이언트 입장에서만 보면 되기 때문에 생각보다 간단해진다!
즉, 안드로이드 개발을 할 때는 토큰을 발급받아 서버에게 보내고, 파이어베이스에서 보내주는 메시지를 보여주면 된다!
 
안드로이드에서 파이어베이스를 연결하는 방법은 생략하도록 하겠다. (인터넷에서 쉽게 서치 가능하다) 이번 블로그는 파이어베이스가 연결되어있다는 것을 가정하에 설명할 것이다.
 

알림 개요

필자는 알림을 구현하기 위해 우선 안드로이드의 알림 개요가 어떻게 되는지 살펴보았다
아래는 안드로이드 알림 디자인 가이드이다.
https://developer.android.com/design/ui/mobile/guides/home-screen/notifications?hl=ko

 

알림  |  Mobile  |  Android Developers

이 페이지는 Cloud Translation API를 통해 번역되었습니다. 알림 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. 알림은 Google 애널리틱스 4와 관련된 앱을 사용하

developer.android.com

안드로이드의 알림은 기본적으로 위처럼 생겼다. 이번 <terning>에서도 해당 디자인을 사용하였다. 
1번, 2번, 5번에 집중해보자.
1번은 앱 아이콘이고,  2번은 헤더 텍스트로 알림의 제목이다. 5번은 콘텐츠 텍스트로 추가정보를 담고 있다. 집중해야 하는 이유는 알림을 직접 구현할 때 1번, 2번, 5번이 없으면 에러가 나기 때문이다.  
 
필자는 앱 아이콘을 추가 안 해서 한동안 알림이 오지 않았던 건 비밀이다.
 
여기서 추가로 6번 큰 아이콘을 설정해주고, 4번 펼쳐진 상태를 구현해줄 수 있는데 자세한 구현 방법은 아래에서 이어서 설명하겠다. 지금은 개요만 알고 넘어가자.

이 외에도 알림에 프로그레스바, 답장 기능 등을 적용할 수 있다. 직접 커스텀해서 여러 종류의 알림을 사용해보면 좋을 것 같다.
 

알림 권한 요청

이제 알림 권한 요청에 대해 알아보자. 사용자가 알림을 수신하기 위해서는 알림 권한 요청을 통해 "허용"을 해줘야 한다.
이번에도 관련 공식문서에서 자세한 내용 확인 가능하다.
https://developer.android.com/develop/ui/views/notifications/notification-permission?hl=ko

 

알림 런타임 권한  |  Views  |  Android Developers

이 페이지는 Cloud Translation API를 통해 번역되었습니다. 알림 런타임 권한 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. Android 13(API 수준 33) 이상에서는 앱

developer.android.com

안드로이드는 안드로이드 제공 다이얼로그로 위처럼 런타임 권한 요청을 제공한다.
이 다이얼로그는 앱 설정의 알림 권한 요청과 연결되는 기능으로, 총 2번만 뜬다.
즉, 다이얼로그를 계속 띄우고 싶으면 앱을 삭제했다가 다시 설치하는 방법밖에 없다. (이 때문에 terning에서는 앱 진입 시에만 안드로이드 제공 다이얼로그를 띄우고, 마이페이지 뷰에서의 푸시알림 수신 여부 결정은 "별도의 다이얼로그"를 통해 안내했다.)
 
여기서 하나 더 주의해야 할 점이 있다. 바로 해당 다이얼로그는 Android 13이상에서만 뜬다는 것이다. 
 
권한 요청을 위한 코드는 짚고 넘어가겠다. (해당 코드는 Jetpack Compose를 사용한다)
우선 권한 관련 라이브러리를 넣어준다.

[versions]
permissions = "0.33.1-alpha"

[libraries]
permissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "permissions" }

 
이제 앱 시작 시 알림 권한을 요청해야 한다.
(1) 알림 권한 상태를 기억했다가
(2) 권한이 부여되지 않았고, 한 번도 요청한 적이 없다면 권한 요청 다이얼로그를 띄운다.
(3) 다이얼로그를 닫았을 때 권한변경을 감지하여 해당 상태를 업데이트 해 준다.

val permissionState =
    rememberPermissionState(permission = Manifest.permission.POST_NOTIFICATIONS) // (1)

LaunchedEffect(Unit) {
    if (!permissionState.status.isGranted && !viewModel.getPermissionRequested()) {
        permissionState.launchPermissionRequest() // (2)

        snapshotFlow { permissionState.status }
            .map { it is PermissionStatus.Granted }
            .distinctUntilChanged()
            .collectLatest { isGranted ->
                viewModel.updateAlarmAvailability(isGranted)
                viewModel.updatePermissionRequested(true)
            } // (3)
    }
}

 
자세한 전체 코드는 아래 링크로 확인 바란다.
📍앱 시작 시 코드 (홈 뷰) : https://github.com/teamterning/TerningAndroid/blob/develop/feature/home/src/main/java/com/terning/feature/home/HomeRoute.kt
📍푸시 알림 권한 설정 코드 (마이페이지 뷰) : https://github.com/teamterning/TerningAndroid/blob/develop/feature/mypage/src/main/java/com/terning/feature/mypage/mypage/MyPageRoute.kt
 

알림 구현 방법 

이제 FirebaseMessaging을 활용해서 본격적으로 알림을 구현해 보자.

@AndroidEntryPoint
class TerningMessagingService : FirebaseMessagingService() {
}

 
FirebaseMessaging를 상속받아서 클래스를 만들어 준다. 해당 클래스는 메니페스트에 등록해 줘야 한다.
메니페스트에 등록해줘야 하는 이유는 안드로이드의 4대 컴포넌트와도 관련이 있다.

<service
    android:name="com.terning.core.firebase.messageservice.TerningMessagingService"
    android:exported="false">
    <intent-filter>
        <action android:name="com.google.firebase.MESSAGING_EVENT" />
    </intent-filter>
</service>

 
service 태그를 통해 알 수 있듯이 백그라운드 서비스를 통해 실행시키는 것이기 때문이다! 
안드로이드 4대 컴포넌트는 사용자가 앱에 들어올 수 있는 진입점을 나타낸다. 
즉, 4대 컴포넌트 중 하나인 서비스를 통해 앱을 백그라운드에서 FCM  메시지를 처리하는 거라고 이해하면 된다!
 
다시 TerningMessagingService로 돌아와서 코드가 어떤 구조로 되어있는지 살펴보자. 이번 블로그에서 남은 시간 동안 알아볼 부분은 밑줄로 표시해 놨다.

  • 오버라이드한 메소드
    • FCM 토큰 수신 (onNewToken)
    • 백그라운드 메시지 처리 (handleIntent)
    • 포그라운드 메시지 처리 (onMessageReceived)
  • 필자가 정의한 메소드
    • 사용자 알림 생성 및 표시 (sendNotification)
    • 알림 클릭 시 딥링크 처리
    • 앱 상태(Foreground 여부)에 따라 알림 동작 분기
    • 이미지 포함 알림 처리

 

알림 클릭 시 딥링크 처리 ✔️

terning에서는 알림의 종류가 총 3가지가 있는데, 각각의 알림을 클릭했을 때 이동해야 하는 화면이 달랐다. 
원래 알림으로 화면 이동을 할 때는 PendingIntent를 활용해서 이동해 주면 된다. (실제로 다른 블로그들에도 이와 같이 정리해 놓았다)
 
그러나 terning은 싱글 액티비티를 사용한다. 즉, 인텐트로 액티비티(화면) 이동은 딱 한 번 가능한 것이다. 
그래서 필자가 생각해 낸 방법은 딥링크이다.
 
큰 흐름은 아래와 같다.

  • 각 화면(Screen)마다 딥링크를 등록해 준다.
  • 서버에서 이동해야 하는 화면의 타입을 받는다. (terning의 경우:  HOME, SEARCH, CALENDAR)
  • 해당 타입에 따라 딥링크를 생성한다. (예: terning://home)
  • 딥링크를 intent의 data에 넣어 MainActivity로 이동했을 때 딥링크로 화면을 바로 띄운다. 
  • 이때 해당 딥링크 값에 따라 NavHost의 startDestination을 바꿔준다. (그래야 앱의 시작점이 딥링크를 기준으로 형성된다.)

 
딥링크를 사용하기 위해서는 Scheme을 메니페스트에 등록해 줘야 한다. tenring은 "terning://{화면 이름}" 형식으로 딥링크를 작성하였다.

<intent-filter>
    <action android:name="android.intent.action.VIEW" />

    <category android:name="android.intent.category.DEFAULT" />
    <category android:name="android.intent.category.BROWSABLE" />

    <data android:scheme="terning" />
</intent-filter>

 
각 화면의 딥링크는 Navigation에 다음과 같이 넣어주면 된다.

fun NavGraphBuilder.homeNavGraph() {
    composable<Home>(
        deepLinks = listOf(
            navDeepLink<Home>(
                basePath = "terning://home"
            )
        )
    ) {
        HomeRoute()
    }
}

 

포그라운드 여부에 따른 동작 분기 ✔️

포그라운드(앱 실행 상태) 여부에 따라서 동작 분기도 해 줘야 한다.
왜냐하면 terning은 알림을 클릭했을 때 무조건 스플래쉬 화면을 거치기로 했다. 그러나, 앱을 실행 중일 때 알림을 클릭했을 때도 스플래쉬가 뜬다면 사용자 입장에서는 어색한 동작이 될 것이다.
 
그래서 포그라운드 여부를 감지하여

  • 백그라운드 상태일 때는 스플래쉬를 거친 후 특정 화면 이동하도록 (딥링크: "terning://splash?redirect=home")
  • 포그라운드 상태일 때는 바로 특정 화면으로 이동하도록 했다. (딥링크: "terning://home")

포그라운드 여부는 아래 함수로 식별 가능하다.

private fun isAppInForeground(): Boolean {
    val appProcesses =
        (getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager).runningAppProcesses

    return appProcesses?.any {
        val isForeground =
            it.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND
        val isCurrentApp = it.processName == packageName

        isForeground && isCurrentApp
    } == true
}

 

이미지 포함 알림 처리 ✔️

위에서 언급했다시피 terning은 큰 아이콘을 적용하기로 했다. 아래와 같이 말이다!

알림의 형식을 만드는 Notification.BuildersetLargeIcon속성을 넣으면 쉽게 해결되긴 한다. (필자가 찾았을 때도 대부분 이렇게 설명되어 있었다.)
공식문서에도 아래처럼 Bitmap으로 만들어 바로 아이콘을 넣어주었다. 

val notification = NotificationCompat.Builder(context, CHANNEL_ID)
        .setLargeIcon(BitmapFactory.decodeResource(resources, R.drawable.logo))
         // 생략
        .build()

 
그러나 terning의 알림 3종류는 각각 들어가는 큰 아이콘도 달랐기 때문에 서버에서 받은 값으로 넣어줘야만 했다.
따라서 Coil3를 사용하여 url을 Bitmap으로 변환해주었다.

val imageLoader = ImageLoader(this)
val request = ImageRequest.Builder(this)
    .data(imageUrl)
    .target(
        onSuccess = { image ->
            val bitmap = (image as BitmapImage).bitmap
            notificationBuilder.setLargeIcon(bitmap)
            notificationManager?.notify(notifyId, notificationBuilder.build())
        },
        onError = {
            notificationManager?.notify(notifyId, notificationBuilder.build())
        }
    )
    .build()

imageLoader.enqueue(request)

 

파이어베이스에서 테스트 하기

추가로, 알림을 파이어베이스에서 테스트 할 수 있는 방법에 대해 작성하려고 한다.

 

Firebase 링크로 들어가서 Messaging 탭으로 들어간다.

오른쪽 상단에 "새 캠패인" -> "알림" 을 누르면 아래와 같은 탭을 확인할 수 있다.

바로, key-value 형태로 넣는 구간이다.

 

여기에 key-value로 넣은 값들은 아래처럼 앱에서 처리할 Payload의 data 값으로 들어간다.

이는 Firebase 공식문서에도 잘 설명되어있다. 안드로이드의 경우, data 안에만 값이 들어가 있다면 Payload에서 값을 꺼내 쓸 수 있다.

{
  "data": {
    "title": "...",
    "body": "...",
  },
}

 

 

마치며

이렇게 길고 길었던 FCM 푸시알림 구현이 끝났다.🥹
 
사실 안드로이드는 단순히 알림을 감지하고 띄우기만 하면 되기 때문에 그다지 어려운 작업은 아니지만
안드로이드의 앱 진입점인 Service 공부부터 시작해서, 딥링크 로직, 알림 커스텀 등등 처음 구현해보기에 공부해야 할 부분이 많아서 시간이 오래 걸렸던 것 같다.
그치만 안드로이드에 대해 전반적으로 알아갈 수 있었던 좋은 기회였던 것 같다!
 
Firebase 관련 모든 코드들이 모아져 있는 :core:firebase 모듈 링크를 남기면서 마무리 하겠다!!
https://github.com/teamterning/Terning-Android/tree/develop/core/firebase/src/main/java/com/terning/core/firebase

 

Terning-Android/core/firebase/src/main/java/com/terning/core/firebase at develop · teamterning/Terning-Android

💚 지금이 안드의 터닝포인트~ 💚. Contribute to teamterning/Terning-Android development by creating an account on GitHub.

github.com