[Flutter] firebase_messaging과 flutter_local_notifications을 사용한 푸시 알림 기능 구현

2021. 10. 18. 13:28Programming/Flutter

반응형

이번에는 파이어베이스를 사용해서 모바일 디바이스에 푸시 알림을 보내고, 알림 소리를 커스터마이징하는 방법에 대해 정리해봅니다. 대충 찾아봐도 예제가 한가득 나오는 것 같지만 굳이 정리하는 이유가 별도로 있는데요. 검색과 삽질을 통해 확인해본 결과 대부분 검색 결과가 예제에만 그쳤기 때문에, 여러 검색 결과를 머릿속에 그러모아 짜집기해서 조립한 뒤에도 삽질에 삽질을 거듭한 뒤에야 간신히 원하는 기능을 구현할 수 있었기 때문입니다.

대부분의 경우 예제만 가지고 필요한 기능을 구현할 수 있겠지만, 검색을 거듭하다 결국 변방의 이 블로그에 이르렀다는 건 삽질에 삽질을 거듭하고 있다는 증거겠죠. 혹시나 삽질에 도움이 되는 내용이 있는지 빠르게 확인할 수 있게, 어떤 순서로 진행할 건지 나열해보겠습니다. 아, 공식문서가 마련되어있는 내용은 길게 서술하지 않고, 공식문서 링크로 대체했습니다.

진행순서

기본적인 내용은 FlutterFire 공식문서를 기반으로 하고 있습니다. 업데이트를 거치면서 onLaunch(), onResume()같은 함수들이 deprecated됐듯 이 글에서 기술된 내용도 미래에는 deprecated됐을 수 있으므로, 뭔가 이상하다싶으면 FlutterFire 공식문서를 확인하도록 합시다.

패키지 설치

우선 파이어베이스에서 클라우드 메시징을 사용하기 위해서 firebase_messaging 패키지와 firebase_core 패키지를 설치해줍니다. 그리고, 수신된 메시지를 기반으로 푸시 알람을 띄우기 위해서 flutter_local_notifications 패키지를 설치해줍니다.

Flutter 앱에 파이어베이스 추가하기 (플랫폼 별 설정)

Flutter 패키지가 설치됐다면 Flutter 앱에 Firebase 추가하기 페이지를 참조하여 Android/iOS 플랫폼 별 설정을 하도록 합시다. 이 내용은 공식문서에 자세하게 기술되어있으므로 생략합니다.

대충 요약하면 안드로이드는 android/build.gradle에 firebase 라이브러리를 다운받을 Google Maven Repository, Google Services Plugin을 추가해주고, android/app/build.gradleGoogle Services Plugin을 적용해줍니다.

GoogleService-Info.plist(iOS), google-services.json(Android) 파일을 추가해주는 것도 잊지 마세요. 추가한 파일에서 App ID를 찾지 못하는 경우, 해당 파일 내에서 App ID를 찾을 수 없다는 에러가 발생합니다.

