Tuesday, November 26, 2019

Flutter 푸시 알림의 완성: firebase_messaging와 flutter_local_notifications 충돌 완벽 해결법

Flutter 푸시 알림의 완성: Firebase와 Local Notifications 충돌 완벽 해결법

Flutter로 앱을 개발하다 보면 사용자에게 시의적절한 정보를 전달하고 재방문을 유도하는 '푸시 알림' 기능은 거의 필수적입니다. Flutter 생태계에서 푸시 알림을 구현할 때 가장 먼저 떠오르는 두 가지 강력한 패키지는 바로 firebase_messagingflutter_local_notifications입니다. firebase_messaging은 Firebase Cloud Messaging(FCM)을 통해 원격 푸시 알림을 수신하는 데 사용되고, flutter_local_notifications는 앱이 포그라운드에 있을 때 알림을 표시하거나, 특정 조건에 따라 기기 자체에서 로컬 알림을 생성하는 데 매우 유용합니다.

하지만 많은 개발자들이 이 두 패키지를 함께 사용하려 할 때 예상치 못한 문제에 부딪힙니다. 특히 iOS 환경에서 "FCM 메시지는 분명히 오는데 onMessage 콜백이 호출되지 않아요!", "알림을 탭해도 아무 반응이 없어요!" 와 같은 미스터리한 현상을 겪곤 합니다. 이 문제의 원인은 두 패키지가 iOS의 알림 처리를 담당하는 '대리자(Delegate)' 자리를 놓고 경쟁하기 때문입니다. 과거에는 이 문제를 해결하기 위해 flutter_local_notifications 패키지를 직접 fork하여 코드를 수정하는 복잡한 방법을 사용해야만 했습니다.

다행히도, 이제는 더 이상 그런 번거로운 과정을 거칠 필요가 없습니다. 최신 버전의 패키지들은 올바른 초기화 순서와 설정을 통해 두 기능을 완벽하게 조화시켜 사용할 수 있는 방법을 제공합니다. 이 글에서는 firebase_messagingflutter_local_notifications의 충돌 원인을 심도 있게 분석하고, 현재 가장 권장되는 최신 통합 방법을 단계별로 상세히 안내하여 여러분의 앱에 안정적이고 강력한 푸시 알림 시스템을 구축할 수 있도록 돕겠습니다. 더 이상 알림 충돌 문제로 시간을 낭비하지 마세요. 이 가이드를 통해 푸시 알림 기능을 완벽하게 정복해 보세요.

1. 왜 충돌이 발생할까? iOS 알림 처리 메커니즘의 이해

문제를 해결하기 위해선 먼저 원인을 알아야 합니다. iOS에서 푸시 알림이 어떻게 처리되는지, 그리고 두 패키지가 왜 서로 충돌하는지를 이해하는 것이 중요합니다.

iOS의 UNUserNotificationCenter와 Delegate 패턴

iOS 10부터 도입된 UserNotifications 프레임워크는 앱의 모든 알림(로컬 및 원격)을 관리하는 중심적인 역할을 합니다. 이 프레임워크의 핵심은 UNUserNotificationCenter라는 싱글톤 객체입니다. 이 객체는 알림 권한을 요청하고, 알림을 예약하며, 수신된 알림을 처리하는 모든 작업을 총괄합니다.

여기서 중요한 개념이 바로 'Delegate(대리자)' 패턴입니다. UNUserNotificationCenter는 알림과 관련된 특정 이벤트가 발생했을 때(예: 앱이 실행 중일 때 알림이 도착한 경우) 그 처리를 직접 하지 않고, 자신에게 등록된 '대리자' 객체에게 위임합니다. 이 대리자 객체는 UNUserNotificationCenterDelegate 프로토콜을 따라야 하며, 다음과 같은 중요한 메서드를 구현해야 합니다.

  • userNotificationCenter(_:willPresent:withCompletionHandler:): 앱이 포그라운드(Foreground) 상태에 있을 때 알림이 도착하면 호출됩니다. 이 메서드 안에서 알림을 사용자에게 어떻게 보여줄지(배너, 소리, 배지 등) 결정할 수 있습니다.
  • userNotificationCenter(_:didReceive:withCompletionHandler:): 사용자가 알림을 탭했을 때, 또는 사일런트 푸시(Silent Push)가 도착했을 때 호출됩니다. 알림 클릭 후 특정 화면으로 이동하는 등의 로직을 처리하는 곳입니다.

