받은 메시지는 기기 상태에 따라 다르게 처리됩니다. 이러한 시나리오와 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에는 별도의 격리가 필요 없음). 이를 통해 애플리케이션이 실행되지 않고 있더라도 메시지를 처리할 수 있습니다.
백그라운드 메시지 핸들러와 관련하여 유의해야 할 몇 가지 사항이 있습니다.
- 익명 함수가 아니어야 합니다.
- 최상위 수준 함수여야 합니다(예: 초기화가 필요한 클래스 메서드가 아님).
- 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
resources/values/strings.xml
에서 기본 언어 메시지를 지정합니다.<string name="notification_title">Hello world</string> <string name="notification_message">This is a message</string>
values-language
디렉터리에 번역된 메시지를 지정합니다. 예를 들어resources/values-fr/strings.xml
로 프랑스어 메시지를 지정합니다.<string name="notification_title">Bonjour le monde</string> <string name="notification_message">C'est un message</string>
서버 페이로드에서
title
,message
,body
키를 사용하는 대신 현지화된 메시지에title_loc_key
및body_loc_key
를 사용하고 표시하려는 메시지의name
속성으로 설정합니다.메시지 페이로드는 다음과 같습니다.
{ "data": { "title_loc_key": "notification_title", "body_loc_key": "notification_message" } }
iOS
Base.lproj/Localizable.strings
에서 기본 언어 메시지를 지정합니다."NOTIFICATION_TITLE" = "Hello World"; "NOTIFICATION_MESSAGE" = "This is a message";
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단계 - 알림 서비스 확장 프로그램 추가
- Xcode에서 파일> 새로 만들기 > 대상...을 클릭합니다.
- 모달에 가능한 대상 목록이 표시됩니다. 아래로 스크롤하거나 필터를 사용하여 알림 서비스 확장 프로그램을 선택합니다. 다음을 클릭합니다.
- 제품 이름을 추가하고(이 튜토리얼에 따라 'ImageNotification' 사용) 언어를 Objective-C로 설정한 후 완료를 클릭합니다.
- 활성화를 클릭하여 스키마를 사용 설정합니다.
2단계 - Podfile에 대상 추가
대상을 Podfile에 추가하여 새 확장 프로그램이 Firebase/Messaging
포드에 액세스할 수 있는지 확인합니다.
탐색기에서 포드 > Podfile을 엽니다.
파일 하단으로 스크롤하여 다음을 추가합니다.
target 'ImageNotification' do use_frameworks! pod 'Firebase/Auth' # Add this line if you are using FirebaseAuth phone authentication pod 'Firebase/Messaging' end
ios
또는macos
디렉터리에서pod install
을 사용하여 포드를 설치하거나 업데이트합니다.
3단계 - 확장 프로그램 도우미 사용
이 시점에서 모든 것이 정상적으로 실행 중이어야 합니다. 마지막 단계는 확장 프로그램 도우미를 호출하는 것입니다.
탐색기에서 ImageNotification 확장 프로그램을 선택합니다.
NotificationService.m
파일을 엽니다.아래와 같이 파일 상단에서
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입니다.