[Flutter] FCM을 통해 특정 사용자에게 푸시 알림 전송하기


이슈

특정 액션이 있을 때마다 특정 사용자에게 푸시 알림을 보내고 싶다. 어떻게 구현할 수 있을까?


해결

라이브러리

FCM

Firebase에서 Firebase Cloud Message, 일명 FCM이라는 서비스로 Android/iOS 애플리케이션에 대한 푸시 알림 서비스를 지원하고 있다. 푸시 알림의 대상은 다음과 같은 옵션들로 지정할 수 있다.

  • 전체 사용자
  • 특정 그룹의 사용자
  • 특정 사용자

또한 이러한 대상들에 대하여 시간을 예약하거나 알림을 보낼 수도 있고 특정 액션이 있을 때마다 전송할 수도 있다. 본 포스트에서는 특정 사용자에게 특정 액션이 발생할 때 푸시 알림을 보낼 것이다.

푸시 알림 요청은 클라이언트 단에서 Firebase Console로 보낼 수도 있고, 자체 개발 서버가 있다면 서버로 API 요청을 한 후, 서버에서 firebase admin 라이브러리를 통해 푸시 알림을 보내는 방식이 있다.

FCM을 사용하려면 AndroidiOS 별로 사용하는 플랫폼에 따라 Firebase Console에 접속하여 프로젝트 내에 설정해주어야 할 사항이 있다. 이 내용은 Firebase Console에서 쉽게 확인할 수 있고 정리된 타 블로그 포스트들도 많으므로 생략한다. 그리고 본 포스트에서는 Android 플랫폼에서의 구현과 설명을 주로 한다.


코드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
import 'package:dio/dio.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';

void fcmSetting() async {
  // firebase core 기능 사용을 위한 필수 initializing
  await Firebase.initializeApp();

  await FirebaseMessaging.instance.requestPermission(
      alert: true,
      announcement: true,
      badge: true,
      carPlay: true,
      criticalAlert: true,
      provisional: true,
      sound: true);
  
  // foreground에서의 푸시 알림 표시를 위한 알림 중요도 설정
  const AndroidNotificationChannel channel = AndroidNotificationChannel(
    'high_importance_channel',
    'High Importance Notifications',
    description: 'This channel is used for important notifications.',
    importance: Importance.max,
  );

  // foreground에서의 푸시 알림 표시를 위한 local notifications 설정
  final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
      FlutterLocalNotificationsPlugin();
  await flutterLocalNotificationsPlugin
      .resolvePlatformSpecificImplementation<
          AndroidFlutterLocalNotificationsPlugin>()
      ?.createNotificationChannel(channel);

  await flutterLocalNotificationsPlugin.initialize(
      InitializationSettings(
          android: AndroidInitializationSettings('@mipmap/launcher_icon'),
          iOS: IOSInitializationSettings()),
      onSelectNotification: (String? payload) async {});

  // foreground 푸시 알림 핸들링
  FirebaseMessaging.onMessage.listen((RemoteMessage message) {
    RemoteNotification? notification = message.notification;
    AndroidNotification? android = message.notification?.android;
    if (notification != null && android != null) {
      flutterLocalNotificationsPlugin.show(
          notification.hashCode,
          notification.title,
          notification.body,
          NotificationDetails(
            android: AndroidNotificationDetails(
              channel.id,
              channel.name,
              channelDescription: channel.description,
              icon: android.smallIcon,
            ),
          ));
    }
  });
  
  // 사용자가 푸시 알림을 허용했는지 확인 (optional)
  SharedPreferences prefs = await SharedPreferences.getInstance();

  final isFCMEnabled = await prefs.getBool('FCM_ENABLED');
    if (isFCMEnabled == null || isFCMEnabled) {
      // firebase token 발급
      String? firebaseToken = await FirebaseMessaging.instance.getToken();
      
      // 서버로 firebase token 갱신
      if (firebaseToken != null) {
        var dio = Dio();
        final firebaseTokenUpdateResponse = await dio.put(
            '/token',
            data: {'token': firebaseToken});
      }
    }
}

