Flutter FCM 백그라운드 알림 수신 지연 해결기
2026. 04. 15 작성 · 트러블슈팅
1. 문제의 발단: 실시간 알림의 생명은 '속도'
최근 로스트아크 거래소 매물 알림 서비스인 '벨로아(Belloa)' 앱을 개발하여 운영 중입니다. 이 서비스의 핵심은 사용자가 원하는 옵션의 악세서리나 각인서가 거래소에 등록되었을 때, 누구보다 빠르게 푸시 알림을 받아 구매할 수 있도록 돕는 것입니다. 1분 1초가 중요한 경매장 시스템의 특성상 알림의 지연은 곧 서비스의 가치 하락을 의미합니다.
초기 개발 단계에서는 앱이 켜져 있는 포그라운드(Foreground) 상태에서는 알림이 즉각적으로 잘 도착했습니다. 그러나 앱을 백그라운드(Background)로 내리거나 화면을 끄고 일정 시간이 지나면, 푸시 알림이 최소 몇 분에서 길게는 수십 분까지 지연되어 한꺼번에 쏟아지는 치명적인 문제가 발생했습니다. 사용자들이 매물을 놓치는 상황이 발생했고, 저는 즉시 원인 분석에 돌입했습니다.
2. 원인 파악: Android Doze 모드와 FCM 페이로드의 차이
안드로이드(Android) 운영체제는 배터리 수명을 극대화하기 위해 디바이스가 오랫동안 사용되지 않을 때 'Doze 모드(잠자기 모드)'로 진입합니다. 이 모드에서는 네트워크 접근과 백그라운드 작업이 엄격하게 제한됩니다. Firebase Cloud Messaging (FCM) 공식 문서를 샅샅이 뒤져본 결과, FCM이 보내는 메시지 타입에 따라 Doze 모드에서의 동작 방식이 완전히 다르다는 것을 알게 되었습니다.
- Notification Message (알림 메시지): FCM SDK가 알림 표시를 직접 처리합니다. 구현이 매우 간단하지만, 기기가 Doze 모드일 경우 시스템이 알림을 보류했다가 화면이 켜지거나 유지보수 시간(Maintenance Window)이 되어서야 일괄 처리합니다. 이것이 바로 지연의 핵심 원인이었습니다.
- Data Message (데이터 메시지): 클라이언트 앱이 메시지 페이로드를 직접 수신하여 처리합니다. 안드로이드 시스템이 개입하여 알림을 띄워주지 않으므로 개발자가 직접 Local Notification을 구현해야 하지만, Doze 모드 상태에서도 앱을 깨워 즉시 데이터를 전달할 수 있는 특징이 있습니다.
3. 문제 해결: 백엔드 발송 로직 수정과 Flutter 백그라운드 핸들러 구현
문제를 해결하기 위해 백엔드(Spring Boot)와 클라이언트(Flutter) 양쪽의 코드를 전면 수정하기로 결정했습니다. 첫 번째로 Spring Boot 서버에서
FCM으로 전송하는 메시지 페이로드(Payload) 구조를 변경했습니다. 기존에는 notification 키에 제목과 내용을 담아 보냈지만, 이를
과감히 삭제하고 모든 데이터를 data 키 내부에만 담아 전송하는 순수 Data Message 형태로 발송 로직을
개편했습니다.
두 번째로 Flutter 앱 내에서 백그라운드 상태의 Data Message를 수신하여 로컬 알림을 띄우는 작업을 진행했습니다.
firebase_messaging 패키지를 활용하여 백그라운드 핸들러를 등록해야 했는데, 이때 중요한 것은 핸들러 함수가 최상위(Top-level)
함수이거나 익명 함수여야 한다는 점이었습니다. Dart 코드 내에 @pragma('vm:entry-point') 어노테이션을 명시하여 앱이 완전히
종료된 상태(Terminated)에서도 Flutter 엔진이 해당 함수를 실행할 수 있도록 진입점을 보장해 주었습니다.
수신된 데이터를 파싱한 후에는 flutter_local_notifications 패키지를 이용해 안드로이드의 Notification Channel을
생성하고, Importance를 Max(최대)로 설정하여 팝업(Heads-up) 알림이 발생하도록 처리했습니다. 안드로이드 13 이상부터 요구되는 알림
권한(POST_NOTIFICATIONS) 요청 로직도 꼼꼼히 추가했습니다.
4. 결과 및 회고
이러한 아키텍처 수정을 거친 후 테스트를 진행한 결과, 기기 화면을 끄고 몇 시간이 지난 Doze 모드 상태에서도 로스트아크 매물 알림이 1초의 지연 없이 즉각적으로 도착하는 것을 확인할 수 있었습니다. 사용자들의 피드백도 긍정적으로 돌아왔고, 앱의 사용 유지율(Retention) 역시 눈에 띄게 상승했습니다.
이번 트러블슈팅을 통해 단순히 라이브러리를 연동하는 것을 넘어, 모바일 OS의 배터리 관리 정책(Doze Mode)과 FCM의 내부 동작 원리를 깊이 이해할 수 있었습니다. 특히 서버와 클라이언트 간의 페이로드 규약이 시스템 레벨의 동작에 얼마나 큰 영향을 미치는지 깨달은 소중한 경험이었습니다.