핵심은 하나의 UNUserNotificationCenter에는 단 하나의 대리자(delegate)만 등록될 수 있다는 점입니다.

두 패키지의 대리자 쟁탈전

이제 firebase_messagingflutter_local_notifications가 어떻게 동작하는지 봅시다.

  1. firebase_messaging의 역할: FCM으로부터 원격 푸시 알림을 수신하고, 이를 처리하기 위해 내부적으로 UNUserNotificationCenter의 대리자로 자신을 등록합니다. 그래야만 onMessage(포그라운드 수신)나 알림 탭 이벤트를 감지할 수 있기 때문입니다.
  2. flutter_local_notifications의 역할: 이 패키지 역시 포그라운드에서 알림을 표시하거나, 사용자가 로컬 알림을 탭했을 때의 이벤트를 처리하기 위해 UNUserNotificationCenter의 대리자로 자신을 등록하려고 시도합니다.

문제는 여기서 발생합니다. 두 패키지를 별다른 설정 없이 함께 초기화하면, 나중에 초기화된 패키지가 먼저 등록된 대리자를 덮어쓰게 됩니다.

예를 들어, firebase_messaging이 먼저 대리자를 등록했는데, 바로 뒤에 flutter_local_notifications가 초기화되면서 UNUserNotificationCenter.current().delegate = self 와 같은 코드를 실행하면, 이제 알림 처리의 모든 권한은 flutter_local_notifications에게 넘어갑니다. 그 결과, firebase_messaging은 더 이상 포그라운드 알림 수신 이벤트나 알림 탭 이벤트를 받을 수 없게 되고, onMessage 콜백은 침묵하게 되는 것입니다. 이것이 바로 많은 개발자들이 겪는 충돌의 실체입니다.

과거에는 이 문제를 해결하기 위해 flutter_local_notifications의 소스 코드에서 대리자를 설정하는 부분을 강제로 삭제하고 사용하는 방식(Fork & Modify)이 제안되었지만, 이는 패키지 업데이트를 어렵게 만들고 유지보수 비용을 증가시키는 매우 비효율적인 방법이었습니다.

2. 최신 접근법: 조화로운 통합 전략

현시점에서 이 문제를 해결하는 가장 올바르고 권장되는 방법은 '역할 분담과 협력'입니다. 즉, firebase_messaging이 원격 알림 수신의 주도권을 계속 가지도록 하고, flutter_local_notifications는 포그라운드에서 알림을 '보여주는' 역할에만 집중하도록 만드는 것입니다.

전체적인 흐름은 다음과 같습니다.

  1. FCM 설정: firebase_messaging을 주 알림 핸들러로 설정하고 초기화합니다. iOS에서 필요한 모든 권한을 요청합니다.
  2. 포그라운드 알림 옵션 설정: firebase_messagingsetForegroundNotificationPresentationOptions 메서드를 사용하여 앱이 포그라운드에 있을 때 FCM 알림을 어떻게 처리할지 시스템에 알립니다. 기본적으로 포그라운드에서는 알림 배너가 뜨지 않으므로, 이 부분을 제어해야 합니다.
  3. Local Notifications 초기화: flutter_local_notifications를 초기화합니다. 단, iOS 초기화 설정에서 대리자(delegate)를 설정하는 로직을 건드리지 않도록 주의합니다. 안드로이드를 위한 알림 채널 설정은 필수입니다.
  4. 이벤트 연결: firebase_messagingonMessage 콜백(포그라운드에서 FCM 수신 시 호출) 내부에서, 수신된 알림 정보를 바탕으로 flutter_local_notifications의 `show()` 메서드를 호출하여 사용자에게 로컬 알림을 '수동으로' 표시합니다.

