푸시 알림이 안 와요 (FCM과 APNs의 함정)
1. "제 폰에는 안 오는데요?"
개발자에게 가장 스트레스 받는 순간 중 하나는 "푸시 알림이 안 온다"는 제보를 받을 때입니다. 로그에는 성공(Success)이라고 뜨는데, 사용자의 폰은 조용합니다. 특히 iOS가 말썽입니다. 안드로이드는 대충 설정해도 잘 오는데, 애플은 인증서 하나만 틀려도 묵묵부답입니다.
2. 원리 이해: Foreground vs Background/Terminated
초보자가 가장 많이 하는 착각은 "푸시는 그냥 오는 거 아닌가?"입니다. 앱의 상태에 따라 처리 방식이 완전히 다릅니다.
- Foreground (앱 켜짐): 알림이 상단 배너로 뜨지 않습니다(기본값). 앱 내부에서
onMessage콜백을 받아서 직접 UI(Dialog, Snackbar)를 띄워줘야 합니다.flutter_local_notifications패키지와 연동이 필요합니다. - Background (홈 화면): 시스템 트레이에 알림이 뜹니다. 클릭하면 앱이 켜집니다.
- Terminated (앱 꺼짐): 가장 어렵습니다. 시스템이 앱을 깨워서 알림을 표시해야 하는데, 설정이 잘못되면 아예 무시됩니다.
3. 문제 1 - iOS APNs 인증서 (p8 vs p12)
iOS 푸시가 안 온다면 99%는 인증서 문제입니다.
예전에는 매년 갱신해야 하는 .p12 인증서를 썼지만, 지금은 APNs Key (.p8) 파일을 쓰는 게 국룰입니다.
체크리스트:
- Apple Developer Console: 'Keys' 메뉴로 가서 APNs Key를 생성했나요?
- Firebase Console: 그
.p8파일을 Firebase Project Settings -> Cloud Messaging -> APNs Authentication Key에 업로드했나요? Team ID와 Key ID가 정확한가요? - Xcode Capabilities:
Signing & Capabilities탭에서Push Notifications와Background Modes (Remote notifications)를 켰나요? (이거 안 켜면 절대 안 옴) - Provisioning Profile: Push 기능이 포함된 프로파일인지 확인하세요.
4. 문제 2 - 안드로이드 Notification Channel (필수)
안드로이드 8.0 (Oreo) 이상부터는 Notification Channel을 설정하지 않으면 알림이 아예 안 뜹니다.
// main.dart에서 앱 시작 전 채널 생성
const AndroidNotificationChannel channel = AndroidNotificationChannel(
'high_importance_channel', // id
'High Importance Notifications', // title
description: 'This channel is used for important notifications.',
importance: Importance.max,
);
await flutterLocalNotificationsPlugin
.resolvePlatformSpecificImplementation<AndroidFlutterLocalNotificationsPlugin>()
?.createNotificationChannel(channel);
그리고 서버에서 보낼 때도 android_channel_id를 맞춰줘야 합니다.
5. 문제 3 - 안드로이드 배터리 최적화 (Doze Mode)
삼성 갤럭시 등은 배터리를 아끼려고 백그라운드 앱을 강제로 재워버립니다. 앱이 꺼져 있을 때 푸시가 안 온다면, 메시지 우선순위(Priority)를 확인해야 합니다.
{
"message": {
"token": "device_token",
"notification": { "title": "제목", "body": "내용" },
"android": {
"priority": "high", // 👈 필살기 1: 잠든 폰 깨우기
"notification": {
"channel_id": "high_importance_channel" // 👈 필살기 2: 채널 ID 일치
}
}
}
}
6. 백그라운드 핸들러 (@pragma) 자세히 살펴보기
앱이 꺼져 있을 때 데이터 처리(예: 읽지 않은 메시지 수 갱신, 로컬 DB 저장)를 하려면 onBackgroundMessage를 써야 합니다.
이 함수는 반드시 최상위 레벨(Top-level)에 선언되어야 하고, @pragma('vm:entry-point') 어노테이션을 붙여야 안드로이드가 찾을 수 있습니다.
@pragma('vm:entry-point')
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
await Firebase.initializeApp(); // 여기서도 초기화 필요
print("백그라운드 메시지 처리: ${message.messageId}");
}
void main() {
FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);
runApp(MyApp());
}
이걸 안 지키면 "클래스를 찾을 수 없음" 에러가 나면서 앱이 죽거나 무시됩니다.
7. iOS Notification Service Extension (이미지) 깊이 들여다보기
iOS 알림에 이미지를 넣으려면 Notification Service Extension이라는 별도의 Target을 Xcode에서 추가해야 합니다.
기본 푸시만으로는 이미지가 안 뜹니다.
- Xcode -> File -> New -> Target -> Select
Notification Service Extension. NotificationService.swift파일이 생깁니다.- 거기서 이미지 URL을 다운로드해서
bestAttemptContent.attachments에 추가하는 코드를 작성해야 합니다. - 주의: Extension의 Bundle ID는 메인 앱 Bundle ID의 하위여야 합니다 (예:
com.myapp->com.myapp.notification).
8. 테스트 팁: PushTry.com
내 서버가 문제인지 앱이 문제인지 헷갈릴 때가 있습니다. PushTry.com 같은 사이트에서 FCM Server Key와 Device Token만 넣고 테스트해보세요. 여기서 잘 오면 서버 문제, 안 오면 앱 설정 문제입니다.
9. 요약
- iOS는 Xcode 설정과 인증서(.p8) 확인.
- 안드로이드는 Notification Channel ID 일치 확인.
- 백그라운드 핸들러는 Top-level에
vm:entry-point필수. - 이미지를 보내려면 iOS 익스텐션 구현 필수.
푸시는 보내는 건 쉬워도 "확실하게" 받는 건 예술의 영역입니다.