내가 실시간 알림에서 “유령” 버그를 쫓던 날
Source: Dev.to
존재하지 않았던 문제… 아니면 있었을까?
몇 달 전, 실시간 알림을 갖춘 모바일 앱을 개발하고 있었습니다. 기능은 간단해 보였습니다: 전자상거래 앱에서 새 주문이 들어오면 사용자가 즉시 푸시 알림을 받아야 한다는 것이죠.
개발 단계에서는 모든 것이 완벽히 작동했습니다—내 기기에서는 알림이 밀리초 단위로 바로 뜨곤 했습니다. 하지만 베타 테스터들이 앱을 사용하기 시작하면서 이상한 현상이 나타났습니다: 일부 사용자는 중복 알림을 받았습니다. 마치 유령을 쫓는 듯한 느낌이었습니다. 시스템이 어떻게 이렇게 일관성 없이 동작할 수 있나요?
앱은 Firebase Cloud Messaging (FCM) 을 푸시 알림에 사용하고 있었습니다. 초기 용의자는 백엔드, 모바일 프레임워크, 혹은 사용자의 기기였습니다. 저는 단계별로 확인했습니다:
- 백엔드 로그 확인 — 모든 알림이 정상적으로 전송되었습니다.
- 문제는 플랫폼에 특화된 미묘한 이슈라고 결론지었습니다.
몇 시간 동안 디버깅한 끝에 원인을 찾았습니다: 예기치 않게 여러 개의 Firebase 리스너가 등록되고 있었습니다.
근본 원인
Flutter 코드에서 다음과 같은 부분이 있었습니다:
// Example listener registration (simplified)
FirebaseMessaging.onMessage.listen((RemoteMessage message) {
// Handle incoming notification
showNotification(message);
});
이 리스너가 사용자가 홈 화면으로 이동할 때마다 등록되고 있었습니다. 화면을 왔다갔다 하면 새 리스너가 추가돼, 예를 들어 세 번 이동하면 같은 알림이 세 번 트리거됩니다. 또한 레이스 컨디션 때문에 앱이 완전히 초기화되기 전에 리스너가 작동해 알림이 지연되거나 누락되기도 했습니다.
해결책
- 앱 시작 시 한 번만 리스너를 등록합니다(예:
main()혹은 최상위 provider에서). 중앙화된 한 곳에 두세요. showNotification()을 멱등(idempotent)하게 만들어 동일한 메시지가 여러 번 도착해도 한 번만 표시되도록 합니다.
// Register once, e.g., in main()
void main() {
WidgetsFlutterBinding.ensureInitialized();
FirebaseMessaging.onMessage.listen(_handleMessage);
runApp(MyApp());
}
// Centralized handler
void _handleMessage(RemoteMessage message) {
showNotification(message);
}
// Idempotent notification display
final Set _displayedMessageIds = {};
void showNotification(RemoteMessage message) {
final String id = message.messageId ?? '';
if (id.isEmpty || _displayedMessageIds.contains(id)) return;
_displayedMessageIds.add(id);
// TODO: Show the notification using your preferred method
}
교훈
- 유령 버그는 종종 미묘한 라이프사이클 문제에서 비롯됩니다.
- 실시간 시스템에서는 리스너가 몇 개 존재하는지, 언제 작동하는지, 사용자가 앱을 이동할 때 어떤 일이 일어나는지를 항상 고려해야 합니다.
- 비동기 이벤트 처리를 중앙화하면 중복 동작을 방지하고 디버깅이 훨씬 쉬워집니다.
알림, 비동기 이벤트, 실시간 시스템을 다루는 개발자라면 리스너 등록과 라이프사이클 관리를 신중히 생각하세요—유령을 쫓는 데 드는 시간을 크게 절감할 수 있습니다.