[Flutter] Android에서 FCM 연동(1) - 푸시 알림 수신 테스트

구현 목표

플러터로 만든 안드로이드 앱에서 매일 특정 시간에 DB를 조회해서

조건 충족 시 푸시 알림을 보내는 기능을 구현해보려고 한다.

 

우선 이전 글에서 Flutter와 Firebase 기초 연동까지 구현했다.

 

[Flutter] Android 앱에 Firebase 연동 기초 설정

Firebase 프로젝트 생성 및 앱 등록Firebase에서 프로젝트를 생성하고 안드로이드 앱을 등록하면 `google-services.josn` 파일을 다운 받을 수 있다.이 파일을 `android\app\google-services.json` 디렉토리로 이동한

divheer.tistory.com

 

이번에는 FCM 토큰을 등록해서 알림을 수신할 수 있도록 간단하게 테스트를 해 보자.

 

FCM 메시지 수신 시 기기 상태

상태 설명
Foreground 애플리케이션이 열려 있고 보기 상태이며 사용 중인 경우
Background 애플리케이션이 열려 있지만 백그라운드에 있는 경우(최소화). 일반적으로 사용자가 기기에서 '홈' 버튼을 누르거나 앱 전환기를 사용하여 다른 앱으로 전환했거나 다른 탭(웹)에서 애플리케이션을 열어 둔 경우에 발생함
Terminated 기기가 잠겨 있거나 애플리케이션이 실행되고 있지 않은 경우

FCM에서 받은 메시지는 기기의 상태에 따라 다르게 처리된다.

 

백그라운드 상태에서 받는  알림은 앱에 바로 표시되는데, 

앱이 열려 있는 상태에서 도착하는 알림은 Android가 자동으로 표시해주지 않는다.

따라서 포그라운드 상태에서 받는 알림은 패키지를 이용해 수동으로 표시해주어야 한다.

 

알림 수신 권한 요청

FirebaseMessaging messaging = FirebaseMessaging.instance;

NotificationSettings settings = await messaging.requestPermission(
  alert: true,
  announcement: false,
  badge: true,
  carPlay: false,
  criticalAlert: false,
  provisional: false,
  sound: true,
);

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

Android 13 이상의 경우 FCM 메시지가 기기에 수신될 수 있도록 하려면

먼저 사용자에게 알림 수신 권한을 요청해야 한다.

`firebase_messaging` 패키지에서 제공하는 `requestPermission` 메서드를 통해 간단하게 권한을 요청할 수 있다.

 

앱에 들어가면 자동으로 알림 수신 권한을 요청하는 다이얼로그가 뜬다.

 

FCM 등록 토큰 발급받기

FirebaseMessaging messaging = FirebaseMessaging.instance;
String? token = await messaging.getToken(); 

FCM 등록 토큰은 클라이언트 앱 인스턴스를 고유하게 식별하는 문자열로,

토큰을 등록해야 FCM 메시지를 수신할 수 있다.

 

포그라운드 알림 수신

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}');
  }
});

애플리케이션이 포그라운드에 있는 동안 알림을 수신한다.

하지만 이 상태에서는 메시지가 콘솔에만 출력되고, 실제로 기기 상단에 알이 표시지는 않는다.

 

flutter pub add flutter_local_notifications

포그라운드 상태에서도 실제로 알을 띄우고 싶다면,

`flutter_local_notifications` 패키지를 함께 사용해서 직접 알림을 띄워줘야 한다.

 

import 'package:flutter_local_notifications/flutter_local_notifications.dart';

final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
    FlutterLocalNotificationsPlugin();

void _initLocalNotification() async {
  const AndroidInitializationSettings initializationSettingsAndroid =
      AndroidInitializationSettings('@mipmap/ic_launcher');

  const InitializationSettings initializationSettings =
      InitializationSettings(android: initializationSettingsAndroid);

  await flutterLocalNotificationsPlugin.initialize(initializationSettings);
}

