Push 알림 — FCM과 flutter_local_notifications
Push 알림 — FCM과 flutter_local_notifications
푸시 알림은 사용자 재방문(retention)을 높이는 핵심 기능입니다. Firebase Cloud Messaging(FCM)으로 서버에서 알림을 보내고, flutter_local_notifications로 로컬 알림을 표시하는 방법을 정리해보겠습니다.
설치
dependencies:
firebase_core: ^3.0.0
firebase_messaging: ^15.0.0
flutter_local_notifications: ^18.0.0
FCM 초기화
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
class NotificationService {
final FirebaseMessaging _fcm = FirebaseMessaging.instance;
final FlutterLocalNotificationsPlugin _localNotifications =
FlutterLocalNotificationsPlugin();
Future<void> initialize() async {
// 1. 권한 요청
await _requestPermission();
// 2. 로컬 알림 초기화
await _initLocalNotifications();
// 3. FCM 토큰 가져오기
final token = await _fcm.getToken();
print('FCM Token: $token');
// 서버에 토큰 전송
// 4. 토큰 갱신 감시
_fcm.onTokenRefresh.listen(_sendTokenToServer);
// 5. 포그라운드 메시지 처리
FirebaseMessaging.onMessage.listen(_handleForegroundMessage);
// 6. 백그라운드에서 알림 클릭 시
FirebaseMessaging.onMessageOpenedApp.listen(_handleMessageOpenedApp);
// 7. 앱이 종료된 상태에서 알림 클릭으로 열린 경우
final initialMessage = await _fcm.getInitialMessage();
if (initialMessage != null) {
_handleMessageOpenedApp(initialMessage);
}
}
Future<void> _requestPermission() async {
final settings = await _fcm.requestPermission(
alert: true,
badge: true,
sound: true,
provisional: false,
);
if (settings.authorizationStatus == AuthorizationStatus.authorized) {
print('알림 권한 허용됨');
} else {
print('알림 권한 거부됨');
}
}
Future<void> _initLocalNotifications() async {
const androidSettings = AndroidInitializationSettings(
'@mipmap/ic_launcher',
);
const iosSettings = DarwinInitializationSettings(
requestAlertPermission: false,
requestBadgePermission: false,
requestSoundPermission: false,
);
await _localNotifications.initialize(
const InitializationSettings(
android: androidSettings,
iOS: iosSettings,
),
onDidReceiveNotificationResponse: (response) {
// 로컬 알림 클릭 시 처리
_handleNotificationTap(response.payload);
},
);
}
void _handleForegroundMessage(RemoteMessage message) {
// 포그라운드에서는 FCM이 자동으로 알림을 표시하지 않음
// 직접 로컬 알림으로 표시
_showLocalNotification(
title: message.notification?.title ?? '',
body: message.notification?.body ?? '',
payload: message.data.toString(),
);
}
void _handleMessageOpenedApp(RemoteMessage message) {
// 알림 클릭으로 앱이 열린 경우
final data = message.data;
if (data.containsKey('screen')) {
// 해당 화면으로 이동
navigatorKey.currentState?.pushNamed(data['screen']!);
}
}
Future<void> _showLocalNotification({
required String title,
required String body,
String? payload,
}) async {
const androidDetails = AndroidNotificationDetails(
'default_channel',
'기본 알림',
channelDescription: '기본 알림 채널',
importance: Importance.high,
priority: Priority.high,
);
const iosDetails = DarwinNotificationDetails(
presentAlert: true,
presentBadge: true,
presentSound: true,
);
await _localNotifications.show(
DateTime.now().millisecondsSinceEpoch ~/ 1000,
title,
body,
const NotificationDetails(
android: androidDetails,
iOS: iosDetails,
),
payload: payload,
);
}
void _sendTokenToServer(String token) {
// API 호출로 서버에 토큰 전송
print('새 토큰: $token');
}
void _handleNotificationTap(String? payload) {
if (payload != null) {
// 페이로드에 따라 화면 이동
print('알림 탭: $payload');
}
}
}
백그라운드 핸들러
// main.dart — 최상위 함수로 선언
@pragma('vm:entry-point')
Future<void> _firebaseMessagingBackgroundHandler(
RemoteMessage message,
) async {
await Firebase.initializeApp();
print('백그라운드 메시지: ${message.messageId}');
}
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
// 백그라운드 핸들러 등록
FirebaseMessaging.onBackgroundMessage(
_firebaseMessagingBackgroundHandler,
);
runApp(const MyApp());
}
알림 채널 (Android)
Android 8.0+에서는 알림 채널이 필수입니다.
// 채널 생성
const channel = AndroidNotificationChannel(
'high_importance',
'중요 알림',
description: '중요한 알림을 위한 채널',
importance: Importance.high,
);
await _localNotifications
.resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin>()
?.createNotificationChannel(channel);
토픽 구독
// 특정 토픽 구독
await FirebaseMessaging.instance.subscribeToTopic('news');
// 구독 해제
await FirebaseMessaging.instance.unsubscribeFromTopic('news');
예약 알림 (로컬)
import 'package:timezone/timezone.dart' as tz;
Future<void> scheduleNotification({
required DateTime scheduledTime,
required String title,
required String body,
}) async {
await _localNotifications.zonedSchedule(
0,
title,
body,
tz.TZDateTime.from(scheduledTime, tz.local),
const NotificationDetails(
android: AndroidNotificationDetails(
'scheduled_channel',
'예약 알림',
),
),
androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle,
matchDateTimeComponents: DateTimeComponents.time, // 매일 반복
);
}
알림 상태별 동작
| 앱 상태 | FCM 알림 표시 | 데이터 메시지 |
|---|---|---|
| 포그라운드 | 자동 표시 안 됨 | onMessage |
| 백그라운드 | 시스템이 표시 | onBackgroundMessage |
| 종료됨 | 시스템이 표시 | getInitialMessage |
면접 포인트: 포그라운드에서는 FCM이 알림을 자동으로 표시하지 않습니다. flutter_local_notifications를 사용해 직접 표시해야 합니다.
정리
- FCM으로 서버 푸시 알림,
flutter_local_notifications로 로컬 알림을 표시합니다 - 포그라운드에서는 FCM이 알림을 자동 표시하지 않으므로 로컬 알림을 직접 표시해야 합니다
- 백그라운드 핸들러는 최상위 함수로 선언해야 합니다
- Android 8.0+에서는 알림 채널 생성이 필수입니다
- FCM 토큰을 서버에 저장하고, 갱신 시에도 업데이트해야 합니다
댓글 로딩 중...