Flutter 앱에서 메시지 수신

받은 메시지는 기기 상태에 따라 다르게 처리됩니다. 이러한 시나리오와 FCM을 자체 애플리케이션에 통합하는 방법을 이해하려면 먼저 기기에 다양한 상태를 설정하는 것이 중요합니다.

상태 설명
포그라운드 애플리케이션이 열려 있고 보기 상태이며 사용 중인 경우
배경 애플리케이션이 열려 있지만 백그라운드에 있는 경우(최소화). 이는 일반적으로 사용자가 기기에서 '홈' 버튼을 누르거나 앱 전환기를 사용하여 다른 앱으로 전환했거나 다른 탭(웹)에서 애플리케이션을 열어 둔 경우에 발생합니다.
종료됨 기기가 잠겨 있거나 애플리케이션이 실행되고 있지 않은 경우.

애플리케이션에서 FCM을 통해 메시지 페이로드를 수신하려면 먼저 충족되어야 하는 몇 가지 전제 조건이 있습니다.

  • FCM에서 등록할 수 있게 하려면 애플리케이션이 최소 한 번 이상 열려 있어야 합니다.
  • iOS의 경우 사용자가 앱 전환기에서 애플리케이션을 스와이프하여 닫은 경우 백그라운드 메시지가 다시 작동하려면 수동으로 다시 열어야 합니다.
  • Android의 경우 사용자가 기기 설정에서 앱을 강제 종료한 경우 메시지가 다시 작동하려면 수동으로 다시 열어야 합니다.
  • 웹에서 웹 푸시 인증서로 토큰(getToken() 사용)을 요청해야 합니다.

메시지 수신 권한 요청

iOS, macOS, 웹 및 Android 13 이상(또는 그 이상)의 경우 FCM 페이로드가 기기에 수신될 수 있도록 하려면 먼저 사용자에게 권한을 요청해야 합니다.

firebase_messaging 패키지는 requestPermission 메서드를 통해 권한을 요청할 수 있는 간단한 API를 제공합니다. 이 API는 알림 페이로드가 포함된 메시징에서 사운드를 트리거하거나 Siri를 통해 메시지를 읽을 수 있는지 여부와 같이 요청하려는 권한 유형을 정의하는 이름이 지정된 인수 여러 개를 허용합니다. 기본적으로 메서드는 적절한 기본 권한을 요청합니다. 참조 API는 각 권한 용도에 대한 전체 문서를 제공합니다.

시작하려면 애플리케이션에서 메서드를 호출합니다. iOS에서는 네이티브 모달이 표시되고 웹에서는 브라우저의 네이티브 API 흐름이 트리거됩니다.

FirebaseMessaging messaging = FirebaseMessaging.instance;

NotificationSettings settings = await messaging.requestPermission(
  alert: true,
  announcement: false,
  badge: true,
  carPlay: false,
  criticalAlert: false,
  provisional: false,
  sound: true,
);

print('User granted permission: ${settings.authorizationStatus}');

요청에서 반환된 NotificationSettings 객체의 authorizationStatus 속성을 사용하여 사용자의 전체 결정을 확인할 수 있습니다.

  • authorized: 사용자에게 권한이 부여되었습니다.
  • denied: 사용자가 권한을 거부했습니다.
  • notDetermined: 사용자가 아직 권한 부여 여부를 선택하지 않았습니다.
  • provisional: 사용자에게 임시 권한이 부여됩니다.

NotificationSettings의 다른 속성은 현재 기기에서 특정 권한이 사용 설정, 중지 또는 지원되지 않는지 여부를 반환합니다.

권한이 부여되고 다양한 유형의 기기 상태가 파악되면 이제 애플리케이션에서 수신 FCM 페이로드를 처리할 수 있습니다.

메시지 처리

애플리케이션의 현재 상태에 따라 서로 다른 메시지 유형의 수신 페이로드를 처리하려면 서로 다른 구현이 필요합니다.

포그라운드 메시지

애플리케이션이 포그라운드에 있는 동안 메시지를 처리하려면 onMessage 스트림을 리슨합니다.

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}');
  }
});

스트림에는 페이로드의 출처, 고유 ID, 보낸 시간, 알림 포함 여부와 같이 페이로드에 대한 다양한 정보를 상세히 설명하는 RemoteMessage가 포함됩니다. 애플리케이션이 포그라운드에 있는 동안 메시지를 가져왔으므로 Flutter 애플리케이션의 상태와 컨텍스트에 직접 액세스할 수 있습니다.