이 방식을 사용하면 iOS의 알림 대리자는 firebase_messaging이 계속 유지하므로 원격 알림 관련 콜백이 정상적으로 동작합니다. 동시에 포그라운드에서는 flutter_local_notifications의 강력한 커스터마이징 기능을 활용하여 사용자에게 풍부한 알림 경험을 제공할 수 있습니다.

3. 단계별 통합 가이드: 실제 코드 구현하기

이제 이론을 바탕으로 실제 프로젝트에 어떻게 적용하는지 단계별로 살펴보겠습니다. 깔끔한 코드 관리를 위해 알림 관련 로직을 별도의 서비스 클래스로 분리하는 것을 추천합니다.

1단계: 프로젝트 설정 및 의존성 추가

먼저, pubspec.yaml 파일에 필요한 패키지들을 추가합니다.


dependencies:
  flutter:
    sdk: flutter
  firebase_core: ^2.27.0 # Firebase 핵심 기능
  firebase_messaging: ^14.7.19 # FCM 기능
  flutter_local_notifications: ^17.0.0 # 로컬 알림 기능

  # 기타 필요한 패키지들...

터미널에서 flutter pub get을 실행하여 패키지를 설치합니다.

2단계: Firebase 프로젝트 설정 및 플랫폼별 구성

이 단계는 기본적인 Firebase 설정 과정입니다. 이미 완료하셨다면 건너뛰어도 좋습니다.

  1. Firebase 프로젝트 생성: Firebase 콘솔에서 새 프로젝트를 생성합니다.
  2. 앱 등록: 프로젝트에 Android 및 iOS 앱을 등록합니다.
    • Android: android/app/build.gradle 파일에서 applicationId를 확인하여 입력하고, `google-services.json` 파일을 다운로드하여 android/app/ 디렉터리에 추가합니다.
    • iOS: ios/Runner.xcworkspace를 Xcode로 열어 Bundle Identifier를 확인하여 입력하고, `GoogleService-Info.plist` 파일을 다운로드하여 Xcode를 통해 Runner/Runner 디렉터리에 추가합니다.
  3. iOS 푸시 알림 기능 활성화: Xcode에서 Runner 타겟을 선택하고, 'Signing & Capabilities' 탭으로 이동합니다. '+ Capability'를 클릭하여 'Push Notifications'를 추가합니다. 또한, 'Background Modes'를 추가하고 'Remote notifications'를 체크합니다.
  4. APNs 인증 키 설정: Apple Developer 계정에서 APNs(Apple Push Notification service) 인증 키(.p8 파일)를 생성하고, Firebase 콘솔의 '프로젝트 설정 > 클라우드 메시징 > Apple 앱 구성'에 업로드합니다. 이는 iOS 기기로 푸시를 보내기 위한 필수 과정입니다.

3단계: Android 매니페스트 및 설정

Android 8.0 (API 26) 이상에서는 알림을 표시하기 위해 '알림 채널(Notification Channel)'이 반드시 필요합니다. 또한, 기본 알림 아이콘과 색상을 지정해주는 것이 좋습니다.

android/app/src/main/AndroidManifest.xml 파일을 열고 태그 내부에 다음 메타데이터를 추가합니다.



<meta-data
    android:name="com.google.firebase.messaging.default_notification_channel_id"
    android:value="high_importance_channel" />


<meta-data
    android:name="com.google.firebase.messaging.default_notification_icon"
    android:resource="@drawable/ic_notification" />
    

<meta-data
    android:name="com.google.firebase.messaging.default_notification_color"
    android:resource="@color/colorAccent" />

ic_notification 아이콘은 일반적으로 하얀색의 간단한 로고 이미지를 사용하며, res/drawable 폴더에 위치해야 합니다. colorAccentres/values/colors.xml 파일에 정의된 색상 리소스입니다.

4단계: 알림 서비스 클래스 작성 (핵심 로직)

이제 모든 설정이 완료되었으니, Dart 코드를 작성할 차례입니다. lib/services/notification_service.dart와 같은 파일을 생성하고 아래 코드를 작성합니다.