void _showNotification(RemoteMessage message) async {
  const AndroidNotificationDetails androidNotificationDetails =
      AndroidNotificationDetails(
    'fcm_default_channel', // 채널 ID
    'FCM 알림', // 채널 이름
    channelDescription: '포그라운드 FCM 알림',
    importance: Importance.max,
    priority: Priority.high,
    ticker: 'ticker',
  );

  const NotificationDetails notificationDetails =
      NotificationDetails(android: androidNotificationDetails);

  await flutterLocalNotificationsPlugin.show(
    0,
    message.notification?.title ?? '알림',
    message.notification?.body ?? '메시지가 도착했습니다.',
    notificationDetails,
  );
}

  @override
  void initState() {
    super.initState();
    _initFCM();
    _initLocalNotification(); // 추가
  }
  
      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}');
        _showNotification(message); // 추가
      }
    });

포그라운드 상태에서도 FCM의 메시지 내용을 받아서 알림을 띄워주는 코드를 작성했다.

코드의 전체적인 구조는 하단에 전체 코드에서 확인하면 된다.

 

포그라운드 알림 수신 결과물!

 

백그라운드 알림 수신

Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
  print("Handling a background message: ${message.messageId}");
}

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp(
    options: DefaultFirebaseOptions.currentPlatform,
  );
  FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);
  runApp(const MyApp());
}

백그라운드에서 수신한 알림은 앱에 바로 표시되므로, 따로 처리할 필요가 없다.

다만 백그라운드 메시지 핸들러와 관련하여 몇 가지 주의할 점이 있다.

  1. 익명 함수가 아니어야 한다.
  2. 최상위 수준 함수여야 한다.
  3. Flutter 버전 3.3.0 이상을 사용하는 경우 메시지 핸들러는 함수 선언 바로 위에 `@pragma('vm:entry-point')`로 주석을 달아야 합니다(그렇지 않으면 출시 모드의 경우 트리 쉐이킹 중에 삭제될 수 있음).

 

FCM에서 테스트 알림 보내기

Firebase Messaging 페이지에서 [첫 번째 캠페인 만들기] 버튼을 클릭한다.

 

[Firebase 알림 메시지]를 선택한다.

 

`FCM 등록 토큰`을 추가해서 테스트 메시지를 전송하면 

FCM이 잘 연동되었는지 확인할 수 있다.

 

전체 코드

// main.dart
import 'package:flutter/material.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'firebase_options.dart';

final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
    FlutterLocalNotificationsPlugin();

// 로컬 알림 초기화
void _initLocalNotification() async {
  const AndroidInitializationSettings initializationSettingsAndroid =
      AndroidInitializationSettings('@mipmap/ic_launcher');

  const InitializationSettings initializationSettings =
      InitializationSettings(android: initializationSettingsAndroid);

  await flutterLocalNotificationsPlugin.initialize(initializationSettings);
}

// 포그라운드 알림 표시
void _showNotification(RemoteMessage message) async {
  const AndroidNotificationDetails androidNotificationDetails =
      AndroidNotificationDetails(
    'fcm_default_channel', // 채널 ID
    'FCM 알림', // 채널 이름
    channelDescription: '포그라운드 FCM 알림',
    importance: Importance.max,
    priority: Priority.high,
    ticker: 'ticker',
  );

  const NotificationDetails notificationDetails =
      NotificationDetails(android: androidNotificationDetails);

  await flutterLocalNotificationsPlugin.show(
    0,
    message.notification?.title ?? '알림',
    message.notification?.body ?? '메시지가 도착했습니다.',
    notificationDetails,
  );
}

// 백그라운드 알림 리스너
@pragma('vm:entry-point')
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
  print("Handling a background message: ${message.messageId}");
}

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp(
    options: DefaultFirebaseOptions.currentPlatform,
  );
  FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);
  runApp(const MyApp());
}

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

  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  String? _token = '';

  @override
  void initState() {
    super.initState();
    _initLocalNotification();
    _initFCM();
  }

  void _initFCM() async {
    FirebaseMessaging messaging = FirebaseMessaging.instance;

    // 권한 요청 (Android 13 이상)
    NotificationSettings settings = await messaging.requestPermission();
    print('User granted permission: ${settings.authorizationStatus}');

    // 포그라운드 알림 리스너
    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}');
        _showNotification(message);
      }
    });

    // 토큰 확인
    String? token = await messaging.getToken();
    print('token: $token');
    setState(() {
      _token = token;
    });
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: const Text('FCM 테스트')),
        body: Center(
          child: SelectableText('FCM 토큰:\n$_token'),
        ),
      ),
    );
  }
}

 

플러터 FCM 연동 시리즈

 

참고 문서