포그라운드 및 알림 메시지

애플리케이션이 포그라운드에 있는 동안에 도착하는 알림 메시지는 기본적으로 Android와 iOS에 보이는 알림을 표시하지 않습니다. 그러나 이 동작을 재정의할 수 있습니다.

  • Android에서는 '높은 우선순위' 알림 채널을 만들어야 합니다.
  • iOS에서는 애플리케이션의 프레젠테이션 옵션을 업데이트할 수 있습니다.

백그라운드 메시지

백그라운드 메시지를 처리하는 프로세스는 네이티브(Android 및 Apple) 플랫폼과 웹 기반 플랫폼에서 다릅니다.

Apple 플랫폼 및 Android

onBackgroundMessage 핸들러를 등록하여 백그라운드 메시지를 처리합니다. 메시지가 수신되면 격리가 생성됩니다(Android 전용, iOS/macOS에는 별도의 격리가 필요 없음). 이를 통해 애플리케이션이 실행되지 않고 있더라도 메시지를 처리할 수 있습니다.

백그라운드 메시지 핸들러와 관련하여 유의해야 할 몇 가지 사항이 있습니다.

  1. 익명 함수가 아니어야 합니다.
  2. 최상위 수준 함수여야 합니다(예: 초기화가 필요한 클래스 메서드가 아님).
  3. Flutter 버전 3.3.0 이상을 사용하는 경우 메시지 핸들러는 함수 선언 바로 위에 @pragma('vm:entry-point')로 주석을 달아야 합니다(그렇지 않으면 출시 모드의 경우 트리 쉐이킹 중에 삭제될 수 있음).
@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());
}

핸들러는 애플리케이션 컨텍스트 외부에서 독립적으로 실행되므로 애플리케이션 상태를 업데이트하거나 UI에 영향을 주는 로직을 실행할 수 없습니다. 그러나 HTTP 요청과 같은 로직을 수행하고 IO 작업(예: 로컬 스토리지 업데이트)을 수행하며 다른 플러그인과 통신할 수 있습니다.

가능한 한 빨리 로직을 완료하는 것이 좋습니다. 집약적이고 오래 실행되는 태스크를 실행하면 기기 성능이 영향을 받고 OS에서 프로세스를 종료할 수 있습니다. 태스크가 30초 넘게 실행되면 기기에서 자동으로 프로세스를 종료할 수 있습니다.

웹에서는 백그라운드에서 실행되는 자바스크립트 서비스 워커를 작성합니다. 서비스 워커를 사용하여 백그라운드 메시지를 처리합니다.

시작하려면 web 디렉터리에 새 파일을 만들고 firebase-messaging-sw.js라고 이름을 지정합니다.

// Please see this file for the latest firebase-js-sdk version:
// https://github.com/firebase/flutterfire/blob/master/packages/firebase_core/firebase_core_web/lib/src/firebase_sdk_version.dart
importScripts("https://www.gstatic.com/firebasejs/10.7.0/firebase-app-compat.js");
importScripts("https://www.gstatic.com/firebasejs/10.7.0/firebase-messaging-compat.js");

firebase.initializeApp({
  apiKey: "...",
  authDomain: "...",
  databaseURL: "...",
  projectId: "...",
  storageBucket: "...",
  messagingSenderId: "...",
  appId: "...",
});

const messaging = firebase.messaging();

// Optional:
messaging.onBackgroundMessage((message) => {
  console.log("onBackgroundMessage", message);
});

이 파일은 앱과 메시징 SDK를 모두 가져오고 Firebase를 초기화하며 messaging 변수를 노출해야 합니다.

그런 다음 작업자를 등록해야 합니다. index.html 파일 내에서 Flutter를 부트스트랩하는 <script> 태그를 수정하여 작업자를 등록합니다.

<script src="flutter_bootstrap.js" async>
  if ('serviceWorker' in navigator) {
    window.addEventListener('load', function () {
      navigator.serviceWorker.register('firebase-messaging-sw.js', {
        scope: '/firebase-cloud-messaging-push-scope',
      });
    });
  }
</script>

여전히 이전 템플릿 시스템을 사용하는 경우 다음과 같이 Flutter를 부트스트랩하는 <script> 태그를 수정하여 작업자를 등록할 수 있습니다.