import 'dart:convert';
import 'dart:io';

import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:http/http.dart' as http;

// 백그라운드/종료 상태에서 메시지를 처리하기 위한 최상위 함수
// @pragma('vm:entry-point') 어노테이션은 AOT 컴파일 시 이 함수가 트리 쉐이킹되지 않도록 보장합니다.
@pragma('vm:entry-point')
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
  // 백그라운드에서 어떤 작업을 수행해야 한다면 여기에 작성합니다.
  // 예를 들어, API 호출, 로컬 데이터베이스 업데이트 등
  // 여기서 UI 관련 작업은 수행할 수 없습니다.
  await Firebase.initializeApp(); // 백그라운드에서도 Firebase 초기화가 필요할 수 있음
  print("백그라운드 메시지 처리: ${message.messageId}");
}

class NotificationService {
  // 싱글톤 패턴으로 인스턴스 관리
  NotificationService._privateConstructor();
  static final NotificationService _instance = NotificationService._privateConstructor();
  factory NotificationService() {
    return _instance;
  }

  // flutter_local_notifications 플러그인 인스턴스 생성
  final FlutterLocalNotificationsPlugin _localNotifications = FlutterLocalNotificationsPlugin();
  // Firebase Messaging 인스턴스 생성
  final FirebaseMessaging _firebaseMessaging = FirebaseMessaging.instance;

  // 알림 초기화를 위한 전체 메서드
  Future<void> initialize() async {
    // 1. FCM 초기 설정
    await _initializeFCM();

    // 2. Local Notifications 초기 설정
    await _initializeLocalNotifications();

    // 3. FCM 메시지 핸들러 설정
    _setupMessageHandlers();
  }

  // 1. FCM 초기 설정
  Future<void> _initializeFCM() async {
    // iOS 포그라운드 알림 옵션 설정 - 이 부분이 중요!
    // 이 설정을 통해 포그라운드에서도 알림(배너, 소리 등)이 보이도록 합니다.
    // 하지만 우리는 Local Notifications를 통해 커스텀 UI를 보여줄 것이므로,
    // 시스템 알림을 끌 수도 있습니다. 여기서는 기본 시스템 알림을 허용합니다.
    await _firebaseMessaging.setForegroundNotificationPresentationOptions(
      alert: true, // Required to display a heads-up notification
      badge: true,
      sound: true,
    );
    
    // iOS 알림 권한 요청
    if (Platform.isIOS) {
      await _requestIOSPermissions();
    }

    // 디바이스 토큰 가져오기 (필요시 서버로 전송)
    final fcmToken = await _firebaseMessaging.getToken();
    print("FCM Token: $fcmToken");

    // 토큰 갱신 감지
    _firebaseMessaging.onTokenRefresh.listen((newToken) {
      print("FCM Token is refreshed: $newToken");
      // 여기서 새로운 토큰을 서버로 전송하는 로직을 구현합니다.
    });
  }

  // iOS 권한 요청
  Future<void> _requestIOSPermissions() async {
    NotificationSettings settings = await _firebaseMessaging.requestPermission(
      alert: true,
      announcement: false,
      badge: true,
      carPlay: false,
      criticalAlert: false,
      provisional: false,
      sound: true,
    );

    print('User granted permission: ${settings.authorizationStatus}');
  }