위 코드는 FCM 관련 초기 설정을 하는 코드로, 앱 구동 시에 맨 앞 단에서 실행시켜줘야 한다. 그런데 요러한 초기 세팅 코드는 다양한 블로그나 심지어 공식 FCM 사이트에서도 관련 내용을 살펴볼 수 있는데 하나같이 오류가 나거나 제대로 적용이 안됐다. Flutter 에서 FCM은 아직도 업데이트가 활발한 듯 하다. 우선 flutter 2.2.3 버전 기준으로 위 코드를 그대로 복붙만 해도 백엔드 단의 코드 작성만 완료해주면 foregroundbackground에서 성공적으로 푸시 알림을 수신할 수 있다.


고찰

Background vs Foreground

앱을 통해 기기에 푸시 알림이 오는 상황을 다음과 같이 두 가지로 구분할 수 있다.

  • 해당 앱이 foreground로 실행 중일 때
  • 해당 앱이 background로 실행 중이거나 꺼져있을 때

위 두 가지 사항에 대하여 FCM은 각기 다른 핸들러를 적용한다. 이 때 background 상황이나 꺼져있는 상황에서는 별도의 핸들러를 구현하지 않고 FCMinitializing만 해줘도 푸시 알림을 수신할 수 있다. 그런데 foreground의 경우, 즉 앱이 기기 화면의 최상단에서 실행 중일 경우 별도의 핸들러를 구현하고 라이브러리를 사용해야 한다.

다만 foreground에서의 경우 또 한가지 특징이 있는데, 바로 알림의 중요도이다. Android에서는 내부적으로 다음과 같은 푸시 알림 정책을 갖고 있다.

앱이 foreground일 때는 이미 앱을 사용하고 있으니 굳이 알림을 안보내도 되겠다!

이 때 알림의 중요도를 MAX로 설정해주면 내부적으로 위와 같은 정책의 예외 사항으로 보고 앱이 foreground인 상황에서도 정상적으로 푸시 알림을 화면과 상단바 알림에 표시해준다.

Flutter Local Notifications

이 라이브러리는 다양한 플랫폼 별로 푸시 알림을 띄워주는 라이브러리이다. AndroidiOS는 물론, Windows, Linux, MacOS와 같은 desktop 기반 OS도 지원한다! Foreground에서의 푸시 알림 수신에서 본 라이브러리를 사용해 알림을 표시해준다. 알림의 제목과 내용, 아이콘을 설정할 수 있으며 자세한 설정 방법은 위 코드에서 확인하거나 공식 문서에서 확인할 수 있다.

Firebase Token

그렇다면 어떻게 사용자 또는 기기를 특정할 수 있을까? 바로 firebase token을 사용한다. 이 토큰의 특징은 다음과 같다.

  • 기기 별로 토큰이 발급된다
  • 앱을 재설치하거나 업데이트할 경우 토큰 값이 변경된다
  • 만료 기간이 있으며 refresh 가능하다

토큰이 기기 별로 발급된다고 했는데, 사실상 동일 기기에서 같은 패키지명을 갖는 앱을 여러개 설치할 순 없으므로 앱 당 발급된다고 봐도 무방하다. 즉, 앱 내에서의 서비스 계정에 따라 발급하는 토큰이 아님에 주의한다.

따라서 앱 최초 실행 시에 firebase token을 발급하고 서버로 전달하여 DB 상에서 사용자 계정과 연결해주어야 한다. 이 때 토큰은 앱을 지웠다 깔거나 만료 기간이 되었을 때 갱신되는데, 토큰이 만료되면 firebase에 그에 따른 refresh 요청을 따로 보내지 않아도 이후부터 알아서 갱신된 토큰으로 발급해준다. 따라서 위 코드와 같이 매 번 앱 실행 시마다 앞 단에서 FCM 세팅을 함과 동시에 firebase token을 갱신해주는 것도 한 가지 방법이다. 앱 실행 시마다 네트워크 통신을 하는 것이 부담스럽다면 Flutter Secure Storage와 같은 보안 내부 저장소에 토큰을 저장해둘 수도 있다. 다만 토큰 갱신이 제 때 이루어지지 않아 푸시 알림의 누락이 발생할 수도 있다.

0%