<html>
<body>
  <script>
      var serviceWorkerVersion = null;
      var scriptLoaded = false;
      function loadMainDartJs() {
        if (scriptLoaded) {
          return;
        }
        scriptLoaded = true;
        var scriptTag = document.createElement('script');
        scriptTag.src = 'main.dart.js';
        scriptTag.type = 'application/javascript';
        document.body.append(scriptTag);
      }

      if ('serviceWorker' in navigator) {
        // Service workers are supported. Use them.
        window.addEventListener('load', function () {
          // Register Firebase Messaging service worker.
          navigator.serviceWorker.register('firebase-messaging-sw.js', {
            scope: '/firebase-cloud-messaging-push-scope',
          });

          // Wait for registration to finish before dropping the <script> tag.
          // Otherwise, the browser will load the script multiple times,
          // potentially different versions.
          var serviceWorkerUrl =
            'flutter_service_worker.js?v=' + serviceWorkerVersion;

          navigator.serviceWorker.register(serviceWorkerUrl).then((reg) => {
            function waitForActivation(serviceWorker) {
              serviceWorker.addEventListener('statechange', () => {
                if (serviceWorker.state == 'activated') {
                  console.log('Installed new service worker.');
                  loadMainDartJs();
                }
              });
            }
            if (!reg.active && (reg.installing || reg.waiting)) {
              // No active web worker and we have installed or are installing
              // one for the first time. Simply wait for it to activate.
              waitForActivation(reg.installing ?? reg.waiting);
            } else if (!reg.active.scriptURL.endsWith(serviceWorkerVersion)) {
              // When the app updates the serviceWorkerVersion changes, so we
              // need to ask the service worker to update.
              console.log('New service worker available.');
              reg.update();
              waitForActivation(reg.installing);
            } else {
              // Existing service worker is still good.
              console.log('Loading app from service worker.');
              loadMainDartJs();
            }
          });

          // If service worker doesn't succeed in a reasonable amount of time,
          // fallback to plaint <script> tag.
          setTimeout(() => {
            if (!scriptLoaded) {
              console.warn(
                'Failed to load app from service worker. Falling back to plain <script> tag.'
              );
              loadMainDartJs();
            }
          }, 4000);
        });
      } else {
        // Service workers not supported. Just drop the <script> tag.
        loadMainDartJs();
      }
  </script>
</body>

다음으로 Flutter 애플리케이션을 다시 시작합니다. 작업자가 등록되며 백그라운드 메시지는 이 파일을 통해 처리됩니다.

상호작용 처리

알림은 시각적인 신호이므로 사용자는 일반적으로 알림을 눌러 상호작용합니다. Android 및 iOS의 기본 동작은 애플리케이션을 여는 것입니다. 애플리케이션이 종료된 상태라면 시작되고, 백그라운드에 있다면 포그라운드로 전환됩니다.

알림의 콘텐츠에 따라 애플리케이션이 열릴 때 사용자의 상호작용을 처리하려고 할 수도 있습니다. 예를 들어 새 채팅 메시지가 알림을 통해 전송되고 사용자가 알림을 누르면 애플리케이션이 열릴 때 특정 대화를 열어야 합니다.

firebase-messaging 패키지는 이 상호작용을 처리하는 두 가지 방법을 제공합니다.

  • getInitialMessage(): 애플리케이션이 종료된 상태에서 열리면 RemoteMessage가 포함된 Future가 반환됩니다. 소비되면 RemoteMessage가 삭제됩니다.
  • onMessageOpenedApp: 애플리케이션이 백그라운드 상태에서 열릴 때 RemoteMessage를 게시하는 Stream입니다.

사용자의 원활한 UX를 위해 두 시나리오를 모두 처리하는 것이 좋습니다. 아래 코드 예시에서 이를 처리하는 방법을 설명합니다.

class Application extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => _Application();
}

class _Application extends State<Application> {
  // It is assumed that all messages contain a data field with the key 'type'
  Future<void> setupInteractedMessage() async {
    // 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);
    }

    // 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),
      );
    }
  }

  @override
  void initState() {
    super.initState();

    // Run code required to handle interacted messages in an async function
    // as initState() must not be async
    setupInteractedMessage();
  }

  @override
  Widget build(BuildContext context) {
    return Text("...");
  }
}

상호작용을 처리하는 방법은 애플리케이션 설정에 따라 다릅니다. 위의 예시는 StatefulWidget을 사용한 기본적인 모습을 보여줍니다.