iOS를 위해 Firebase와 APNs를 연동하는 방법은 [https://firebase.flutter.dev/docs/messaging/apple-integration/] 문서를 참조해주세요. 아니면 eunding님의 '[Flutter] Firebase Cloud Messaging 연동 + 파베 콘솔에서 푸쉬보내기'에도 잘 설명되어있습니다. 연동이 제대로 됐더라도 iOS 에뮬레이터에서는 푸쉬 메시지를 수신할 수 없다는 점에 주의하시구요. (*2021년 10월 기준으로 이 글에서 기재되어있는 연동방법은 deprecated됐습니다. 참고해주세요.)

플랫폼에 따른 추가 설정은 아래의 링크를 참조해주세요.

Firebase 초기화

  • Firebase.initializeApp();
    • Firebase를 사용하기 위해서는 Firebase core의 initializeApp()을 호출해서 초기화해야 합니다. 초기화하지 않은 채 사용하려고 하면 예외가 발생하니 주의해주세요.
  • 권한 요청
    • 푸쉬 메시지를 띄우기 위해서는 FirebaseMessaging의 requestPermission()을 요청해서 권한을 요청해야합니다. requestPermission의 인자값은 alert, announcement, badge, carPlay, criticalAlert, provisional이며, 어떤 옵션을 제공하는지는 FlutterFire - Requesting Permission 페이지를 참조해주세요.
  • 안드로이드 NotificationChannel 생성
    • 안드로이드에서 푸쉬 알람을 보내기 위해서는 채널을 생성해야합니다. 인자값은 id, name, description, groupId, importance, playSound, sound, enableVibration, vibrationPattern, showBadge, enableLights, ledColor이며, id, name, description 이외에는 optional입니다. 채널에 따른 커스텀 사운드 등도 채널을 생성할 때 지정할 수 있습니다.
    • 안드로이드에서 flutter_local_notifications을 사용하여 푸쉬 메시지를 띄우기 위해서는, 위에서 생성한 채널을 flutter_local_notifications에 등록해줘야합니다. createNotificationChannel()에 앞에서 생성한 채널을 인자로 넘겨줘서 채널을 생성하도록 합시다.
  • iOS 알림 설정
    • iOS에서 FirebaseMessaging의 setForegroundNotificationPresentationOptions()를 호출하면 알림 설정에 필요한 옵션을 지정할 수 있습니다.
  • FlutterLocalNotificationsPlugin의 초기화
    • 출력된 푸쉬 알림을 탭했을 때에 대한 동작을 처리하기 위해, FlutterLocalNotificationsPlugin의 initialize()를 통해 callback을 등록해줍니다.
      초기화에 대한 코드는 아래의 예제를 참조해주세요.
Future<void> main() async {
  await Firebase.initializeApp();
  NotificationSettings settings = await FirebaseMessaging.instance.requestPermission(
      alert: true,
      announcement: false,
      badge: true,
      carPlay: false,
      criticalAlert: false,
      provisional: false,
      sound: true,
    );

  if (settings.authorizationStatus == AuthorizationStatus.authorized) {
    FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();

    flutterLocalNotificationsPlugin.initialize(
      InitializationSettings(
        android: AndroidInitializationSettings("default_icon"),
        iOS: IOSInitializationSettings(),
      ),
      onSelectNotification: (String payload) {},
    );

    if (Platform.isAndroid) {
      AndroidNotificationChannel channel =
        AndroidNotificationChannel(
          'NOTIFICATION_CHANNEL', // id
          'Push Notification', // title
          'This channel is used for push notification.', // description
          importance: Importance.high,
        );

      await flutterLocalNotificationsPlugin
          .resolvePlatformSpecificImplementation<
              AndroidFlutterLocalNotificationsPlugin>()
          ?.createNotificationChannel(channel);
    }

    if (Platform.isIOS) {
      await FirebaseMessaging.instance
          .setForegroundNotificationPresentationOptions(
        alert: true,
        badge: true,
        sound: true,
      );
    }

  } else if (settings.authorizationStatus == AuthorizationStatus.provisional) {
    print('User granted provisional permission');
  } else {
    print('User declined or has not accepted permission');
  }
}

Foreground에서 푸쉬 알람 출력

FirebaseMessaging.onMessage.listen()을 호출하면 FirebaseMessaging으로 전달되는 메시지를 수신할 수 있습니다. listen은 StreamSubscription을 반환하므로, listen에 인자로 넘겨준 onData, onError, onDone, onCancelOnError 콜백을 통해 처리합니다. 이때 RemoteMessage는 onData로만 수신 가능하므로, 별도의 처리가 필요 없다면 onData만 신경써주면 됩니다.

아래의 코드는 FirebaseMessaging에서 onMessage로 메시지를 수신했을 때, FlutterLocalNotificationPlugin의 show()를 호출해서 메시지를 표시합니다. flutterLocalNotificationsPluginchannel값은 위에서 선언한 값입니다. Optional로 제공되는 payload 값은 푸시 메시지를 탭했을 때, 인자로 전달될 데이터를 의미합니다.

FirebaseMessaging.onMessage.listen(
  (RemoteMessage message) async {
    RemoteNotification notification = message.notification;
    flutterLocalNotificationsPlugin.show(
      notification.hashCode,
      "Push message title",
      "Push message content,
      NotificationDetails(
        android: AndroidNotificationDetails(
          channel.id,
          channel.name,
          channel.description,
          icon: 'default_icon',
          importance: Importance.max,
          priority: Priority.max,
          enableLights: true,
          visibility: NotificationVisibility.public,
        ),
        iOS: IOSNotificationDetails(),
      ),
      payload: message?.data,
    );

  },
);

Background에서 푸쉬 알람 출력

FirebaseMessaging의 onBackgroundMessage()를 호출하면, isolate가 생성되어 인자로 전달한 callback함수가 동작하게 됩니다. 별도의 isolate이므로 등록한 callback이 동작할 때도 Firebase를 초기화해줘야된다는 점에 주의해주세요.

그 외에 메시지를 띄우는 점은 동일하므로, 중복되는 코드는 생략하도록 하겠습니다.

FirebaseMessaging.onBackgroundMessage((RemoteMessage message) async {
  await Firebase.initializeApp();
  // Firebase 초기화 구문과 동일
  // Foreground에서 푸쉬 알람 출력 구문과 동일
});

커스텀 사운드

먼저 iOS의 Notifications In Your App - Preparing Custom Alert Sounds 페이지를 확인합니다. 페이지를 확인해보면 30초 이내의 사운드만 지정할 수 있다고 명시되어있습니다. 안드로이드는 별도의 제한이 없으니, iOS의 제한에 맞추도록 합시다.

afconvert를 사용하면 .caf형식으로 변환하는게 가능합니다. 아래는 afconvert를 사용해서 ``Submarine.aiff 파일을 caf 파일로 변환하는 방법입니다.

afconvert /System/Library/Sounds/Submarine.aiff ~/Desktop/sub.caf -d ima4 -f caff -v

안드로이드는 mp3 파일을 사용하면 됩니다. iOS를 위해 변환한 caf 파일은 /ios/Resources/에 넣어주고, 안드로이드를 위한 mp3 파일은 /android/app/src/main/res/raw에 넣어줍니다. 이 때 안드로이드는 /android/app/src/main/res/raw 경로에 keep.xml파일을 작성하지 않으면 릴리즈 시 이미지 파일과 음악 파일이 바이너리에 포함되지 않는다는 점에 주의해주세요. 또, 파일명은 소문자와 언더스코어만 사용할 수 있다는 점도 주의해주세요.

아래는 drawable 및 raw 경로에 있는 파일들을 모두 바이너리에 포함시키기 위한 keep.xml파일의 내용입니다.

<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"
    tools:keep="@drawable/*,@raw/*" />

이제 FlutterLocalNotificationsPluginshow()를 호출할 때 플랫폼 별 NotificationDetails를 설정할 시 sound값으로 커스텀 사운드를 넘겨주기만 하면 됩니다. 안드로이드는 RawResourceAndroidNotificationSound()를 사용해서, iOS는 파일명을 문자열로 지정해주면 됩니다.

아래는 FlutterLocalNotificationsPluginshow()를 호출하는 코드입니다.

FlutterLocalNotificationsPlugin.show(
  notification.hashCode,
  "PUSH_MESSAGE_TITLE",
  mappedLog,
  NotificationDetails(
    android: AndroidNotificationDetails(
      "PUSH_MESSAGE_ID",
      "PUSH_MESSAGE_NAME",
      "PUSH_MESSAGE_CONTENTS",
      icon: 'default_icon',
      importance: Importance.max,
      priority: Priority.max,
      enableLights: true,
      visibility: NotificationVisibility.public,
      playSound: true,
      sound: RawResourceAndroidNotificationSound('custom_sound'),
    ),
    iOS: IOSNotificationDetails(
      presentAlert: true,
      presentBadge: true,
      presentSound: true,
      sound: 'custom_sound.caf',
    ),
  ),
  payload: message?.data,
);

정리

이것으로 flutter_local_notifications 패키지와 firebase_messaging 패키지를 사용하여 Flutter에서 푸쉬 메시지를 다루는 방법에 대해 살펴봤습니다. 혹시 작성한 글에 문제가 있거나, 잘못된 내용은 댓글로 제보해주시면 바로 수정하도록 하겠습니다. :)


추가

FlutterLocalNotificationsPlugin.getNotificationAppLaunchDetails()을 사용하면 LocalNotificationPlugin을 사용해서 앱이 시작됐을 때, 푸시 메시지에서 payload로 전달한 값을 읽어들일 수 있습니다. FirebaseMessaging.onMessageOpenedApp()는 푸시 메시지 수신으로 앱이 실행됐을 때 발생하므로, 착각하지 않도록 주의합시다.

반응형