  // 2. Local Notifications 초기 설정
  Future<void> _initializeLocalNotifications() async {
    // Android 초기화 설정
    const AndroidInitializationSettings initializationSettingsAndroid =
        AndroidInitializationSettings('@mipmap/ic_launcher'); // 앱 아이콘 사용

    // iOS 초기화 설정
    // onDidReceiveLocalNotification은 오래된 iOS 버전을 위한 콜백입니다.
    // **가장 중요한 부분**: `defaultPresentAlert`, `defaultPresentBadge`, `defaultPresentSound`를 false로 설정하여
    // Local Notifications 플러그인이 자동으로 포그라운드 알림을 표시하지 않도록 합니다.
    // 우리는 FCM의 onMessage에서 수동으로 알림을 표시할 것이기 때문입니다.
    // 이것이 바로 과거의 Delegate 충돌을 피하는 핵심적인 현대적 방법입니다.
    final DarwinInitializationSettings initializationSettingsIOS = DarwinInitializationSettings(
      requestAlertPermission: false,
      requestBadgePermission: false,

      requestSoundPermission: false,
      onDidReceiveLocalNotification: onDidReceiveLocalNotification,
    );

    final InitializationSettings initializationSettings = InitializationSettings(
      android: initializationSettingsAndroid,
      iOS: initializationSettingsIOS,
    );

    // 알림 플러그인 초기화 및 콜백 설정
    await _localNotifications.initialize(
      initializationSettings,
      onDidReceiveNotificationResponse: onDidReceiveNotificationResponse,
      onDidReceiveBackgroundNotificationResponse: onDidReceiveBackgroundNotificationResponse,
    );
  }

  // 3. FCM 메시지 핸들러 설정
  void _setupMessageHandlers() {
    // 앱이 포그라운드에 있을 때 메시지 처리
    FirebaseMessaging.onMessage.listen((RemoteMessage message) {
      print('Got a message whilst in the foreground!');
      print('Message data: ${message.data}');

      if (message.notification != null) {
        print('Message also contained a notification: ${message.notification}');

        // **핵심 로직**: FCM 메시지를 받으면 Local Notification을 사용하여 알림을 표시합니다.
        // 이를 통해 일관된 알림 경험을 제공하고, 커스터마이징이 가능해집니다.
        showLocalNotification(message);
      }
    });

    // 앱이 백그라운드 또는 종료 상태에 있을 때 메시지 처리
    FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);
    
    // 앱이 종료된 상태에서 알림을 탭하여 열었을 때 메시지 가져오기
    _firebaseMessaging.getInitialMessage().then((RemoteMessage? message) {
      if (message != null) {
        print("앱 종료 상태에서 알림 탭: ${message.data}");
        // 여기서 payload를 기반으로 특정 페이지로 네비게이션하는 로직을 추가할 수 있습니다.
        // 예: navigatorKey.currentState?.pushNamed('/details', arguments: message.data);
      }
    });

    // 앱이 백그라운드 상태에서 알림을 탭하여 열었을 때 메시지 처리
    FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) {
      print('A new onMessageOpenedApp event was published!');
      print("앱 백그라운드 상태에서 알림 탭: ${message.data}");
       // 여기서 payload를 기반으로 특정 페이지로 네비게이션하는 로직을 추가할 수 있습니다.
    });
  }

  // Local Notification을 실제로 표시하는 함수
  Future<void> showLocalNotification(RemoteMessage message) async {
    final remoteNotification = message.notification;
    final android = message.notification?.android;
    
    // Android용 알림 채널 생성 (매번 호출해도 이미 생성되어 있으면 무시됨)
    const AndroidNotificationChannel channel = AndroidNotificationChannel(
      'high_importance_channel', // ID
      'High Importance Notifications', // 제목
      description: 'This channel is used for important notifications.', // 설명
      importance: Importance.max,
    );

    await _localNotifications
        .resolvePlatformSpecificImplementation<AndroidFlutterLocalNotificationsPlugin>()
        ?.createNotificationChannel(channel);

    if (remoteNotification != null) {
      _localNotifications.show(
        remoteNotification.hashCode, // 알림 ID
        remoteNotification.title, // 알림 제목
        remoteNotification.body, // 알림 본문
        NotificationDetails(
          android: AndroidNotificationDetails(
            channel.id,
            channel.name,
            channelDescription: channel.description,
            icon: android?.smallIcon ?? '@mipmap/ic_launcher',
            // 기타 안드로이드 전용 설정
          ),
          iOS: const DarwinNotificationDetails(
            badgeNumber: 1, // 배지 숫자 등 iOS 전용 설정
          ),
        ),
        payload: jsonEncode(message.data), // 알림 탭 시 전달할 데이터
      );
    }
  }

  // --- Local Notifications 콜백 함수들 ---

  // (오래된 iOS 버전용) 로컬 알림 수신 시 콜백
  void onDidReceiveLocalNotification(int id, String? title, String? body, String? payload) {
    print('onDidReceiveLocalNotification: id-$id, title-$title, payload-$payload');
  }

  // (최신 버전) 알림 탭 시 호출되는 콜백 (앱이 포그라운드/백그라운드일 때)
  void onDidReceiveNotificationResponse(NotificationResponse notificationResponse) {
    final String? payload = notificationResponse.payload;
    if (payload != null) {
      print('notification payload: $payload');
      final data = jsonDecode(payload);
      // 여기서 payload 데이터를 사용하여 특정 페이지로 이동하는 로직을 구현합니다.
      // 예: if (data['type'] == 'chat') { navigatorKey.currentState?.push(...) }
    }
  }
}

