250207 TIL

나고수·2025년 2월 7일
0

2025 TIL

목록 보기
7/11
post-thumbnail

Flutter FCM 연동 삽질 기록 🪓


APNs 인증키 설정 관련

  1. 애플디벨로퍼에서 발급받은 하나의 APNs 인증키를 여러 앱에 쓸 수 있는가? → ⭕️ Yes
  2. 인증키 vs 인증서 차이점
    👉 1,2번에 대한 답은 여기에 잘 나와있음

iOS FCM 설정 관련

iOS FCM 설정방법은 여기에 잘 나와있음

💡 키 ID는 key만드는 곳에서 쉽게 확인 가능
💡 팀 ID는 애플디벨로퍼 → 멤버십 세부사항 → 팀ID에서 확인가능


iOS identifier 설정 관련

xCode에서 Push관련된 설정을 하고, 애플디벨로퍼에서 push Capabilities가 포함된 identifier을 새로 등록 해줘야한다.


FCM 테스트 방법

  1. 가장 쉬운 방법은 FCM 콘솔에서 메시징을 보내는 것이다.
    하지만 data 영역을 넣어서 테스트 해 볼 수 없다는 단점이 있음

  2. 테스트 사이트 이용
    사이트를 이용하면 data 영역도 넣어서 테스트 해 볼 수 있다.
    나는 토큰을 이용해서 테스트했다.


엑세스토큰은 여기를 참고해서 얻는다.

프로젝트ID는 firebase console -> 프로젝트 개요 옆 설정 아이콘 클릭 -> 일반 탭 -> 프로젝트ID에서 찾을 수 있다.

디바이스토큰은 흔히 말하는 fcm토큰을 의미한다.

//이 코드를 통해 얻을 수 있음
//딜레이 1초를 하지 않으면 iOS에서 에러가난다.


  await Firebase.initializeApp(
    options: DefaultFirebaseOptions.currentPlatform,
  );
  
 Future.delayed(Duration(seconds: 1), () async {
    String? fcmToken = await FirebaseMessaging.instance.getToken();
    debugPrint("FCM Token: $fcmToken");
  });
  1. 혹은 여기에 나와있는데로 포스트맨으로 테스트 해볼수도 있음

FCM 관련 플러터 코드

플러터 FCM관련 코드는 봐도봐도 적응이 안된다 너무 복잡함 🤬
os자체도 2개인데다가, 앱 상태(포그라운드, 백그라운드, 터미네이트) 상태, 알림이 수신 되었을때 & 알림을 클릭 했을 때 상황을 모두 고려해야하면 상황이 12가지나 됨.. 🤬

더군다나 안드로이드는 포그라운드일 때 헤드업 알림을 띄우지 않는게 os 설정이라, local_notification 이라는 디펜던시를 이용해서 추가로 포그라운드일 때 헤드업 알림이 보이도록 구현해줘야한다.

말로만 해도 너무 복잡.. 😵‍💫

그래도 이번에 최대한 깔끔하게 정리했고, 삽질한 내용도 최대한 주석으로 정리했다.

//main.dart

//어떤 클래스에 속한 함수가 아니라 최상단 함수로 구현해야함 
('vm:entry-point')
Future < void > handleFcmBackgroundMessage(RemoteMessage message) async {
 debugPrint('$debuggingLog handleFcmBackgroundMessage: $message');
 //iOS는 알림이 올때 뱃지를 자동으로 구현해 주지 않기 때문에, 여기서 
 //flutter_app_badge_control 이 디펜던시를 활용해 직접 구현해줘야함 
 // 그건 다음번에.. 🥹
}

Future < void > main() async {
 WidgetsFlutterBinding.ensureInitialized();

 //initialize firebase
 await Firebase.initializeApp(
   options: DefaultFirebaseOptions.currentPlatform,
 );

 // Future.delayed(Duration(seconds: 1), () async {
 //   String? fcmToken = await FirebaseMessaging.instance.getToken();
 //   debugPrint("FCM Token: $fcmToken");
 // });

 //initialize fcm
 FcmService().initializeFcmService();
 // [안드로이드 - 백그라운드, 터미네이트 / iOS - 백그라운드, 터미네이트] 알림을 수신할 때(클릭X) 호출됨
 // main()함수 안에 있어야 제대로 작동함
 /* 서버에서 아래와 같은 형식으로 알림을 보내야 작동함
 * apns: {
   payload: {
       aps: {
           'mutable-content': 1,
           'content-available': 1
       }
   }
}*/
 FirebaseMessaging.onBackgroundMessage(handleFcmBackgroundMessage);

 runApp(const MyApp());
}
//fcm_util.dart 

/* [Android]
* 포그라운드 알림 수신 -> displayLocalNotification
* 포그라운드 알림 클릭 -> _handleLocalNotificationTap
*
* 백그라운드 알림 수신 -> handleFcmBackgroundMessage
* 백그라운드 알림 클릭 -> _handleFcmMessageOpenedApp
*
* 터미네이트 알림 수신 -> handleFcmBackgroundMessage
* 터미네이트 알림 클릭 -> _handleFcmInitialMessage
*
* */

/* [iOS]
* 포그라운드 알림 수신 -> displayLocalNotification
* 포그라운드 알림 클릭 -> _handleFcmMessageOpenedApp
*
* 백그라운드 알림 수신 -> handleFcmBackgroundMessage
* 백그라운드 알림 클릭 -> _handleFcmMessageOpenedApp
*
* 터미네이트 알림 수신 -> handleFcmBackgroundMessage
* 터미네이트 알림 클릭 -> _handleFcmInitialMessage
*
* */

