본문 바로가기
Flutter/FCM

[Flutter/FCM] FCM과 Flutter Local Notification으로 iOS 푸시 알림 기능 구현 시 주의점 : apns push type 변경에 따른 대응

by 6cess 2024. 12. 14.

FCM으로 메시지를 송수신하고 Flutter Local Notifications 라이브러리로 알림을 표시하는 기능을 유지 보수하다가

iOS 알림이 안정적이지 않은 이슈를 해결한 과정과 내용을 공유하려 한다.

0. 프로젝트 및 이슈  설명

이슈가 발생한 이 프로젝트는 웹소켓으로  채팅 서비스를 구현하고 필요시에는 FCM으로 알림 메시지를 송수신하면 수신한 앱에서 Flutter Local Notification 라이브러리로 로컬 알림을 생성하도록 설계되었다.

 

하지만 문제는 iOS 백그라운드 푸시 알림이 종종 기기의 메모리 부족 문제로 메시지 수신과 앱 초기화 과정에서 멈추어 알림이 생성되지 않는 이슈 발생

 

1. apns-push-type : background 의 한계

 

서버(Spring+FCM라이브러리)에서는 APNs 헤더 값을 따로 설정하지 않고 메시지에 Data키에 데이터를 담아 보내고 있었다.  

private void sendDataTo(String deviceid, ChatMessage dto) throws FirebaseMessagingException {
    System.out.println("data sent by user "+deviceid);
    Message data = Message.builder()
            .putData("roomid", dto.getRoomid())
            .putData("fromid", dto.getFrom())
            .putData("message", dto.getMessage())
            .putData("subcon", dto.getSubcon())
            .setToken(deviceid)
            .build();
    String rtn = this.instance.send(data);
    System.out.println("return from fcm data : "+rtn);
}

 

Notification 키 없이 Data 키만 보낼 경우 APNs 에서는 기본적으로 apns-push-type이 Background으로 간주된다.

apns-push-type이 Background 일 경우 iOS 시스템은 알림을 직접 발생시키지 않고 단지 해당 앱을 깨워 메시지를 전달한다.

 

Sending notification requests to APNs | Apple Developer Documentation

Transmit your remote notification payload and device token information to Apple Push Notification service (APNs).

developer.apple.com

 

그래서 apns-push-type이 background인 경우 앱에서 직접 푸시 알림을 발생시켜야 한다. 이를 위해 현 프로젝트는 Flutter Local Notifications 라이브러리를 사용하여 앱에서 로컬 알림을 발생시킨 것.

 

flutter_local_notifications | Flutter package

A cross platform plugin for displaying and scheduling local notifications for Flutter applications with the ability to customise for each platform.

pub.dev

 

하지만 문제는 apns-push-type: background 인 메시지인 경우 기기의 메모리 상태, 우선 순위 등으로 인해 메시지 수신과 앱 초기화 과정에서 지연 및 생략될 가능성이 존재한다는 것이다. 그것이 내가 맞닥뜨린 이슈였던 것.

결국 우리 팀은 apns-push-type을 alert 로 변경하기로 결정했다. 물론 이에 대한 영향으로 기기의 배터리 소모 등 고려할 부분들이 있었지만 채팅 서비스의 알림은 기본적으로 알림은 무조건 수신될 것으로 기대되기 때문에 그렇게 결정했다.

2. APNs Header 설정 변경으로 인한 추가 작업들

apns-push-type을 background 에서 alert 로 변경할 때 가장 큰 변화는 백그라운드 상태에서는 iOS 시스템에서 FCM 메시지를 알림으로 여기고 알림을 직접 생성한다는 점이다.

이 프로젝트의 원래 설계 구조는 background 상태일 경우에 무조건 앱을 초기화한 후 전달 받은 메시지를 기반으로 앱이 알림을 생성을 하는 것이었다.

이제는 앱이 초기화되지 않아도 알림이 생성되기 때문에 이로 인한 추가 작업 사항들이 생겼다.

 

1. 앱이 메시지와 데이터를 받아 알림에 표시할 title, body, badge count, sound  값 등을 가공했다면 iOS 시스템에서 생성될 알림은 앱을 초기화하기 전에 발생하므로 서버에서 직접 가공하여 notification 키 값에 이를 포함하여 메시지를 전송해야 함

 

2. iOS 시스템에서 직접 생성한 알림의 메시지와 데이터는 어떻게 앱 내부로 가져올 것인가? 그리고 기존에는 Flutter Local Notificaions 라이브러리를 사용하여 앱이 알림을 생성할 때 터치 이벤트(ex 앱 진입 시 화면 전환 로직)를 포함시켰다면, 이제 iOS 시스템에서 직접 생성한 알림을 터치하여 앱에 진입했을 경우의 화면 전환 로직은 어떻게 처리할 것인가?

 

1번은 서버 개발자에게 요청하자 ..! ㅎㅎ

2번은 FCM이 제공하는 리스너 메서드들을 통해 해결해보자

 

3. FCM 의 중요 수신 메서드들 정리