// (최신 버전) 앱이 종료된 상태에서 알림 탭 시 호출되는 콜백
@pragma('vm:entry-point')
void onDidReceiveBackgroundNotificationResponse(NotificationResponse notificationResponse) {
  print('onDidReceiveBackgroundNotificationResponse: payload - ${notificationResponse.payload}');
  // 이 콜백은 앱이 완전히 종료된 상태에서 알림을 탭했을 때 호출됩니다.
  // main 함수에서 초기화 시 이 콜백이 실행되었는지 확인하고 데이터를 처리할 수 있습니다.
}

5단계: main.dart에서 서비스 초기화

마지막으로, 앱이 시작될 때 위에서 만든 NotificationService를 초기화해야 합니다. main.dart 파일을 다음과 같이 수정합니다.


import 'package:flutter/material.dart';
import 'package:firebase_core/firebase_core.dart';
import 'firebase_options.dart'; // flutterfire configure를 통해 생성된 파일
import 'services/notification_service.dart'; // 방금 만든 서비스 파일

void main() async {
  // Flutter 엔진과 위젯 바인딩 초기화
  WidgetsFlutterBinding.ensureInitialized();
  
  // Firebase 초기화
  await Firebase.initializeApp(
    options: DefaultFirebaseOptions.currentPlatform,
  );

  // 알림 서비스 초기화
  await NotificationService().initialize();

  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Notification Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const HomePage(),
    );
  }
}

class HomePage extends StatelessWidget {
  const HomePage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('FCM & Local Notifications'),
      ),
      body: const Center(
        child: Text('푸시 알림을 기다리는 중...'),
      ),
    );
  }
}

이제 모든 준비가 끝났습니다. 앱을 실행하고 Firebase 콘솔이나 여러분의 백엔드 서버를 통해 FCM 메시지를 보내보세요. 앱이 포그라운드 상태일 때도 flutter_local_notifications에 의해 생성된 아름다운 알림이 표시되고, 알림을 탭했을 때 onDidReceiveNotificationResponse 콜백이 정상적으로 호출되는 것을 확인할 수 있을 것입니다.

4. 심화 주제 및 문제 해결

알림 페이로드(Payload)와 화면 이동

실제 앱에서는 알림을 탭했을 때 특정 화면으로 이동하는 기능이 필수적입니다. 이는 알림의 `payload`를 통해 구현할 수 있습니다.

  1. FCM 메시지 보낼 때: `data` 필드에 화면 이동에 필요한 정보를 담아 보냅니다.
    
        {
            "to": "FCM_TOKEN",
            "notification": {
                "title": "새로운 메시지",
                "body": "친구가 메시지를 보냈습니다."
            },
            "data": {
                "type": "chat",
                "screen": "/chat_room",
                "chat_room_id": "12345"
            }
        }
        
  2. Flutter에서 처리: `NotificationService`의 `onDidReceiveNotificationResponse` 콜백에서 payload를 파싱하여 처리합니다.
    
        // main.dart에 NavigatorKey를 전역으로 선언
        final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
    
        // onDidReceiveNotificationResponse 내부
        void onDidReceiveNotificationResponse(NotificationResponse notificationResponse) {
            final String? payload = notificationResponse.payload;
            if (payload != null) {
                final data = jsonDecode(payload);
                if (data['screen'] != null) {
                    navigatorKey.currentState?.pushNamed(data['screen'], arguments: data);
                }
            }
        }
        

    이 방법을 사용하려면 MaterialApp에 `navigatorKey`를 설정하고, `onGenerateRoute`를 통해 라우팅 로직을 구현해야 합니다.