bool isLocalNotificationInitialized = false;
late AndroidNotificationChannel notificationChannel;
late FlutterLocalNotificationsPlugin localNotificationPlugin;
late FirebaseMessaging fcmInstance;

class FcmService {
 Future < void > initializeFcmService() async {
   fcmInstance = FirebaseMessaging.instance;

   //////////////로컬 알림 초기화 및 설정//////////////
   localNotificationPlugin = FlutterLocalNotificationsPlugin();

   if (isLocalNotificationInitialized) {
     return;
   }

   // 알림 채널 설정
   notificationChannel =
     const AndroidNotificationChannel(
       'channelID',
       'channelName',
       description: 'This is channel description.',
       importance: Importance.high,
     );

   // 알림 채널 생성
   await localNotificationPlugin
     .resolvePlatformSpecificImplementation <
     AndroidFlutterLocalNotificationsPlugin > ()
     ?.createNotificationChannel(notificationChannel);

   isLocalNotificationInitialized = true;

   // 알림 설정
   await localNotificationPlugin.initialize(
     const InitializationSettings(
         android: AndroidInitializationSettings('@mipmap/ic_launcher'),
         iOS: DarwinInitializationSettings(),
       ),
       onDidReceiveNotificationResponse: _handleLocalNotificationTap,
   );
   //////////////////////////

   // FCM 설정
   await _configureFcmSettings();

   // FCM 메시지 핸들러 설정
   _setupFcmMessageHandlers();
 }

 /// FCM 기본 설정
 Future < void > _configureFcmSettings() async {
   // FCM 포그라운드 알림 설정
   await FirebaseMessaging.instance
     .setForegroundNotificationPresentationOptions(
       alert: true,
       badge: true,
       sound: true,
     );

   // FCM 권한 요청
   await fcmInstance.requestPermission();
   fcmInstance.setAutoInitEnabled(true);
 }

 // [안드로이드 - 포그라운드] (로컬)알림을 클릭했을 때 호출됨
 void _handleLocalNotificationTap(NotificationResponse response) {
   debugPrint('$debuggingLog _handleLocalNotificationTap: $response');
 }

 /// FCM 메시지 핸들러 설정
 void _setupFcmMessageHandlers() {
   // [안드로이드 - 포그라운드 / iOS - 포그라운드] 알림을 수신할 때(클릭X) 호출됨
   FirebaseMessaging.onMessage.listen(displayLocalNotification);

   // [안드로이드 - 백그라운드 / iOS - 포그라운드, 백그라운드] 알림을 클릭했을 때 호출됨
   FirebaseMessaging.onMessageOpenedApp.listen(_handleFcmMessageOpenedApp);

   //[안드로이드 - 터미네이트 / iOS - 터미네이트] 알림을 클릭했을 때 호출됨
   // 안드로이드,iOS - 알림을 클릭해서 앱이 시작된것이 아니라 터미네이트 상태에서 런쳐아이콘을 클릭해 앱을 시작한 경우에도 호출됨 -> RemoteMessage null로 들어옴
   fcmInstance.getInitialMessage().then(_handleFcmInitialMessage);
 }

 void _handleFcmMessageOpenedApp(RemoteMessage message) async {
   removeIosBadge();
   debugPrint('$debuggingLog _handleFcmMessageOpenedApp: $message');
 }

 void _handleFcmInitialMessage(RemoteMessage ? message) async {
   removeIosBadge();
   debugPrint('$debuggingLog _handleFcmInitialMessage: $message');
 }

 removeIosBadge() {
   FlutterAppBadgeControl.removeBadge();
 }
}

/// 로컬 알림 표시 함수
// 안드로이드만 포그라운드 시 localNotification으로 알림을 표시하면됨
// iOS는 localNotification 사용 시 알림이 두번 뜸
Future < void > displayLocalNotification(RemoteMessage message) async {
 debugPrint('$debuggingLog displayLocalNotification: $message');
 RemoteNotification ? notification = message.notification;
 AndroidNotification ? android = message.notification?.android;

 if (notification != null && android != null) {
   http.Response ? response;
   ByteArrayAndroidBitmap ? notificationImage;
   StyleInformation ? imageStyle;

   // 이미지가 있는 경우 처리
   if (android.imageUrl != null) {
     response = await http
       .get(Uri.parse(message.notification!.android!.imageUrl ?? ''));
     notificationImage = ByteArrayAndroidBitmap.fromBase64String(
       base64Encode(response.bodyBytes));
     imageStyle = BigPictureStyleInformation(
       notificationImage,
       largeIcon: notificationImage,
       hideExpandedLargeIcon: true,
     );
   }

   // 로컬 알림 표시
   localNotificationPlugin.show(
     notification.hashCode,
     notification.title,
     notification.body,
     NotificationDetails(
       android: AndroidNotificationDetails(
         notificationChannel.id,
         notificationChannel.name,
         channelDescription: notificationChannel.description,
         icon: '@mipmap/ic_launcher',
         largeIcon: notificationImage,
         priority: Priority.high, // 이부분을 high이상으로 설정해야 head-up 알림으로 작동함 
         importance: Importance.high, // 이부분을 high이상으로 설정해야 head-up 알림으로 작동함 
         styleInformation: imageStyle,
       )
     ),
     payload: message.data.toString(),
   );
 }
}
profile
되고싶다

0개의 댓글