메시지 현지화

다음 두 가지 방법으로 현지화된 문자열을 전송할 수 있습니다.

  • 서버에 각 사용자의 기본 언어를 저장하고 언어별로 맞춤설정된 알림을 보냅니다.
  • 앱에 현지화된 문자열을 삽입하고 운영체제의 기본 언어 설정을 활용합니다.

다음은 두 번째 방법에 해당합니다.

Android

  1. resources/values/strings.xml에서 기본 언어 메시지를 지정합니다.

    <string name="notification_title">Hello world</string>
    <string name="notification_message">This is a message</string>
    
  2. values-language 디렉터리에 번역된 메시지를 지정합니다. 예를 들어 resources/values-fr/strings.xml로 프랑스어 메시지를 지정합니다.

    <string name="notification_title">Bonjour le monde</string>
    <string name="notification_message">C'est un message</string>
    
  3. 서버 페이로드에서 title, message, body 키를 사용하는 대신 현지화된 메시지에 title_loc_keybody_loc_key를 사용하고 표시하려는 메시지의 name 속성으로 설정합니다.

    메시지 페이로드는 다음과 같습니다.

    {
      "data": {
        "title_loc_key": "notification_title",
        "body_loc_key": "notification_message"
      }
    }
    

iOS

  1. Base.lproj/Localizable.strings에서 기본 언어 메시지를 지정합니다.

    "NOTIFICATION_TITLE" = "Hello World";
    "NOTIFICATION_MESSAGE" = "This is a message";
    
  2. language.lproj 디렉터리에 번역된 메시지를 지정합니다. 예를 들어 fr.lproj/Localizable.strings로 프랑스어 메시지를 지정합니다.

    "NOTIFICATION_TITLE" = "Bonjour le monde";
    "NOTIFICATION_MESSAGE" = "C'est un message";
    

    메시지 페이로드는 다음과 같습니다.

    {
      "data": {
        "title_loc_key": "NOTIFICATION_TITLE",
        "body_loc_key": "NOTIFICATION_MESSAGE"
      }
    }
    

메시지 전송 데이터 내보내기 사용 설정

BigQuery로 메시지 데이터를 내보내서 추가 분석을 할 수 있습니다. BigQuery를 사용하면 BigQuery SQL로 데이터를 분석하여 다른 클라우드 제공업체로 내보내거나, 커스텀 ML 모델에 데이터를 사용할 수 있습니다. BigQuery로 내보내기에는 메시지 유형 또는 메시지가 API나 알림 작성기를 통해 전송되는지 여부에 관계없이 메시지의 모든 데이터가 포함됩니다.

내보내기를 사용 설정하려면 먼저 여기에 설명된 단계를 따른 후 다음 안내를 따르세요.

Android

다음 코드를 사용할 수 있습니다.

await FirebaseMessaging.instance.setDeliveryMetricsExportToBigQuery(true);

iOS

iOS의 경우 AppDelegate.m을 다음 콘텐츠로 변경해야 합니다.

#import "AppDelegate.h"
#import "GeneratedPluginRegistrant.h"
#import <Firebase/Firebase.h>

@implementation AppDelegate

- (BOOL)application:(UIApplication *)application
    didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
  [GeneratedPluginRegistrant registerWithRegistry:self];
  // Override point for customization after application launch.
  return [super application:application didFinishLaunchingWithOptions:launchOptions];
}

- (void)application:(UIApplication *)application
    didReceiveRemoteNotification:(NSDictionary *)userInfo
          fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler {
  [[FIRMessaging extensionHelper] exportDeliveryMetricsToBigQueryWithMessageInfo:userInfo];
}

@end

웹의 경우 SDK v9 버전을 사용하려면 서비스 워커를 변경해야 합니다. v9 버전은 번들해야 하므로 서비스 워커가 작동하도록 하려면 esbuild와 같은 번들러를 사용해야 합니다. 이 작업을 실행하는 방법은 예시 앱을 참고하세요.

v9 SDK로 마이그레이션한 후 다음 코드를 사용할 수 있습니다.

import {
  experimentalSetDeliveryMetricsExportedToBigQueryEnabled,
  getMessaging,
} from 'firebase/messaging/sw';
...

const messaging = getMessaging(app);
experimentalSetDeliveryMetricsExportedToBigQueryEnabled(messaging, true);

