
푸시 알림이 안 와요 (FCM과 APNs의 함정)
안드로이드는 오는데 iOS는 조용합니다. 혹은 앱이 켜져 있을 때만 옵니다. Background/Terminated 상태 처리, APNs 인증서, 그리고 Notification Channel 설정까지 완벽하게 해결합니다.

안드로이드는 오는데 iOS는 조용합니다. 혹은 앱이 켜져 있을 때만 옵니다. Background/Terminated 상태 처리, APNs 인증서, 그리고 Notification Channel 설정까지 완벽하게 해결합니다.
로그인 화면을 만들었는데 키보드가 올라오니 노란 줄무늬 에러가 뜹니다. resizeToAvoidBottomInset부터 스크롤 뷰, 그리고 채팅 앱을 위한 reverse 팁까지, 키보드 대응의 모든 것을 정리해봤습니다.

안드로이드는 Xcode보다 낫다고요? Gradle 지옥에 빠져보면 그 말이 쏙 들어갈 겁니다. minSdkVersion 충돌, Multidex 에러, Namespace 변경(Gradle 8.0), JDK 버전 문제, 그리고 의존성 트리 분석까지 완벽하게 해결해 봅니다.

Debug에선 잘 되는데 Release에서만 죽나요? 범인은 '난독화'입니다. R8의 원리, Mapping 파일 분석, 그리고 Reflection을 사용하는 라이브러리를 지켜내는 방법(@Keep)을 정리해봤습니다.

서버에서 잘 오던 데이터가 갑자기 앱을 죽입니다. 'type Null is not a subtype of type String' 에러의 원인과, 안전한 JSON 파싱을 위한 Null Safety 전략을 정리해봤습니다.

개발자에게 가장 스트레스 받는 순간 중 하나는 "푸시 알림이 안 온다"는 제보를 받을 때입니다. 로그에는 성공(Success)이라고 뜨는데, 사용자의 폰은 조용합니다. 특히 iOS가 말썽입니다. 안드로이드는 대충 설정해도 잘 오는데, 애플은 인증서 하나만 틀려도 묵묵부답입니다.
초보자가 가장 많이 하는 착각은 "푸시는 그냥 오는 거 아닌가?"입니다. 앱의 상태에 따라 처리 방식이 완전히 다릅니다.
onMessage 콜백을 받아서 직접 UI(Dialog, Snackbar)를 띄워줘야 합니다. flutter_local_notifications 패키지와 연동이 필요합니다.iOS 푸시가 안 온다면 99%는 인증서 문제입니다.
예전에는 매년 갱신해야 하는 .p12 인증서를 썼지만, 지금은 APNs Key (.p8) 파일을 쓰는 게 국룰입니다.
체크리스트:
.p8 파일을 Firebase Project Settings -> Cloud Messaging -> APNs Authentication Key에 업로드했나요? Team ID와 Key ID가 정확한가요?Signing & Capabilities 탭에서 Push Notifications와 Background Modes (Remote notifications)를 켰나요? (이거 안 켜면 절대 안 옴)안드로이드 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를 맞춰줘야 합니다.
삼성 갤럭시 등은 배터리를 아끼려고 백그라운드 앱을 강제로 재워버립니다. 앱이 꺼져 있을 때 푸시가 안 온다면, 메시지 우선순위(Priority)를 확인해야 합니다.
{
"message": {
"token": "device_token",
"notification": { "title": "제목", "body": "내용" },
"android": {
"priority": "high", // 👈 필살기 1: 잠든 폰 깨우기
"notification": {
"channel_id": "high_importance_channel" // 👈 필살기 2: 채널 ID 일치
}
}
}
}
앱이 꺼져 있을 때 데이터 처리(예: 읽지 않은 메시지 수 갱신, 로컬 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());
}
이걸 안 지키면 "클래스를 찾을 수 없음" 에러가 나면서 앱이 죽거나 무시됩니다.
iOS 알림에 이미지를 넣으려면 Notification Service Extension이라는 별도의 Target을 Xcode에서 추가해야 합니다.
기본 푸시만으로는 이미지가 안 뜹니다.
Notification Service Extension.NotificationService.swift 파일이 생깁니다.bestAttemptContent.attachments에 추가하는 코드를 작성해야 합니다.com.myapp -> com.myapp.notification).내 서버가 문제인지 앱이 문제인지 헷갈릴 때가 있습니다. PushTry.com 같은 사이트에서 FCM Server Key와 Device Token만 넣고 테스트해보세요. 여기서 잘 오면 서버 문제, 안 오면 앱 설정 문제입니다.
vm:entry-point 필수.
푸시는 보내는 건 쉬워도 "확실하게" 받는 건 예술의 영역입니다.