nueijeel

[Android] 앱이 Background 상태일 때 fcm 알림 수신하기 본문

Android/에러 및 문제 해결

[Android] 앱이 Background 상태일 때 fcm 알림 수신하기

nueijeel 2025. 1. 21. 23:41

 

프로젝트에서 FCM(Firebase Cloud Messaging)을 이용한 푸시 알림을 구현해야 했는데

앱이 Background 상태일 때 푸시가 뜨지 않고 작업 표시줄에만 알림이 표시되는 현상이 발생했다.

 

fcm이 알림을 처리하는 방식과 관련이 있기 때문에 이걸 이용해서 해결해보려고 한다.

 

 

앱 상태에 따른 수신 메시지 핸들링

 

Firebase 공식 문서에 따르면

FCM은 FirebaseMessagingService 클래스의 onMessageReceived() 함수를 통해 메시지를 수신한다.

 

보통의 경우 onMessageReceived() 콜백 함수가 작동해 알림을 표시하도록 하면 되지만 두 가지 예외 케이스가 있다.

 

    1. 앱이 Background 상태일때 Notification 메시지만 수신되는 경우

          -> 기기의 System tray에 메시지가 전달되어 표시됨

 

    2. 앱이 Background 상태일 때 Notification, Data 메시지 모두 수신되는 경우

         -> Notification 메시지는 System tray에, Data는 실행되는 Activity의 Intent 객체 내 extras 로 전달됨

 

 

 

현재 안드로이드가 서버로부터 넘겨받는 알림 데이터 구조를 살펴보면 아래와 같은 형태로

{
  "message":{
    "to":"수신 기기의 fcm token 값",
    "notification":{
      "title":"알림 타이틀",
      "body":"알림 콘텐츠"
    },
    "data" : {
      "boardId" : "00",
      "acceptStatus" : "PENDING"
    }
  }
}

 

알림 창에 표시될 body와 title이 담긴 notification 필드와

화면 전환에 필요한 데이터가 담긴 data 필드가 함께 수신되고 있다.

 

 

따라서 이 앱은

Foreground 상태에서 알림 수신

 

Foreground 상태일 때 onMessageReceived 함수를 통해 알림 데이터를 전달받고

 

Background 상태에서 알림 수신

 

Background 상태일 때 Notification은 System tray로, Data는 intent.extras로 알림 데이터를 전달받기 때문에

메시지가 수신되어도 onMessageReceived() 함수가 호출되지 않는다.

 

 

해결 방법

 

이 문제를 해결할 방법은 두 가지가 있다.

  • 서버에 payload 수정 요청
  • FirebaseMessagingService 내부적으로 메시지 처리하는 함수 오버라이딩

 

이 앱이 안드로이드로만 개발되는거라면 바로 서버에 메시지 형식 수정을 요청했겠지만 ios도 함께 개발하는 중이기 때문에 서버 수정은 신중히 결정해야 한다.

 

서버에서 알림 데이터를 보내면 안드로이드에 notification 필드로 담겨오는 내용이 ios에서는 aps라는 필드로 전달되는데

ios에서는 Notification Payload로써 지정되는 JSON Dictionary 내에 한 개 이상의 aps Key가 포함되야 하기 때문에 aos를 위해서 모든 값들을 data 필드로 보내도록 수정하는 방법은 선택할 수 없었다.

 

또 notification 필드에 click_action 속성을 추가해 시작할 Activity의 intent-filter로 활용하는 방법도 있다고 하는데 

Main Activity를 등록해도 activity 내에서 또 Fragment 이동 분기도 처리해야 하고..

서버 쪽이 불필요한 수정 과정을 거치게 되고 ios쪽도 필요없는 값이 전달되기 때문에 그다지 적절하지 않은 방법이라고 생각했다.

 

 

그래서 남은 방법으로 문제를 해결했다.

 

 

우선 FirebaseMessagingService 클래스 내부의 함수를 살펴보면

  /** Dispatch a message to the app's onMessageReceived method, or show a notification */
  private void dispatchMessage(Intent intent) {
    Bundle data = intent.getExtras();
    if (data == null) {
      // The intent should always have at least one extra so this shouldn't be null, but
      // this is the easiest way to handle the case where it does happen.
      data = new Bundle();
    }
    // First remove any parameters that shouldn't be passed to the app
    // * The wakelock ID set by the WakefulBroadcastReceiver
    data.remove("androidx.content.wakelockid");
    if (NotificationParams.isNotification(data)) {
      NotificationParams params = new NotificationParams(data);

      ExecutorService executor = FcmExecutors.newNetworkIOExecutor();
      DisplayNotification displayNotification = new DisplayNotification(this, params, executor);
      try {
        if (displayNotification.handleNotification()) {
          // Notification was shown or it was a fake notification, finish
          return;
        }
      } finally {
        // Ensures any executor threads are cleaned up
        executor.shutdown();
      }

      // App is in the foreground, log and pass through to onMessageReceived below
      if (MessagingAnalytics.shouldUploadScionMetrics(intent)) {
        MessagingAnalytics.logNotificationForeground(intent);
      }
    }
    onMessageReceived(new RemoteMessage(data));
  }

 