새 버전의 서비스 워커를 web 폴더로 내보내려면 yarn build를 실행해야 합니다.

iOS의 알림에 이미지 표시

Apple 기기에서 수신된 FCM 알림에 FCM 페이로드의 이미지를 표시하려면 알림 서비스 확장 프로그램을 더 추가하고 이를 사용하도록 앱을 구성해야 합니다.

Firebase 전화 인증을 사용하는 경우 Firebase 인증 포드를 Podfile에 추가해야 합니다.

1단계 - 알림 서비스 확장 프로그램 추가

  1. Xcode에서 파일> 새로 만들기 > 대상...을 클릭합니다.
  2. 모달에 가능한 대상 목록이 표시됩니다. 아래로 스크롤하거나 필터를 사용하여 알림 서비스 확장 프로그램을 선택합니다. 다음을 클릭합니다.
  3. 제품 이름을 추가하고(이 튜토리얼에 따라 'ImageNotification' 사용) 언어를 Objective-C로 설정한 후 완료를 클릭합니다.
  4. 활성화를 클릭하여 스키마를 사용 설정합니다.

2단계 - Podfile에 대상 추가

대상을 Podfile에 추가하여 새 확장 프로그램이 Firebase/Messaging 포드에 액세스할 수 있는지 확인합니다.

  1. 탐색기에서 포드 > Podfile을 엽니다.

  2. 파일 하단으로 스크롤하여 다음을 추가합니다.

    target 'ImageNotification' do
      use_frameworks!
      pod 'Firebase/Auth' # Add this line if you are using FirebaseAuth phone authentication
      pod 'Firebase/Messaging'
    end
    
  3. ios 또는 macos 디렉터리에서 pod install을 사용하여 포드를 설치하거나 업데이트합니다.

3단계 - 확장 프로그램 도우미 사용

이 시점에서 모든 것이 정상적으로 실행 중이어야 합니다. 마지막 단계는 확장 프로그램 도우미를 호출하는 것입니다.

  1. 탐색기에서 ImageNotification 확장 프로그램을 선택합니다.

  2. NotificationService.m 파일을 엽니다.

  3. 아래와 같이 파일 상단에서 NotificationService.h 바로 뒤에 FirebaseMessaging.h를 가져옵니다.

    NotificationService.m의 콘텐츠를 다음으로 바꿉니다.

    #import "NotificationService.h"
    #import "FirebaseMessaging.h"
    #import "FirebaseAuth.h" // Add this line if you are using FirebaseAuth phone authentication
    #import <UIKit/UIKit.h> // Add this line if you are using FirebaseAuth phone authentication
    
    @interface NotificationService ()
    
    @property (nonatomic, strong) void (^contentHandler)(UNNotificationContent *contentToDeliver);
    @property (nonatomic, strong) UNMutableNotificationContent *bestAttemptContent;
    
    @end
    
    @implementation NotificationService
    
    /* Uncomment this if you are using Firebase Auth
    - (BOOL)application:(UIApplication *)app
                openURL:(NSURL *)url
                options:(NSDictionary<UIApplicationOpenURLOptionsKey, id> *)options {
      if ([[FIRAuth auth] canHandleURL:url]) {
        return YES;
      }
      return NO;
    }
    
    - (void)scene:(UIScene *)scene openURLContexts:(NSSet<UIOpenURLContext *> *)URLContexts {
      for (UIOpenURLContext *urlContext in URLContexts) {
        [FIRAuth.auth canHandleURL:urlContext.URL];
      }
    }
    */
    
    - (void)didReceiveNotificationRequest:(UNNotificationRequest *)request withContentHandler:(void (^)(UNNotificationContent * _Nonnull))contentHandler {
        self.contentHandler = contentHandler;
        self.bestAttemptContent = [request.content mutableCopy];
    
        // Modify the notification content here...
        [[FIRMessaging extensionHelper] populateNotificationContent:self.bestAttemptContent withContentHandler:contentHandler];
    }
    
    - (void)serviceExtensionTimeWillExpire {
        // Called just before the extension will be terminated by the system.
        // Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the original push payload will be used.
        self.contentHandler(self.bestAttemptContent);
    }
    
    @end
    

4단계 - 페이로드에 이미지 추가

이제 알림 페이로드에서 이미지를 추가할 수 있습니다. iOS 문서에서 전송 요청을 빌드하는 방법을 확인하세요. 기기에서 허용하는 최대 이미지 크기는 300KB입니다.