백그라운드/종료 상태에서의 데이터 처리

FirebaseMessaging.onBackgroundMessage 핸들러는 UI 컨텍스트와 격리된 별도의 Isolate에서 실행됩니다. 따라서 이 함수 내에서는 setState와 같은 UI 업데이트나 네비게이션 작업을 직접 수행할 수 없습니다.

  • 허용되는 작업: HTTP 요청, 로컬 스토리지(SharedPreferences, Hive 등)에 데이터 저장, 다른 플러그인과의 상호작용(메서드 채널을 통해) 등.
  • 주의사항: 이 핸들러는 반드시 최상위(top-level) 함수이거나 static 메서드여야 합니다. 클래스 내부의 인스턴스 메서드는 될 수 없습니다.

자주 겪는 문제와 해결책 (Troubleshooting)

  • 문제: 안드로이드에서 알림이 아예 오지 않아요.
    • 해결책 1: AndroidManifest.xmlcom.google.firebase.messaging.default_notification_channel_id 메타데이터가 올바르게 설정되었는지 확인하세요.
    • 해결책 2: NotificationService에서 AndroidNotificationChannel을 생성하고 `createNotificationChannel`을 호출하는 로직이 포함되어 있는지 확인하세요. Android 8 이상에서는 채널이 없으면 알림이 표시되지 않습니다.
    • 해결책 3: 기기의 앱 설정에서 알림 권한이 활성화되어 있는지 확인하세요.
  • 문제: iOS 시뮬레이터에서 푸시 알림이 오지 않아요.
    • 해결책: iOS 시뮬레이터는 원격 푸시 알림을 지원하지 않습니다. APNs를 통한 푸시 알림 테스트는 반드시 실제 iOS 기기에서 진행해야 합니다.
  • 문제: onMessage는 호출되는데, 포그라운드에서 알림 배너가 보이지 않아요.
    • 해결책 1: firebase_messaging.setForegroundNotificationPresentationOptions가 호출되었는지, `alert: true` 옵션이 포함되었는지 확인하세요.
    • 해결책 2: `onMessage` 리스너 내부에서 flutter_local_notifications.show() 메서드를 호출하여 수동으로 알림을 표시하는 로직이 제대로 구현되었는지 확인하세요.

결론: 이제 충돌은 없다

Flutter에서 firebase_messagingflutter_local_notifications를 함께 사용하는 것은 더 이상 골치 아픈 문제가 아닙니다. 과거의 충돌은 iOS의 알림 처리 Delegate를 두 패키지가 서로 차지하려 했기 때문에 발생했지만, 이제는 명확한 역할 분담을 통해 두 패키지의 장점만을 취할 수 있습니다.

핵심을 다시 정리하면 다음과 같습니다.

  1. 주도권은 firebase_messaging에게: FCM이 원격 알림 수신과 관련된 모든 이벤트를 주도적으로 처리하도록 합니다.
  2. flutter_local_notifications는 조력자: 포그라운드 상태에서 FCM 메시지를 수신했을 때, 이 패키지를 사용하여 사용자에게 시각적인 알림을 '보여주는' 도구로 활용합니다.
  3. 올바른 초기화가 관건: 두 패키지를 초기화할 때, 특히 flutter_local_notifications의 iOS 설정을 조정하여 Delegate 충돌을 원천적으로 방지합니다.

이 가이드에서 제시한 단계별 통합 방법과 코드 예제를 따르면, 여러분의 Flutter 앱에 안정적이고 확장 가능한 푸시 알림 시스템을 성공적으로 구축할 수 있을 것입니다. 이제 알림 충돌의 미스터리에서 벗어나, 사용자에게 더 나은 경험을 제공하는 데 집중하시기 바랍니다.


0 개의 댓글:

Post a Comment