플러터:푸시 알람(FCM)
보이기
- 플러터:개요
- 플러터:실행
- 플러터:개념 잡기
- 권한 사용
- 위젯
- 플러터:DB연결
- 플러터:Firebase(미완)
- 플러터:MySQL(미완)
- 디자인
- 플러터:배포
- 플러터:배포(안드로이드)(미완)
- 플러터:배포(iOS)(미완)
- 플러터:참고자료
- 플러터:위젯
- 플러터:구글 AdMob(미완)
- 플러터:라이브러리
FCM(Firebase Cloud Messaging)은 Firebase에서 제공하는 푸시 알림 전송 서비스로, 안드로이드와 iOS를 동시에 지원.
플러터(Flutter) 환경에서는 사실상 표준적인 푸시 알림 수단.
iOS와 안드로이드에서만 사용 가능하다. 브라우저와 이외 OS에선 웹소캣 등 따로 구성해주어야 함.
- 서버 키를 통해 알림을 보내는 주체가 관리자임을 인증하고,
- 기기마다 주어지는 토큰을 통해 어떤 기기로 알림을 보낼지 정한다.
| 구분 | 설명 | 비고 |
|---|---|---|
| FCM 서버 키(Server Key) |
|
|
| FCM 토큰(Registration Token) |
|
서버에서 계정과 매핑 |
[사용자 로그인]
↓
[FCM 토큰 발급 (앱)]
↓
[서버로 토큰 전송]
↓
[서버 DB: 계정 ↔ FCM 토큰 저장]
↓
[알림 발생]
↓
[FCM 서버 → 기기]
FCM은 알림을 전달할 뿐, 어떤 사용자에게 보낼지는 서버가 결정한다.
| 과정 | 설명 | 비고 |
|---|---|---|
| Firebase 프로젝트 생성 | ||
| Android / iOS 앱 등록 | ||
google-services.json 또는 GoogleService-Info.plist 설정 완료
|
||
| Flutter 설정 |
dependencies:
flutter:
sdk: flutter
firebase_core: ^2.24.2
firebase_messaging: ^14.7.10
|
flutter pub get |
- onMessage, onMessageOpenedApp, getInitialMessage 이들을 이해하는 게 핵심.
import 'package:flutter/material.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
/// 🔹 백그라운드 메시지 핸들러 (top-level 필수)
Future<void> firebaseMessagingBackgroundHandler(
RemoteMessage message) async {
await Firebase.initializeApp();
// UI 접근 불가, 필요하면 로그/저장만
debugPrint('백그라운드 메시지 수신: ${message.messageId}');
}
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
// 백그라운드 핸들러 등록
FirebaseMessaging.onBackgroundMessage(
firebaseMessagingBackgroundHandler,
);
runApp(const MyApp());
}
class MyApp extends StatefulWidget {
const MyApp({super.key});
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
String? fcmToken;
final List<String> messages = [];
@override
void initState() {
super.initState();
_initFCM();
_setupMessageHandlers();
}
/// FCM 초기 설정 + 토큰 발급
Future<void> _initFCM() async {
// iOS 권한 요청 (Android에서는 무시됨)
await FirebaseMessaging.instance.requestPermission();
final token = await FirebaseMessaging.instance.getToken();
setState(() {
fcmToken = token;
});
}
/// 메시지 수신 / 클릭 처리
void _setupMessageHandlers() {
// 1️⃣ 포그라운드 수신
FirebaseMessaging.onMessage.listen((RemoteMessage message) {
final title = message.notification?.title ?? '(no title)';
final body = message.notification?.body ?? '(no body)';
setState(() {
messages.add('[FOREGROUND] $title - $body');
});
});
// 2️⃣ 백그라운드 상태에서 알림 클릭
FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) {
final title = message.notification?.title ?? '(no title)';
setState(() {
messages.add('[CLICK - BG] $title');
});
});
// 3️⃣ 종료 상태에서 알림 클릭
FirebaseMessaging.instance.getInitialMessage().then((message) {
if (message != null) {
final title = message.notification?.title ?? '(no title)';
setState(() {
messages.add('[CLICK - TERMINATED] $title');
});
}
});
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('FCM 단일 파일 예제')),
body: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'FCM Token',
style: TextStyle(fontWeight: FontWeight.bold),
),
SelectableText(
fcmToken ?? '토큰 불러오는 중...',
style: const TextStyle(fontSize: 12),
),
const SizedBox(height: 16),
const Text(
'수신 / 클릭 메시지',
style: TextStyle(fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
Expanded(
child: ListView.builder(
itemCount: messages.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(messages[index]),
);
},
),
),
],
),
),
),
);
}
}
await FirebaseMessaging.instance.requestPermission(
alert: true,
badge: true,
sound: true,
);
FirebaseMessaging.onMessage.listen((RemoteMessage message) {
print('푸시 수신: ${message.notification?.title}');
});
Future<void> firebaseMessagingBackgroundHandler(
RemoteMessage message) async {
await Firebase.initializeApp();
}
FirebaseMessaging.onBackgroundMessage(
firebaseMessagingBackgroundHandler,
);
| 필드명 | 설명 |
|---|---|
| user_id | 사용자 ID |
| fcm_token | 기기 토큰 |
| platform | android / ios |
{
"to": "FCM_TOKEN",
"notification": {
"title": "테스트 알림",
"body": "푸시 알림이 도착했습니다"
}
}
앱 실행 후 FCM 토큰을 발급받는다.
FirebaseMessaging messaging = FirebaseMessaging.instance;
String? token = await messaging.getToken();
로그인 성공 시, 해당 토큰을 서버로 전송한다.
서버는 사용자 계정과 FCM 토큰을 매핑하여 저장한다.
| user_id | fcm_token | platform |
|---|---|---|
| 123 | token_A | android |
| 123 | token_B | ios |
- 하나의 계정은 여러 기기를 가질 수 있다.
- FCM 토큰은 기기 단위로 관리한다.
- 현재 기기의 FCM 토큰만 삭제
- 다른 기기 로그인은 유지
DELETE FROM device_token
WHERE user_id = 123
AND fcm_token = 'token_A';
- 해당 계정에 연결된 모든 FCM 토큰 삭제
DELETE FROM device_token
WHERE user_id = 123;
다음과 같은 경우 FCM 토큰이 변경될 수 있다.
- 앱 재설치
- 기기 변경
- OS 업데이트
- Firebase 정책 변경
Flutter에서는 토큰 변경 이벤트를 수신하여 서버에 반영해야 한다.
FirebaseMessaging.instance.onTokenRefresh.listen((newToken) {
// 서버로 새 토큰 전송
});
- 서버는 앱 삭제 여부를 직접 알 수 없음
- 푸시 전송 시 FCM에서 에러 반환
대표적인 에러:
NotRegisteredInvalidRegistration
이 경우 서버에서 해당 토큰을 삭제해야 한다.
서버는 FCM 서버 키를 사용하여 푸시 요청을 전송한다.
{
"to": "fcm_token",
"notification": {
"title": "새 메시지",
"body": "홍길동: 안녕하세요"
}
}
- FCM 서버 키는 서버에서만 사용
- 클라이언트에 노출 금지
- FCM은 전달자 역할만 수행한다.
- 사용자와 기기의 연결은 서버에서 직접 관리해야 한다.
- 로그인 = 기기 등록
- 로그아웃 = 기기 해제
- 탈퇴 = 모든 기기 해제
- 로그인 시 FCM 토큰 등록
- 로그아웃 시 해당 토큰 삭제
- 탈퇴 시 전체 토큰 삭제
- 토큰 갱신 처리
- FCM 에러 발생 시 토큰 정리