fcm 메시지가 도착했을 때 메시지를 분석하여 알림으로 표시할지 onMessageReceived() 함수로 전달할지를 결정한다.

 

NotificationParams.isNotification(data)를 호출하여 해당 메시지가 알림 관련 메시지인지 확인하고,

알림인 경우 DisplayNotification 객체를 생성해 알림 처리 로직을 수행한다.

알림이 아닌 경우 RemoteMessage 객체로 데이터를 변환해 onMessageReceived() 함수에 전달한다.

  public static boolean isNotification(Bundle data) {
    return "1".equals(data.getString(MessageNotificationKeys.ENABLE_NOTIFICATION))
        || "1"
            .equals(data.getString(keyWithOldPrefix(MessageNotificationKeys.ENABLE_NOTIFICATION)));
  }

 

isNotification() 함수에서는 해당 메시지 데이터의 키값을 비교해 알림인지 여부를 반환해준다.

 

fcm 메시지가 도착했을 때 dispatchMessage() 함수가 호출 되기 전 알림 데이터가 전달되는 handleIntent() 함수를 재정의하여 notification 필드를 제거한 메시지를 넘겨주도록 했다.

 

override fun handleIntent(intent: Intent?) {
    val new = intent?.apply {
        val temp = extras?.apply {
            remove(MessageNotificationKeys.ENABLE_NOTIFICATION)
            remove(keyWithOldPrefix(MessageNotificationKeys.ENABLE_NOTIFICATION))
        }
        replaceExtras(temp)
    }
    super.handleIntent(new)
}
 

https://medium.com/@jms8732/background%EC%97%90%EC%84%9C-onmessagereceived%EA%B0%80-%ED%98%B8%EC%B6%9C-%EC%95%88%EB%90%98%EB%8A%94-%ED%98%84%EC%83%81%EC%97%90-%EA%B4%80%ED%95%98%EC%97%AC-7595df624d91

해당 링크를 참고해 handleIntent 함수를 작성했는데

 

 

keyWithOldPrefix() 메서드가 작동하지 않아서 살펴보니

내부에서 private 제한자가 붙어서 호출할 수 없었다.

private static String keyWithOldPrefix(String key) {
  if (!key.startsWith(MessageNotificationKeys.NOTIFICATION_PREFIX)) {
    return key;
  }

  return key.replace(
      MessageNotificationKeys.NOTIFICATION_PREFIX,
      MessageNotificationKeys.NOTIFICATION_PREFIX_OLD);
}
// NOTIFICATION_PREFIX -> "gcm.n"
// NOTIFICATION_PREFIX_OLD -> "gcm.notification"

 

keyWithOldPrefix 함수는 key가 "gcm.n"으로 시작하지 않으면 그대로 키 값을 반환하고

그렇지 않으면 해당 키의 "gcm.n"부분을 "gcm.notification"으로 치환해 반환하는 구조를 가지고 있었다.

 

그래서 handleIntent 함수를 아래와 같이 수정해서 notification 관련 키를 제거해주었다.

override fun handleIntent(intent: Intent?) {
    val new = intent?.apply {
        val temp = extras?.apply {
            remove(MessageNotificationKeys.ENABLE_NOTIFICATION)
            remove("gcm.notification.e")
        }
        replaceExtras(temp)
    }
    super.handleIntent(new)
}

 

이제 앱이 Background 상태일 때도 onMessageReceived() 함수가 호출되어

Foreground 상태일 때와 마찬가지로 푸시 알림을 띄울 수 있었다.

 

 

하지만 이렇게하면 Foreground 상태일때도 마찬가지로 Notification 필드가 지워진 상태이기 때문에 푸시 알림에 표시되는 타이틀과 콘텐츠가 없다.

lateinit var body: String
lateinit var title: String

override fun handleIntent(intent: Intent?) {
    body = intent?.extras?.getString("gcm.notification.body") ?: ""
    title = intent?.extras?.getString("gcm.notification.title") ?: ""
    ...
}

 

간단하게 전역변수를 선언해서 handleIntent 함수 내에서 notification 필드를 지우기 전 저장해두고 onMessageReceived 함수에서도 사용할 수 있게 해결했다.

 

 

 

 

728x90