먼저 FCM 문서에서 예시 코드만 뽑아왔다. 이 메서드들을 통해 iOS 시스템에서 직접 생성한 알림의 데이터를 어떻게 접근할 것인지 알아보자

 

1) onMessage : 포그라운드 메시지 수신

앱이 Foreground상태인 경우 이 리스너를 통해 메시지 데이터를 전달 받을 수 있다. 앱이 Foreground 상태에서는 alert, background 타입 메시지 모두 바로 이 리스너로 들어온다.

FirebaseMessaging.onMessage.listen((RemoteMessage message) {
  print('Got a message whilst in the foreground!');
  print('Message data: ${message.data}');

  if (message.notification != null) {
    print('Message also contained a notification: ${message.notification}');
  }
});

 

2) ⭐️onBackgroundMessage⭐️ : 백그라운드 메시지 수신

앱이 Background상태인 경우 이 리스너를 통해 메시지 데이터를 전달 받을 수 있다... 라고 말하면 자칫 잘못 이해하게 될 수도 있다!

메시지의 apns push type 값이 background 라면 위 문장는 맞는 말이다. 하지만 alert라면?

apns push type 이 alert 라면 앱이 background 상태여도 이 리스너로 메시지를 전달하지 않는다. 그저 iOS 시스템에서 알림을 생성하는 것으로 마무리 된다.

즉, alert타입 메시지는 앱이 초기화되거나 onBackgroundMessage 리스너가 호출되지 않는다

그러므로 alert 타입 메시지는 다른 경로를 통해 그 메시지 데이터에 접근해야 한다는 말 

그리고 이는 iOS가 직접 생성한 알림을 눌렀을 때 앱에 진입하게 되는 타이밍에 전달 받을 수 있다.

iOS가 직접 생성한 알림을 눌렀을 때 앱의 상태는 둘 중 하나이다(포그라운드는 FCM이 받으니) 종료되었거나, 백그라운드 상태이거나.

그래서 아래에 이어지는 두 메서드가 필요하다.

@pragma('vm:entry-point')
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
  // If you're going to use other Firebase services in the background, such as Firestore,
  // make sure you call `initializeApp` before using other Firebase services.
  await Firebase.initializeApp();

  print("Handling a background message: ${message.messageId}");
}

void main() {
  FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);
  runApp(MyApp());
}

 

3) getInitialMessage : 앱 종료 상태에서 알림을 눌러 애플리케이션이 열릴 때 해당 알림의 메시지 접근

// Get any messages which caused the application to open from
    // a terminated state.
RemoteMessage? initialMessage =
    await FirebaseMessaging.instance.getInitialMessage();

// If the message also contains a data property with a "type" of "chat",
// navigate to a chat screen
if (initialMessage != null) {
  _handleMessage(initialMessage);
}

 

4) onMessageOpenedApp : 백그라운드 상태에서 알림을 눌러 애플리케이션이 열릴 때 해당 알림의 메시지 접근

// Also handle any interaction when the app is in the background via a
// Stream listener
FirebaseMessaging.onMessageOpenedApp.listen(_handleMessage);

// ... //

void _handleMessage(RemoteMessage message) {
    if (message.data['type'] == 'chat') {
      Navigator.pushNamed(context, '/chat',
        arguments: ChatArguments(message),
      );
    }
}

 

4. iOS 에서 생성한 알림의 메시지 데이터를 가져오려면

이제 위 FCM 메서드들 정리를 하면 알 수 있겠지만

앱 초기화 과정에서 터치한 알림의 메시지 데이터를 얻으려면 getInitialMessage을 

백그라운드에서 포그라운드로 전환시킨 알림의 메시지 데이터를 얻으려면 onMessageOpenedApp 메서드를 활용하자

그리고 이 리스너들을 통해서 iOS 알림을 통해 앱에 진입했다는 것을 인식할 수 있으니 

Message 를 기반으로 이제 어느 화면으로 전환시켜야 하는지 결정한 후 화면 전환 로직을 실행하자!(이건 apns ,fcm 밖의 영역이니 생략)

5. 기타 주저리주저리

APNs, FCM 관련된 이 작업을 하면서 생각보다 시간을 소모하게 했던 것은 실제 서버에서 넘어오는 메시지의 APNs 설정이 우리 팀의 예상과는 다른 경우가 있다는 것이다.

APNs 설정은 문서에서도 알 수 있겠지만 단순히 하나의 헤더의 key value 를 추가한다고 그렇게 결정되어 넘어오지 않는다. apns-push-type 과 apns-priority 가 약속된 값으로 짝을 맞춰야 하는 경우도 있고, notification키가 있냐,data키 만 있냐에 따라서 따로 설정하지 않는 속성들이 바뀌는 경우도 있다.

이러한 apns header 설정에 대해 서버팀과 소통하여 제대로 설정한 다음, 실제 넘어오는 APNs 메시지의 Header를 디바이스 로그를 통해 확인해보며 작업하는 것이 시간 절약에 도움이 될 거라고.. 시간을 쏟아부었던 필자가 말해본다..