안녕하세요! Firebase Cloud Messaging(FCM)을 사용하여 Node.js 서버와 Flutter 클라이언트 앱 간에 Push 알림 기능을 구현하는 방법에 대해 자세히 알아보겠습니다.
Firebase Cloud Messaging(FCM)은 Firebase에서 제공하는 교차 플랫폼 메시징 솔루션으로, 서버에서 클라이언트 앱으로 메시지를 안정적으로 전송할 수 있게 해줍니다. FCM을 사용하면 사용자에게 알림을 보내 참여를 유도하거나, 데이터 동기화를 지시하는 등의 다양한 용도로 활용할 수 있습니다.
즉, FCM은 앱이 실행 중이지 않을 때도 사용자에게 알림을 보내거나 데이터를 전달할 수 있게 해주는 서비스로,
집 주소로 편지가 오는 것처럼, FCM은 각 앱(정확히는 앱이 설치된 각 기기)에 고유한 "주소"(토큰)를 부여하고, 이 주소로 메시지를 보내는 역할을 합니다.
주요 장점:
FCM을 사용하기 위해서는 먼저 Firebase 프로젝트를 설정해야 합니다.
프로젝트가 생성되면 Android 및 iOS 앱을 Firebase 프로젝트에 등록해야 합니다.
Android 앱 등록:
android/app/build.gradle
파일에 있는 applicationId
를 입력합니다. (예: com.example.your_app
)keytool -list -v -keystore ~/.android/debug.keystore -alias androiddebugkey -storepass android -keypass android
keytool -list -v -keystore "%USERPROFILE%\\.android\\debug.keystore" -alias androiddebugkey -storepass android -keypass android
iOS 앱 등록:
Bundle Identifier
를 입력합니다. (예: com.example.yourApp
)앱 등록 후 Firebase 설정 파일을 다운로드하여 각 플랫폼별 프로젝트에 추가해야 합니다.
Android:
google-services.json
파일을 다운로드합니다.
다운로드한 파일을 Flutter 프로젝트의 android/app/
디렉터리로 이동합니다.
android/build.gradle
파일에 다음 classpath를 추가합니다:
buildscript {
// ...
dependencies {
// ...
classpath 'com.google.gms:google-services:4.4.1' // 최신 버전 확인
}
}
android/app/build.gradle
파일 하단에 다음 플러그인을 추가합니다:
apply plugin: 'com.google.gms.google-services'
iOS:
GoogleService-Info.plist
파일을 다운로드합니다.ios/Runner/
디렉터리에 다운로드한 파일을 추가합니다. (파일을 드래그 앤 드롭할 때 "Copy items if needed" 옵션을 선택해야 합니다.)Node.js 서버는 클라이언트 앱으로 FCM 메시지를 발송하는 역할을 합니다.
먼저, Node.js 프로젝트에 firebase-admin
패키지를 설치합니다.
npm install firebase-admin
# 또는
yarn add firebase-admin
Firebase Admin SDK를 사용하면 서버에서 Firebase 서비스와 상호 작용할 수 있습니다. 서비스 계정 키를 사용하여 SDK를 초기화합니다.
config
폴더)
주의: 이 키 파일은 민감한 정보를 포함하므로, 버전 관리 시스템(Git 등)에 직접 커밋하지 않도록 .gitignore
에 추가하세요.다음은 Admin SDK 초기화 예시입니다 (index.js
또는 유사한 파일):
import * as admin from "firebase-admin";
// Firebase가 이미 초기화되었는지 확인
export function getFirebaseApp() {
try {
return admin.app(); // 이미 존재하는 앱 반환
} catch (error) {
// 앱이 존재하지 않는 경우 초기화
const serviceAccount = require("../../your-service-account-key.json");
return admin.initializeApp({
credential: admin.credential.cert(serviceAccount as admin.ServiceAccount),
});
}
}
// 앱과 서비스 내보내기
export const firebaseApp = getFirebaseApp();
export const messaging = admin.messaging();
클라이언트로부터 특정 요청을 받으면 해당 클라이언트의 FCM 토큰으로 메시지를 보내는 API 엔드포인트를 구현합니다.
// ... (Firebase Admin SDK 초기화 코드 이후)
/**
* 특정 FCM 토큰으로 메시지를 보냅니다.
* @param {string} fcmToken - 메시지를 받을 클라이언트의 FCM 토큰
* @param {string} title - 알림 제목
* @param {string} body - 알림 본문
* @param {object} data - 알림과 함께 전송할 추가 데이터 (선택 사항)
*/
async function sendMessageToToken(fcmToken, title, body, data = {}) {
const message = {
notification: {
title: title,
body: body,
},
token: fcmToken, // 특정 디바이스 토큰
data: data, // 선택적 데이터 페이로드
android: { // Android 특정 옵션
priority: 'high',
notification: {
sound: 'default',
channel_id: 'channel_id' // Flutter에서 설정한 채널 ID와 일치
}
},
apns: { // iOS 특정 옵션
payload: {
aps: {
sound: 'default',
badge: 1, // 앱 아이콘 배지 숫자
}
}
}
};
try {
const response = await admin.messaging().send(message);
console.log('Successfully sent message:', response);
return { success: true, messageId: response };
} catch (error) {
console.error('Error sending message:', error);
return { success: false, error: error.message };
}
}
// 예시: Express.js를 사용한 API 엔드포인트
// app.post('/send-notification', async (req, res) => {
// const { token, title, body, data } = req.body;
//
// if (!token || !title || !body) {
// return res.status(400).send({ error: 'Missing required fields: token, title, body' });
// }
//
// const result = await sendMessageToToken(token, title, body, data);
// if (result.success) {
// res.status(200).send({ message: 'Notification sent successfully', messageId: result.messageId });
// } else {
// res.status(500).send({ error: 'Failed to send notification', details: result.error });
// }
// });
// // 서버 시작
// const PORT = process.env.PORT || 3000;
// app.listen(PORT, () => {
// console.log(`Server is running on port ${PORT}`);
// });
참고
위 코드에서 channel_id는 Flutter 클라이언트에서 로컬 알림을 위해 설정한 Android 알림 채널 ID와 일치해야 합니다.
Flutter 앱에서 FCM 메시지를 수신하고 처리하는 방법을 설정합니다.
pubspec.yaml
파일에 다음 패키지들을 추가합니다:
dependencies:
flutter:
sdk: flutter
firebase_core: ^2.27.2 # 최신 버전 확인
firebase_messaging: ^14.8.0 # 최신 버전 확인
flutter_local_notifications: ^17.0.0 # 최신 버전 확인
flutter_dotenv: ^5.1.0 # .env 파일 사용 시
http: ^1.2.1 # 서버로 토큰 전송 시
패키지를 추가한 후 터미널에서 flutter pub get
을 실행합니다.
main.dart
앱이 시작될 때 Firebase를 초기화하고 백그라운드 메시지 핸들러를 설정합니다.
// lib/main.dart
import 'package:flutter/material.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart'; // .env 사용 시
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'services/fcm_service.dart'; // FCM 서비스 클래스
// ... 기타 import
// 백그라운드/종료 상태에서 FCM 메시지 수신 시 호출될 핸들러
// 이 핸들러는 애플리케이션 컨텍스트 외부에 있어야 합니다 (최상위 레벨 또는 static 함수).
('vm:entry-point')
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
// 백그라운드에서 Firebase가 여러 번 초기화되는 것을 방지
if (Firebase.apps.isEmpty) {
await Firebase.initializeApp(
// options: FirebaseOptions(...) // main에서 사용하는 옵션과 동일하게 설정 가능
// 또는 DefaultFirebaseOptions.currentPlatform 사용 (flutterfire configure로 생성된 경우)
);
}
print("백그라운드 메시지 처리: ${message.messageId}");
// 여기서 로컬 알림을 직접 표시하거나, 메시지 데이터를 기반으로 다른 백그라운드 작업을 수행할 수 있습니다.
// 예: FCMService().showLocalNotification(...) - 단, FCMService 인스턴스 생성 방식에 주의
}
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await dotenv.load(fileName: ".env"); // .env 사용 시
// Firebase 초기화
// flutterfire configure를 통해 생성된 firebase_options.dart 사용 권장
// await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
// 또는 수동 설정
if (Firebase.apps.isEmpty) {
await Firebase.initializeApp(
options: FirebaseOptions(
apiKey: dotenv.env['FIREBASE_API_KEY'] ?? '',
appId: dotenv.env['FIREBASE_APP_ID'] ?? '',
messagingSenderId: dotenv.env['FIREBASE_MESSAGING_SENDER_ID'] ?? '',
projectId: dotenv.env['FIREBASE_PROJECT_ID'] ?? '',
),
);
}
// 백그라운드 메시지 핸들러 등록
FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);
// FCM 서비스 초기화 (포그라운드 메시지 처리 및 로컬 알림 설정 포함)
await FCMService().init();
runApp(const ProviderScope(child: MyApp())); // Riverpod 사용 시
// runApp(const MyApp()); // 일반적인 경우
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return MaterialApp(
// ... 앱 설정
home: SplashScreen(), // 예시 시작 화면
);
}
}
Android 특정 설정 (android/app/src/main/AndroidManifest.xml
):
flutter_local_notifications
를 사용할 때, Android 12 (API 31) 이상에서는 예약된 알림을 사용하기 위해 SCHEDULE_EXACT_ALARM
권한이 필요할 수 있습니다.
<manifest xmlns:android="<http://schemas.android.com/apk/res/android>"
package="com.example.your_app">
<uses-permission android:name="android.permission.INTERNET"/>
<!-- Android 12 (API 31) 이상에서 정확한 알람 권한 (필요한 경우) -->
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
<!-- Android 13 (API 33) 이상에서 알림 권한 -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<application
android:label="your_app_name"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity
// ...
>
<!-- FCM 클릭 시 열릴 액티비티 설정 -->
<intent-filter>
<action android:name="FLUTTER_NOTIFICATION_CLICK" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
<!-- ... -->
<!-- FCM 메시지 수신을 위한 서비스 (firebase-messaging 플러그인이 자동으로 추가할 수 있음) -->
<!-- <service
android:name=".MyFirebaseMessagingService"
android:exported="false">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service> -->
<meta-data
android:name="com.google.firebase.messaging.default_notification_channel_id"
android:value="00000_channel_id" /> <!-- 서버에서 보낼 때 channel_id와 일치시키거나, 여기서 기본 채널을 지정합니다. -->
</application>
</manifest>
Android 13 (API 33)부터는 알림을 표시하기 위해 런타임 권한 POST_NOTIFICATIONS
이 필요합니다. FCMService().requestPermission()
에서 이 권한을 요청합니다.
iOS 특정 설정:
Push 알림을 사용하려면 Xcode에서 프로젝트의 "Signing & Capabilities" 탭으로 이동하여 "+ Capability"를 클릭하고 "Push Notifications"와 "Background Modes" (Background fetch, Remote notifications 체크)를 추가해야 합니다.
lib/services/fcm_service.dart
)FCM 관련 로직을 캡슐화하는 서비스 클래스를 만듭니다. 현재 프로젝트의 FCMService
클래스와 유사한 구조를 가집니다.
// lib/services/fcm_service.dart
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart'; // .env 사용 시
import 'package:http/http.dart' as http; // 서버로 토큰 전송 시
import 'dart:convert'; // 서버로 토큰 전송 시
class FCMService {
final FirebaseMessaging _messaging = FirebaseMessaging.instance;
final FlutterLocalNotificationsPlugin _flutterLocalNotifications =
FlutterLocalNotificationsPlugin();
static final FCMService _instance = FCMService._internal();
bool _isInitialized = false;
factory FCMService() {
return _instance;
}
FCMService._internal();
Future<void> init() async {
if (_isInitialized) return;
// 1. 로컬 알림 플러그인 초기화
// Android 설정
const AndroidInitializationSettings initializationSettingsAndroid =
AndroidInitializationSettings('@mipmap/ic_launcher'); // 앱 아이콘 사용
// iOS 설정
const DarwinInitializationSettings initializationSettingsIOS =
DarwinInitializationSettings(
requestAlertPermission: false, // 권한 요청은 별도로 처리
requestBadgePermission: false,
requestSoundPermission: false,
);
const InitializationSettings initializationSettings =
InitializationSettings(
android: initializationSettingsAndroid,
iOS: initializationSettingsIOS,
);
await _flutterLocalNotifications.initialize(
initializationSettings,
onDidReceiveNotificationResponse: (NotificationResponse response) async {
// 알림 클릭 시 로직 (예: 특정 화면으로 이동)
print("로컬 알림 클릭됨. Payload: ${response.payload}");
if (response.payload != null) {
// final Map<String, dynamic> data = jsonDecode(response.payload!);
// Navigator.of(GlobalContext.navigatorKey.currentContext!).pushNamed('/detail', arguments: data);
}
},
);
// 2. Android 알림 채널 생성 (로컬 알림용)
await _createNotificationChannel();
// 3. 포그라운드 메시지 리스너 설정
FirebaseMessaging.onMessage.listen((RemoteMessage message) {
print('포그라운드 메시지 수신: ${message.messageId}');
RemoteNotification? notification = message.notification;
AndroidNotification? android = message.notification?.android;
AppleNotification? apple = message.notification?.apple;
// notification 페이로드가 있고, Android의 경우 channelId가 설정되어 있으면 Firebase가 자동으로 알림을 표시할 수 있음
// 하지만, 커스텀 로컬 알림을 사용하거나 iOS에서 포그라운드 알림을 항상 표시하려면 직접 처리
if (notification != null) {
// iOS의 경우 포그라운드에서 기본적으로 알림이 표시되지 않으므로 로컬 알림으로 표시
// Android도 커스텀하게 처리하고 싶다면 여기서 로컬 알림 표시
_showLocalNotification(
title: notification.title ?? '알림',
body: notification.body ?? '',
payload: jsonEncode(message.data), // message.data를 payload로 전달
);
}
});
// 4. 앱이 종료된 상태에서 알림을 클릭하여 열렸을 때 메시지 가져오기
FirebaseMessaging.instance.getInitialMessage().then((RemoteMessage? message) {
if (message != null) {
print('앱 종료 상태에서 알림 클릭으로 실행됨: ${message.messageId}');
// 여기서도 message.data를 사용하여 특정 화면으로 네비게이션 등의 작업 수행
// 예: Navigator.of(GlobalContext.navigatorKey.currentContext!).pushNamed('/detail', arguments: message.data);
}
});
// 5. 앱이 백그라운드 상태에서 알림을 클릭하여 열렸을 때 메시지 처리 리스너
FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) {
print('백그라운드에서 알림 클릭으로 앱 열림: ${message.messageId}');
// message.data를 사용하여 특정 화면으로 네비게이션 등의 작업 수행
// 예: Navigator.of(GlobalContext.navigatorKey.currentContext!).pushNamed('/detail', arguments: message.data);
});
// 6. iOS 포그라운드 알림 표시 설정 (선택 사항, 기본적으로는 로컬 알림으로 처리)
// 이 설정을 하면 iOS 포그라운드에서도 시스템 알림이 표시됨.
// flutter_local_notifications로 직접 표시하는 경우 중복될 수 있으므로 주의.
await _messaging.setForegroundNotificationPresentationOptions(
alert: true, // Required to display a heads up notification
badge: true,
sound: true,
);
_isInitialized = true;
print('FCMService initialized.');
}
// Android 알림 채널 생성
Future<void> _createNotificationChannel() async {
const AndroidNotificationChannel channel = AndroidNotificationChannel(
'000000_channel_id', // ID (서버에서 보낼 때 이 ID를 사용하거나, 기본 채널로 지정)
'000 알림', // Title
description: '0000 중요 알림 채널입니다.', // Description
importance: Importance.max, // 중요도
playSound: true,
sound: RawResourceAndroidNotificationSound('notification_sound'), // res/raw/notification_sound.mp3 (확장자 없이)
);
await _flutterLocalNotifications
.resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin>()
?.createNotificationChannel(channel);
print('Android Notification Channel "0000_channel_id" created.');
}
/// 알림 권한 요청 (iOS & Android 13+)
Future<bool> requestPermission() async {
NotificationSettings settings = await _messaging.requestPermission(
alert: true,
announcement: false,
badge: true,
carPlay: false,
criticalAlert: false,
provisional: false, // provisional은 사용자가 명시적으로 허용하지 않아도 조용한 알림을 받을 수 있게 함
sound: true,
);
if (settings.authorizationStatus == AuthorizationStatus.authorized) {
print('알림 권한 허용됨');
return true;
} else if (settings.authorizationStatus == AuthorizationStatus.provisional) {
print('임시 알림 권한 허용됨 (iOS)');
return true;
} else {
print('알림 권한 거부됨');
return false;
}
}
/// FCM 토큰 가져오기
Future<String?> getToken() async {
try {
String? token = await _messaging.getToken();
print("FCM Token: $token");
// APNs 토큰 (iOS 전용, 디버깅 용도)
// if (defaultTargetPlatform == TargetPlatform.iOS) {
// String? apnsToken = await _messaging.getAPNSToken();
// print("APNS Token: $apnsToken");
// }
return token;
} catch (e) {
print("FCM 토큰 가져오기 실패: $e");
return null;
}
}
/// 서버에 토큰 등록 (예시)
Future<bool> registerTokenOnServer(String token, String userId) async {
// 실제 서버 URL로 교체해야 합니다.
final String serverUrl = dotenv.env['TOKEN_SERVER_URL'] ?? '<http://your-server.com/register-token>';
try {
final response = await http.post(
Uri.parse(serverUrl),
headers: {"Content-Type": "application/json"},
body: jsonEncode({"fcm_token": token, "user_id": userId}),
).timeout(const Duration(seconds: 10));
if (response.statusCode == 200 || response.statusCode == 201) {
print("FCM 토큰 서버에 등록 성공");
return true;
} else {
print("FCM 토큰 서버 등록 실패: ${response.statusCode}, ${response.body}");
return false;
}
} catch (e) {
print("FCM 토큰 서버 등록 중 오류: $e");
return false;
}
}
/// 로컬 알림 표시
Future<void> _showLocalNotification({
required String title,
required String body,
String? payload, // JSON 문자열 형태의 데이터 전달
}) async {
// 고유한 알림 ID 생성 (매번 다른 알림으로 표시하기 위함)
final int notificationId = DateTime.now().millisecondsSinceEpoch.remainder(100000);
// Android 알림 상세 설정
const AndroidNotificationDetails androidPlatformChannelSpecifics =
AndroidNotificationDetails(
'000_channel_id', // 채널 ID (위에서 생성한 채널과 일치)
'00000 알림', // 채널 이름
channelDescription: '0000 등 중요 알림 채널입니다.',
importance: Importance.max,
priority: Priority.high,
ticker: 'ticker', // 상태 표시줄에 잠시 표시되는 텍스트
playSound: true,
sound: RawResourceAndroidNotificationSound('notification_sound'), // res/raw/notification_sound.mp3 (확장자 없이)
// icon: '@mipmap/ic_launcher', // 알림 아이콘 (설정하지 않으면 앱 아이콘 사용)
);
// iOS 알림 상세 설정
const DarwinNotificationDetails iOSPlatformChannelSpecifics =
DarwinNotificationDetails(
presentAlert: true,
presentBadge: true,
presentSound: true,
sound: 'notification_sound.aiff', // ios/Runner/Assets 폴더에 추가 (확장자 포함)
// badgeNumber: 1, // 앱 아이콘 배지 숫자
);
const NotificationDetails platformChannelSpecifics = NotificationDetails(
android: androidPlatformChannelSpecifics,
iOS: iOSPlatformChannelSpecifics,
);
await _flutterLocalNotifications.show(
notificationId,
title,
body,
platformChannelSpecifics,
payload: payload,
);
print('로컬 알림 표시됨: $title - $body');
}
// 외부에서 로컬 알림을 쉽게 호출할 수 있도록 public 메소드 제공 (선택 사항)
Future<void> showCustomLocalNotification({
required String title,
required String body,
String? payload,
}) async {
await _showLocalNotification(title: title, body: body, payload: payload);
}
// 주제 구독/구독 취소 (선택 사항)
Future<void> subscribeToTopic(String topic) async {
await _messaging.subscribeToTopic(topic);
print('Subscribed to topic: $topic');
}
Future<void> unsubscribeFromTopic(String topic) async {
await _messaging.unsubscribeFromTopic(topic);
print('Unsubscribed from topic: $topic');
}
}
// 알림 클릭 시 Context 접근을 위한 GlobalKey (신중하게 사용해야 함)
// class GlobalContext {
// static final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
// }
주의사항 및 추가 설정:
@mipmap/ic_launcher
는 기본 앱 아이콘을 사용합니다. 커스텀 아이콘을 사용하려면 android/app/src/main/res/drawable
(또는 mipmap 폴더)에 아이콘 파일을 추가하고 AndroidInitializationSettings
및 AndroidNotificationDetails
의 icon
파라미터에 파일 이름을 지정합니다 (확장자 제외).res/raw
폴더에 사운드 파일(예: notification_sound.mp3
)을 추가하고 RawResourceAndroidNotificationSound('notification_sound')
로 지정합니다.Runner/Assets
(또는 유사한) 폴더에 사운드 파일(예: notification_sound.aiff
또는 .caf
)을 추가하고 DarwinNotificationDetails
의 sound
파라미터에 파일 이름을 지정합니다. (확장자 포함)GlobalContext.navigatorKey
: 알림 클릭 시 특정 화면으로 이동하기 위해 NavigatorState
의 BuildContext
가 필요할 수 있습니다. MaterialApp
에 navigatorKey
를 설정하고 이를 static 변수로 관리하여 서비스 클래스 등 어디서든 접근할 수 있게 할 수 있지만, 전역 상태 관리는 신중하게 사용해야 합니다. Riverpod과 같은 상태 관리 솔루션과 결합하여 더 깔끔하게 처리할 수 있습니다.FCMService().getToken()
을 호출하여 FCM 토큰을 가져오고, FCMService().registerTokenOnServer(token, userId)
를 통해 백엔드 서버에 해당 사용자의 토큰을 등록합니다. (현재 프로젝트의 login_screen.dart
참고)// lib/screens/login_screen.dart (일부)
// ...
Future<void> _login() async {
if (_formKey.currentState!.validate()) {
// ... 로그인 로직 ...
if (loginSuccess) {
final fcmToken = await FCMService().getToken();
if (fcmToken != null && mounted) {
// 실제 사용자 ID 또는 식별자 사용
await FCMService().registerTokenOnServer(fcmToken, "user_id_from_login");
}
// 홈 화면으로 이동
Navigator.pushReplacementNamed(context, '/home');
}
// ...
}
}
// ...
FCM 메시지는 앱의 상태(포그라운드, 백그라운드, 종료)에 따라 다르게 처리됩니다.
FirebaseMessaging.onMessage.listen
콜백이 호출됩니다.flutter_local_notifications
를 사용하여 직접 로컬 알림을 표시하는 것이 일반적입니다.message.notification
(알림 페이로드)과 message.data
(데이터 페이로드)를 모두 포함할 수 있습니다.notification
페이로드가 포함되어 있으면, 시스템이 자동으로 시스템 트레이에 알림을 표시합니다. 사용자가 이 알림을 클릭하면 앱이 포그라운드로 전환됩니다.FirebaseMessaging.onMessageOpenedApp.listen
콜백이 알림 클릭 시 호출됩니다._firebaseMessagingBackgroundHandler
는 notification
페이로드만 있는 메시지(데이터 메시지 없이)에 대해서는 일반적으로 호출되지 않고, 데이터 메시지 또는 알림+데이터 메시지에 대해 호출될 수 있습니다 (플랫폼 및 메시지 구성에 따라 다름). 주로 데이터 전용 메시지를 백그라운드에서 처리할 때 유용합니다.notification
페이로드가 포함된 메시지는 시스템 트레이에 알림으로 표시됩니다. 사용자가 알림을 클릭하면 앱이 실행됩니다.FirebaseMessaging.instance.getInitialMessage()
를 통해 앱이 알림 클릭으로 인해 시작되었는지, 그렇다면 어떤 메시지인지 확인할 수 있습니다. 이 코드는 main
함수나 스플래시 화면 등 앱 초기 실행 시점에 호출하는 것이 좋습니다._firebaseMessagingBackgroundHandler
는 앱이 종료된 상태에서 데이터 메시지나 알림+데이터 메시지를 수신했을 때 호출될 수 있습니다.메시지 페이로드 종류:
notification
키를 포함합니다. (예: {"notification": {"title": "Hello", "body": "World"}, "to": "DEVICE_TOKEN"}
)onMessage
로 전달되지만, 자동 표시는 되지 않을 수 있습니다.data
키만 포함합니다. (예: {"data": {"key1": "value1", "key2": "value2"}, "to": "DEVICE_TOKEN"}
)onMessage
(포그라운드) 또는 _firebaseMessagingBackgroundHandler
(백그라운드/종료)로 전달됩니다.notification
과 data
키를 모두 포함합니다.notification
부분은 시스템 트레이에 표시되고, data
부분은 사용자가 알림을 클릭하여 앱을 열 때 onMessageOpenedApp
또는 getInitialMessage
를 통해 전달됩니다.onMessage
로 notification
과 data
모두 전달됩니다.일반적으로 사용자에게 즉각적인 알림을 주려면 notification
페이로드를 사용하고, 앱 내부 로직 처리를 위한 숨겨진 데이터를 보내려면 data
페이로드를 사용합니다. 두 가지를 함께 사용하여 유연성을 높일 수도 있습니다.
FCMService().getToken()
으로 얻은 FCM 등록 토큰을 추가하고 "테스트"를 클릭합니다.print
문을 통해 포그라운드, 백그라운드 메시지 수신 및 토큰 관련 로그를 확인합니다.google-services.json
파일이 올바르게 추가되었는지 확인합니다.build.gradle
파일 설정이 올바른지 확인합니다.channel_id
가 일치하는지 확인합니다. (FCM이 자동으로 채널을 만들기도 하지만, 커스텀 사운드 등을 위해서는 직접 생성 권장)POST_NOTIFICATIONS
권한이 허용되었는지 확인합니다.GoogleService-Info.plist
파일이 올바르게 추가되었는지 확인합니다.AppDelegate.swift
(또는 Objective-C)에 푸시 알림 관련 코드가 중복되거나 충돌하지 않는지 확인합니다 (Flutter 플러그인이 대부분 처리해 줌).onMessage
또는 백그라운드 핸들러로 전달되지 않음: 메시지 페이로드 형식이 올바른지, 앱 상태에 맞는 핸들러를 사용하고 있는지 확인합니다.이 링크를 통해 구매하시면 제가 수익을 받을 수 있어요. 🤗