사용자 참여를 유도하고 중요한 정보를 적시에 전달하는 푸시 알림은 현대 모바일 앱의 필수 기능입니다. Flutter 생태계에서는 Google의 강력한 백엔드 서비스인 Firebase Cloud Messaging(FCM)과, 기기 자체에서 알림을 유연하게 제어할 수 있는 flutter_local_notifications 패키지의 조합이 가장 이상적인 해결책으로 꼽힙니다. 하지만 이 두 가지 강력한 도구를 매끄럽게 연동하는 과정은 생각보다 많은 함정을 내포하고 있습니다. 특히 플랫폼별 설정의 미묘한 차이와 버전 업데이트에 따른 변경 사항들은 개발자들을 혼란에 빠뜨리곤 합니다.
이 글에서는 단순히 '따라하기' 식의 가이드를 넘어, 왜 FCM과 로컬 알림을 함께 사용해야 하는지에 대한 근본적인 이유부터 시작하여, 최신 Flutter 및 Firebase 버전에 맞는 가장 안정적이고 권장되는 설정 방법을 상세히 다룹니다. Firebase 프로젝트 설정부터 안드로이드와 iOS의 플랫폼별 네이티브 설정, 다양한 앱 상태(포그라운드, 백그라운드, 종료 상태)에 따른 메시지 처리, 그리고 개발자들이 가장 많이 겪는 문제들에 대한 해결책까지, 푸시 알림 기능 구현에 필요한 모든 것을 총망라하여 제공합니다. 이 가이드를 끝까지 따라오신다면, 여러분의 Flutter 앱에 안정적이고 사용자 친화적인 푸시 알림 시스템을 자신 있게 구축할 수 있을 것입니다.
1. 왜 FCM과 Flutter Local Notifications를 함께 사용해야 할까?
푸시 알림 구현을 시작하기 전에, 왜 firebase_messaging
만으로는 부족하고 flutter_local_notifications
를 추가로 사용해야 하는지 이해하는 것이 중요합니다. 각 패키지의 역할과 시너지를 알면 전체 구조를 설계하는 데 큰 도움이 됩니다.
-
Firebase Cloud Messaging (FCM) -
firebase_messaging
: 서버에서 클라이언트(앱)로 푸시 메시지를 보내는 파이프라인 역할을 합니다. Google의 인프라를 통해 안정적으로 메시지를 전달받는 핵심 통로입니다. 앱이 백그라운드나 종료 상태일 때 수신된 'notification' 페이로드를 통해 기본적인 시스템 알림을 자동으로 표시해 줄 수 있습니다. 하지만 앱이 활성화된 상태(포그라운드)에서는 알림이 자동으로 표시되지 않으며, 표시되는 알림의 디자인이나 동작을 세밀하게 커스터마이징하는 데 한계가 있습니다. -
로컬 알림 -
flutter_local_notifications
: 서버 연동 없이 앱 코드 내에서 직접 알림을 생성하고 관리하는 역할을 합니다. 즉, 기기 자체에서 알림을 만드는 도구입니다. 이를 통해 알림 채널(Android), 커스텀 사운드, 중요도, 진행률 표시줄, 액션 버튼 등 매우 풍부하고 사용자 정의된 알림을 만들 수 있습니다.
이 둘의 시너지는 다음과 같은 시나리오에서 극대화됩니다.
- 포그라운드(Foreground) 상태에서의 일관된 경험 제공: 사용자가 앱을 활발하게 사용 중일 때 FCM 메시지가 도착하면, 이는 데이터 형태로만 전달됩니다. 이때
firebase_messaging
의onMessage
스트림에서 이 데이터를 받아,flutter_local_notifications
를 사용하여 사용자에게 시각적인 알림을 즉시 보여줄 수 있습니다. 이를 통해 사용자는 어떤 상태에서든 일관된 형태의 알림을 경험하게 됩니다. - 고도로 커스터마이징된 알림: FCM은 메시지 '전달'에 집중하고, '표시'는
flutter_local_notifications
에 위임하는 구조입니다. 서버에서 보낸 데이터(예: 이미지 URL, 특정 페이지로 이동하기 위한 정보)를 기반으로 로컬 알림 패키지를 사용해 사진이 포함된 알림, '답장'이나 '자세히 보기' 같은 버튼이 있는 알림 등 풍부한 형태의 알림을 구현할 수 있습니다. - 플랫폼별 정책 대응: 특히 Android에서는 알림 채널(Notification Channel) 설정이 필수적입니다.
flutter_local_notifications
를 사용하면 이러한 채널을 손쉽게 생성하고 관리하여 사용자가 알림 종류별로 수신 여부를 선택할 수 있게 하는 등 최신 OS 정책에 유연하게 대응할 수 있습니다.
결론적으로, FCM을 '우편 배달부'로, 로컬 알림을 '예쁜 편지지를 만드는 디자이너'로 비유할 수 있습니다. 배달부가 가져온 소식(데이터)을 디자이너가 멋지게 꾸며 사용자에게 최종적으로 보여주는, 역할 분담을 통한 완벽한 시스템을 구축하는 것이 우리의 목표입니다.
2. 1단계: Firebase 프로젝트 설정 및 Flutter 연동
본격적인 코드 작성에 앞서, 우리의 Flutter 프로젝트와 Firebase를 연결하는 준비 작업이 필요합니다. 최신 개발 환경에서는 Firebase CLI(Command Line Interface)를 사용하는 것이 가장 효율적이고 실수를 줄이는 방법입니다.
2.1. Firebase 프로젝트 생성하기
가장 먼저, 알림을 보내고 관리할 Firebase 프로젝트가 필요합니다.
- Firebase 콘솔로 이동하여 Google 계정으로 로그인합니다.
- '프로젝트 추가' 버튼을 클릭합니다.
- 프로젝트 이름을 입력합니다. (예: 'My Awesome App')
- (선택 사항) 이 프로젝트에 Google 애널리틱스를 사용할지 결정합니다. 일반적으로 사용하는 것이 데이터 분석에 유리합니다. '계속'을 클릭하고 애널리틱스 계정을 선택하거나 새로 만듭니다.
- '프로젝트 만들기'를 클릭하고 잠시 기다리면 새로운 Firebase 프로젝트가 생성됩니다.
이제 이 Firebase 프로젝트에 우리의 Flutter 앱을 등록할 차례입니다.
2.2. FlutterFire CLI 설치 및 Flutter 프로젝트 연결
과거에는 플랫폼별(Android, iOS)로 설정 파일을 직접 다운로드하여 프로젝트에 추가하는 번거로운 과정을 거쳐야 했습니다. 하지만 이제는 FlutterFire CLI
를 통해 이 모든 과정을 자동화할 수 있습니다.
-
Firebase CLI 설치: 터미널을 열고 아래 명령어를 실행하여 Firebase CLI를 설치합니다. (이미 설치되어 있다면 이 단계를 건너뛰세요.)
npm install -g firebase-tools
-
Firebase 로그인: 다음 명령어로 Firebase 계정에 로그인합니다. 브라우저 창이 열리면 로그인할 계정을 선택하세요.
firebase login
-
FlutterFire CLI 활성화: Dart의 전역 패키지로
flutterfire_cli
를 활성화합니다.dart pub global activate flutterfire_cli
-
Flutter 프로젝트 연결: 이제 여러분의 Flutter 프로젝트 최상위 디렉토리에서 아래 명령어를 실행합니다.
이 명령어를 실행하면, CLI가 여러분의 Firebase 계정에 있는 프로젝트 목록을 보여줍니다. 위에서 생성한 프로젝트를 선택하세요. 그 후, 어떤 플랫폼(android, ios, macos, web)에 대해 설정을 진행할지 묻습니다. 필요한 플랫폼을 선택(스페이스바로 선택, 엔터로 확인)하면 CLI가 자동으로 각 플랫폼에 맞는 설정 파일(flutterfire configure
google-services.json
for Android,GoogleService-Info.plist
for iOS)을 다운로드하고, 프로젝트에 필요한 네이티브 코드 수정을 일부 진행해줍니다.
이 과정이 성공적으로 완료되면, lib/
디렉토리에 firebase_options.dart
파일이 생성된 것을 확인할 수 있습니다. 이 파일은 각 플랫폼에 맞는 Firebase 프로젝트 식별 정보를 담고 있으며, 앱 초기화 시 사용됩니다. 절대 이 파일을 수동으로 수정하지 마세요.
2.3. 필요한 Flutter 패키지 설치하기
이제 pubspec.yaml
파일에 푸시 알림 연동에 필요한 패키지들을 추가할 차례입니다. 또는 터미널에서 다음 명령어를 실행해도 됩니다.
flutter pub add firebase_core
flutter pub add firebase_messaging
flutter pub add flutter_local_notifications
firebase_core
: 모든 Firebase 플러그인의 기반이 되는 핵심 패키지입니다.firebase_messaging
: FCM 메시지를 수신하고 상호작용하기 위한 메인 패키지입니다.flutter_local_notifications
: 수신한 FCM 데이터를 바탕으로 사용자 정의 알림을 표시하기 위한 패키지입니다.
패키지 추가 후에는 IDE의 팝업을 통해 또는 터미널에서 flutter pub get
명령어를 실행하여 의존성을 설치해주세요.
2.4. 앱의 진입점(main.dart)에서 Firebase 초기화하기
앱이 시작될 때 Firebase 서비스가 Flutter 엔진보다 먼저 초기화되도록 설정해야 합니다. main()
함수를 다음과 같이 수정합니다.
import 'package:flutter/material.dart';
import 'package:firebase_core/firebase_core.dart';
import 'firebase_options.dart'; // flutterfire configure로 생성된 파일
void main() async {
// Flutter 엔진과 위젯 바인딩이 초기화되었는지 확인
// Firebase.initializeApp()을 호출하기 전에 반드시 필요합니다.
WidgetsFlutterBinding.ensureInitialized();
// 플랫폼별 Firebase 설정을 로드하여 Firebase 앱을 초기화
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform,
);
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'FCM Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(),
);
}
}
class MyHomePage extends StatelessWidget {
const MyHomePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Flutter FCM & Local Notification'),
),
body: const Center(
child: Text('푸시 알림 테스트 준비 완료!'),
),
);
}
}
WidgetsFlutterBinding.ensureInitialized();
는 runApp()
이 호출되기 전에 Flutter 프레임워크의 여러 기능을 사용해야 할 때(플랫폼 채널 통신 등) 반드시 먼저 호출되어야 합니다. Firebase.initializeApp()
이 내부적으로 플랫폼 채널을 사용하므로 이 라인은 필수입니다.
3. 2단계: 플랫폼별 상세 설정 (Android & iOS)
기본적인 Flutter 프로젝트 설정이 끝났습니다. 이제 각 모바일 운영체제의 특성에 맞는 네이티브 설정을 진행해야 합니다. 이 단계에서의 작은 실수가 알림이 동작하지 않는 주된 원인이 되므로 주의 깊게 따라와 주세요.
3.1. Android 플랫폼 설정
안드로이드는 비교적 설정이 간단한 편입니다. flutterfire configure
가 대부분의 작업을 처리해주지만, 몇 가지 확인하고 추가해야 할 부분이 있습니다.
-
Google Services 플러그인 확인:
android/build.gradle
파일에google-services
클래스패스가 추가되었는지 확인합니다.// android/build.gradle buildscript { // ... dependencies { // ... classpath 'com.google.gms:google-services:4.4.1' // 버전은 다를 수 있음 } }
android/app/build.gradle
파일 최하단에google-services
플러그인이 적용되었는지 확인합니다.// android/app/build.gradle // ... apply plugin: 'com.google.gms.google-services'
-
AndroidManifest.xml 설정:
android/app/src/main/AndroidManifest.xml
파일은 안드로이드 앱의 모든 구성요소를 정의하는 중요한 파일입니다. 여기에 몇 가지 설정을 추가해야 합니다.-
인터넷 권한: FCM이 서버와 통신하려면 인터넷 권한이 필요합니다.
<manifest>
태그 바로 아래에 추가합니다.<uses-permission android:name="android.permission.INTERNET"/>
-
기본 알림 채널 ID (중요): Android 8.0(Oreo) 이상에서는 모든 알림이 '채널'에 속해야 합니다. FCM 메시지에 별도의 채널 정보가 없을 때 사용할 기본 채널 ID를 지정해주는 것이 좋습니다. 이 ID는 나중에
flutter_local_notifications
에서 채널을 생성할 때 사용할 ID와 일치시켜야 합니다.<application>
태그 내부에 추가합니다.<meta-data android:name="com.google.firebase.messaging.default_notification_channel_id" android:value="high_importance_channel" />
-
기본 알림 아이콘 및 색상 (브랜딩에 필수): FCM이 백그라운드에서 자동으로 알림을 표시할 때 사용할 아이콘과 색상을 지정합니다. 여기서 아이콘을 지정하지 않으면 회색 사각형으로 표시될 수 있습니다.
<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/ic_notification_color" />
위 설정에 사용된 리소스를 생성해야 합니다.
- 아이콘(
ic_notification.xml
):android/app/src/main/res/drawable/
디렉토리에 생성합니다. 안드로이드 상태 표시줄 아이콘은 반드시 흰색 또는 단색이어야 하며, 배경은 투명해야 합니다. 컬러 아이콘은 시스템에 의해 자동으로 단색 처리됩니다. Android Studio의 'Vector Asset' 생성 도구를 사용하면 편리합니다. - 색상(
colors.xml
):android/app/src/main/res/values/
디렉토리에colors.xml
파일을 만들고 색상을 정의합니다. 이 색상은 주로 아이콘의 배경 틴트나 앱 이름 텍스트에 적용됩니다.<?xml version="1.0" encoding="utf-8"?> <resources> <color name="ic_notification_color">#FFFFFF</color> </resources>
- 아이콘(
-
인터넷 권한: FCM이 서버와 통신하려면 인터넷 권한이 필요합니다.
-
Android 13 (API 33) 이상 알림 권한 요청:
Android 13부터는 앱이 알림을 보내기 위해 사용자에게 명시적으로 런타임 권한을 요청해야 합니다. 이 과정 없이는 어떤 알림도 표시되지 않습니다. 이 권한은 보통 앱이 시작될 때 요청하며,
permission_handler
같은 패키지를 사용하면 쉽게 구현할 수 있습니다.먼저 패키지를 추가합니다:
flutter pub add permission_handler
그리고
AndroidManifest.xml
에 권한을 선언합니다.<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
이제 Dart 코드에서 권한을 요청하는 로직을 추가합니다. 이 로직은 나중에 만들 알림 서비스 클래스 내에 위치시키는 것이 좋습니다.
3.2. iOS 플랫폼 설정
iOS 설정은 Android에 비해 조금 더 복잡하며, 많은 개발자들이 어려움을 겪는 구간입니다. Apple의 정책과 시스템 구조에 대한 이해가 필요합니다.
-
Xcode Capability 설정:
- Flutter 프로젝트의
ios
폴더를 Xcode로 엽니다. (Runner.xcworkspace
파일을 여는 것을 권장합니다.) - 왼쪽 네비게이터에서 'Runner' 프로젝트를 선택한 후, 중앙 에디터에서 'Runner' 타겟을 선택합니다.
- 상단의 'Signing & Capabilities' 탭으로 이동합니다.
- '+ Capability' 버튼을 클릭하여 다음 두 가지 기능을 추가합니다.
- Push Notifications: 앱이 원격 알림을 수신할 수 있도록 허용하는 가장 기본적인 설정입니다.
- Background Modes: 앱이 백그라운드 상태에서 특정 작업을 수행할 수 있도록 합니다. 추가 후 'Remote notifications' 항목을 체크합니다. 이것이 있어야 백그라운드 상태에서 수신된 푸시를 처리할 수 있습니다.
- Flutter 프로젝트의
-
Apple Push Notification service (APNs) 인증 키 설정:
Firebase가 Apple의 푸시 알림 서버(APNs)를 통해 iOS 기기로 알림을 보내려면, 인증을 위한 키가 필요합니다. 인증서(.p12) 방식도 있지만, 만료 기간이 없는 인증 키(.p8) 방식이 훨씬 편리하고 권장됩니다.
- Apple 개발자 계정에 로그인합니다.
- 'Certificates, Identifiers & Profiles' 메뉴로 이동합니다.
- 왼쪽 메뉴에서 'Keys'를 선택하고 '+' 버튼을 눌러 새 키를 생성합니다.
- Key Name을 입력하고(예: 'My App APNs Key'), 'Apple Push Notifications service (APNs)' 서비스에 체크합니다.
- 'Continue'와 'Register'를 차례로 클릭합니다.
- 매우 중요: 키 생성 완료 페이지에서 .p8 파일을 딱 한 번만 다운로드할 수 있습니다. 즉시 다운로드하여 안전한 곳에 보관하세요. 또한, 페이지에 표시된 **Key ID**와 페이지 우상단의 계정 이름 아래에 있는 **Team ID**를 복사해둡니다.
- 이제 Firebase 콘솔로 돌아가 프로젝트 설정 > 클라우드 메시징 탭으로 이동합니다.
- 'Apple 앱 구성' 섹션에서 'APN 인증 키'를 업로드하는 곳을 찾아, 방금 다운로드한 .p8 파일과 복사해둔 Key ID, Team ID를 입력하고 '업로드' 버튼을 클릭합니다.
-
Info.plist 설정 및 핵심 문제 해결:
ios/Runner/Info.plist
파일에 키-값 쌍을 추가하여 앱의 동작을 설정합니다. 여기서 많은 개발자들이 겪는 문제가 발생합니다.배경 지식: 메소드 스위즐링(Method Swizzling)
Firebase iOS SDK는 개발자의 편의를 위해 '메소드 스위즐링'이라는 기술을 사용합니다. 이는 앱의 델리게이트 메소드(예: 알림 수신 관련 메소드)를 Firebase의 자체 메소드로 자동으로 바꿔치기하여 알림 관련 작업을 쉽게 처리하도록 돕는 기능입니다.문제점과 해결책
하지만 우리가flutter_local_notifications
와 같은 다른 알림 처리 라이브러리를 함께 사용할 때, 이 자동 바꿔치기 기능은 오히려 충돌을 일으키거나 우리가 원하는 대로 알림을 제어할 수 없게 만드는 원인이 됩니다. 따라서 우리는 Firebase의 이 스위즐링 기능을 끄고, 모든 알림 흐름을 수동으로 직접 제어하는 것이 더 안정적입니다.Info.plist
파일을 열고 다음 키-값 쌍을 추가하여 스위즐링을 비활성화합니다.<key>FirebaseAppDelegateProxyEnabled</key> <string>NO</string>
‼️ 극히 중요한 함정: 많은 공식 문서나 오래된 자료에서 이 값을 불리언(Boolean) 타입인
<false/>
로 설정하라고 안내합니다. 하지만 수많은 실제 사례에서<false/>
는 제대로 동작하지 않는 경우가 빈번하게 보고되었습니다. 가장 확실하고 안정적인 방법은 타입(Type)을 문자열(String)로 설정하고 값(Value)을NO
로 지정하는 것입니다. 이 작은 차이가 몇 시간의 디버깅을 아껴줄 수 있습니다. -
AppDelegate.swift 수정:
메소드 스위즐링을 비활성화했기 때문에, APNs로부터 받은 기기 토큰(device token)을 Firebase SDK에 수동으로 전달해주는 코드를 추가해야 합니다. 이 과정이 없으면 Firebase가 어떤 기기에 알림을 보내야 할지 알 수 없습니다.
ios/Runner/AppDelegate.swift
파일을 열고 다음과 같이 코드를 추가하거나 수정합니다.import UIKit import Flutter import Firebase // Firebase 임포트 추가 @UIApplicationMain @objc class AppDelegate: FlutterAppDelegate { override func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { // Firebase.configure()는 flutterfire configure를 통해 자동으로 처리될 수 있으나, // 명시적으로 호출해주는 것이 안전할 수 있습니다. FirebaseApp.configure() GeneratedPluginRegistrant.register(with: self) // 포그라운드 알림 설정 (iOS 10 이상) if #available(iOS 10.0, *) { UNUserNotificationCenter.current().delegate = self as? UNUserNotificationCenterDelegate } return super.application(application, didFinishLaunchingWithOptions: launchOptions) } // APNs 토큰을 Firebase에 전달하기 위한 코드 override func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { Messaging.messaging().apnsToken = deviceToken super.application(application, didRegisterForRemoteNotificationsWithDeviceToken: deviceToken) } }
iOS 설정이 완료되면, ios
디렉토리에서 pod install --repo-update
명령어를 실행하여 새로 추가된 네이티브 의존성들을 설치해주는 것이 좋습니다.
4. 3단계: Dart 코드 구현 - 로직 통합
모든 플랫폼별 설정이 끝났습니다. 이제 Flutter(Dart) 코드를 작성하여 이 모든 것을 하나로 묶고, 실제로 알림을 수신하고 처리하는 로직을 구현할 차례입니다. 재사용성과 유지보수성을 위해 모든 알림 관련 로직을 별도의 서비스 클래스로 분리하는 것을 강력히 권장합니다.
4.1. 알림 처리 로직 설계
우리가 구현해야 할 핵심 기능들은 다음과 같습니다.
- 사용자에게 알림 수신 권한 요청하기
- FCM 기기 토큰 가져오기 (서버 전송용)
- 앱 상태별 FCM 메시지 처리
- 포그라운드(Foreground): 앱이 화면에 떠 있을 때. (커스텀 알림 표시 필요)
- 백그라운드(Background): 앱이 화면에 없지만 메모리에 살아있을 때.
- 종료(Terminated): 사용자가 앱을 완전히 종료했을 때.
- 사용자가 알림을 탭했을 때 특정 화면으로 이동시키기
이 모든 로직을 관리할 `notification_service.dart` 파일을 생성하고 클래스를 만들어 보겠습니다.
// lib/services/notification_service.dart
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
// 백그라운드/종료 상태에서 메시지를 처리하기 위한 최상위 함수
// 이 함수는 반드시 클래스 외부에, 최상위 레벨에 위치해야 합니다.
@pragma('vm:entry-point')
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
// 백그라운드에서 어떤 작업을 수행해야 한다면 여기에 작성합니다.
// 예를 들어, 수신된 데이터를 로컬 DB에 저장하는 등의 작업을 할 수 있습니다.
// 이 핸들러는 UI 업데이트와 관련된 작업을 직접 수행할 수 없습니다.
print("백그라운드 메시지 수신: ${message.messageId}");
print("데이터: ${message.data}");
}
class NotificationService {
// 싱글톤 패턴으로 인스턴스 관리
NotificationService._privateConstructor();
static final NotificationService _instance = NotificationService._privateConstructor();
factory NotificationService() {
return _instance;
}
final FirebaseMessaging _firebaseMessaging = FirebaseMessaging.instance;
final FlutterLocalNotificationsPlugin _localNotifications = FlutterLocalNotificationsPlugin();
// 여기에 초기화 및 기타 메소드들을 추가할 것입니다.
}
4.2. 초기화 메소드 구현
앱이 시작될 때 단 한 번 호출될 `init()` 메소드를 만들어, 모든 초기 설정을 이곳에서 처리합니다.
// NotificationService 클래스 내부에 추가
Future<void> init() async {
// 1. FCM 권한 요청 (iOS & Android 13+)
await _requestPermission();
// 2. FCM 기본 설정
await _setupFCM();
// 3. 로컬 알림 초기화
await _initLocalNotifications();
// 4. FCM 토큰 가져오기
final fcmToken = await getFcmToken();
print("FCM Token: $fcmToken");
// 이 토큰을 서버로 전송하여 사용자별 푸시를 보낼 수 있습니다.
}
Future<void> _requestPermission() async {
// FCM 권한 요청
await _firebaseMessaging.requestPermission(
alert: true,
announcement: false,
badge: true,
carPlay: false,
criticalAlert: false,
provisional: false,
sound: true,
);
// Android 13+ 알림 권한 요청 (permission_handler 사용 시)
// final status = await Permission.notification.request();
// if (status.isDenied) {
// // 권한 거부 시 처리
// }
}
Future<String?> getFcmToken() async {
return await _firebaseMessaging.getToken();
}
이제 `main.dart`에서 이 초기화 메소드를 호출해줍니다.
// main.dart
// ... (import 문들)
import 'package:my_awesome_app/services/notification_service.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform,
);
// 백그라운드 핸들러 설정
FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);
// 알림 서비스 초기화
await NotificationService().init();
runApp(const MyApp());
}
4.3. 로컬 알림 및 FCM 메시지 핸들러 설정
이제 `NotificationService` 클래스에 가장 중요한 부분인 로컬 알림 설정과 FCM 메시지 리스너를 추가합니다.
// NotificationService 클래스 내부
// 안드로이드 알림 채널 정의
final AndroidNotificationChannel _channel = const AndroidNotificationChannel(
'high_importance_channel', // id
'High Importance Notifications', // title
description: 'This channel is used for important notifications.', // description
importance: Importance.max,
);
Future<void> _initLocalNotifications() async {
// 안드로이드 초기화 설정
const AndroidInitializationSettings initializationSettingsAndroid =
AndroidInitializationSettings('@drawable/ic_notification'); // AndroidManifest.xml에 설정한 아이콘 이름
// iOS 초기화 설정
const DarwinInitializationSettings initializationSettingsIOS = DarwinInitializationSettings(
requestAlertPermission: false,
requestBadgePermission: false,
requestSoundPermission: false,
);
final InitializationSettings initializationSettings = InitializationSettings(
android: initializationSettingsAndroid,
iOS: initializationSettingsIOS,
);
// 로컬 알림 플러그인 초기화 및 알림 탭 콜백 설정
await _localNotifications.initialize(
initializationSettings,
onDidReceiveNotificationResponse: (NotificationResponse response) async {
// 알림을 탭했을 때의 로직
print('로컬 알림 탭! 페이로드: ${response.payload}');
// payload를 기반으로 특정 페이지로 네비게이션
},
);
// 안드로이드 포그라운드 알림을 위해 채널 생성
await _localNotifications
.resolvePlatformSpecificImplementation<AndroidFlutterLocalNotificationsPlugin>()
?.createNotificationChannel(_channel);
}
Future<void> _setupFCM() async {
// iOS 포그라운드 알림 표시 설정
await _firebaseMessaging.setForegroundNotificationPresentationOptions(
alert: true,
badge: true,
sound: true,
);
// 1. 포그라운드 상태에서 메시지 수신 리스너
FirebaseMessaging.onMessage.listen((RemoteMessage message) {
print('포그라운드에서 메시지 수신!');
if (message.notification != null) {
print('메시지 포함 알림: ${message.notification!.title}');
// 포그라운드 상태에서는 FCM이 자동으로 알림을 띄우지 않으므로,
// 로컬 알림을 사용해 직접 띄워줍니다.
showLocalNotification(message);
}
});
// 2. 백그라운드/종료 상태에서 알림을 탭하여 앱이 열렸을 때 처리
FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) {
print('알림 탭으로 앱 열림 (백그라운드)');
// message.data를 기반으로 특정 페이지로 네비게이션
});
}
// 로컬 알림을 표시하는 메소드
void showLocalNotification(RemoteMessage message) {
final notification = message.notification;
final android = message.notification?.android;
if (notification != null) {
_localNotifications.show(
notification.hashCode,
notification.title,
notification.body,
NotificationDetails(
android: AndroidNotificationDetails(
_channel.id, // 필수: 채널 ID
_channel.name, // 필수: 채널 이름
channelDescription: _channel.description,
icon: android?.smallIcon, // 아이콘
importance: Importance.max,
priority: Priority.high,
),
iOS: const DarwinNotificationDetails(
presentAlert: true,
presentBadge: true,
presentSound: true,
),
),
payload: message.data['route'], // 알림 탭 시 전달할 데이터
);
}
}
이제 `init()` 메소드에서 `_setupFCM()`과 `_initLocalNotifications()`를 순서대로 호출하도록 구성하면, 포그라운드에서 FCM 메시지를 수신했을 때 `flutter_local_notifications`를 통해 커스텀 알림이 표시됩니다.
4.4. 알림 탭(클릭) 시 화면 이동 처리
사용자가 알림을 탭했을 때 앱의 특정 페이지로 이동시키는 것은 매우 일반적인 요구사항입니다. 이는 앱의 상태에 따라 처리 방식이 나뉩니다.
- 앱이 종료(Terminated)된 상태에서 알림 탭:
이 경우, `main` 함수가 처음부터 실행됩니다. `FirebaseMessaging.instance.getInitialMessage()`를 사용하여 앱을 실행시킨 초기 메시지를 가져올 수 있습니다.
// main.dart 또는 스플래시 화면의 initState void handleInitialMessage() async { RemoteMessage? initialMessage = await FirebaseMessaging.instance.getInitialMessage(); if (initialMessage != null) { print("앱 종료 상태에서 알림 탭으로 실행됨"); // initialMessage.data['route'] 등을 사용하여 네비게이션 // 예: navigatorKey.currentState.pushNamed(initialMessage.data['route']); } }
- 앱이 백그라운드(Background) 상태에서 알림 탭:
이때는 `FirebaseMessaging.onMessageOpenedApp` 스트림이 이벤트를 발생시킵니다. 이 리스너는 `_setupFCM` 메소드 안에 이미 설정되어 있습니다.
- 앱이 포그라운드(Foreground) 상태에서 로컬 알림 탭:
포그라운드에서는 우리가 직접 띄운 로컬 알림을 탭하게 됩니다. 이 이벤트는 `_localNotifications.initialize`의 `onDidReceiveNotificationResponse` 콜백에서 처리됩니다.
5. 4단계: 고급 활용 및 트러블슈팅
기본 연동이 완료되었습니다. 이제 몇 가지 고급 기능과 자주 발생하는 문제들에 대해 알아보겠습니다.
5.1. Firebase 콘솔에서 테스트 메시지 보내기
구현한 기능이 잘 동작하는지 테스트해볼 시간입니다.
- Firebase 콘솔로 이동하여 왼쪽 메뉴의 '참여' 섹션에서 'Cloud Messaging'을 선택합니다.
- 'Send your first message' 또는 '새 캠페인' > '알림'을 클릭합니다.
- 알림 제목과 텍스트를 입력합니다.
- '테스트 메시지 보내기' 버튼을 클릭합니다.
- 앱을 실행하여 콘솔에 출력된 FCM 등록 토큰을 복사하여 입력 필드에 붙여넣고 '+' 버튼을 눌러 추가합니다.
- '테스트' 버튼을 클릭합니다.
앱이 포그라운드, 백그라운드, 종료 상태일 때 각각 테스트하여 모든 시나리오에서 알림이 정상적으로 수신되고 표시되는지 확인합니다.
팁: 데이터 페이로드 보내기
알림에 추가 데이터를 담아 보내려면, 알림 작성 화면의 '추가 옵션' 섹션에서 '맞춤 데이터' 키-값 쌍을 추가하면 됩니다. 예를 들어 `key: route`, `value: /detail/123` 과 같이 보내면, `RemoteMessage` 객체의 `data` 맵을 통해 이 값을 수신하여 화면 이동에 사용할 수 있습니다.
5.2. 자주 묻는 질문 및 트러블슈팅
-
Q: 알림이 전혀 오지 않아요.
- (공통) FCM 토큰이 정상적으로 생성되는지 확인하세요.
- (iOS) APNs 인증 키(.p8)가 Firebase 콘솔에 정확히 등록되었는지, Xcode에서 'Push Notifications'와 'Background Modes' Capability가 활성화되었는지 다시 확인하세요.
- (iOS) 시뮬레이터는 원격 푸시 알림을 지원하지 않습니다. 반드시 실제 기기로 테스트해야 합니다.
- (Android)
google-services.json
파일이android/app
디렉토리에 올바르게 위치해 있는지 확인하세요. - (Android 13+) `POST_NOTIFICATIONS` 권한을 요청하고 사용자가 허용했는지 확인하세요.
-
Q: Android에서 알림 아이콘이 회색 네모/원으로 나와요.
- 상태 표시줄 아이콘은 알파 채널이 있는(배경이 투명한) 단색(주로 흰색)이어야 합니다.
android/app/src/main/res/drawable
에 있는 아이콘 파일을 확인하세요. 컬러풀한 앱 런처 아이콘을 사용하면 안 됩니다.
- 상태 표시줄 아이콘은 알파 채널이 있는(배경이 투명한) 단색(주로 흰색)이어야 합니다.
-
Q: iOS에서 포그라운드 알림이 안 떠요.
await _firebaseMessaging.setForegroundNotificationPresentationOptions(...)
코드가 호출되었는지 확인하세요. 또는, `onMessage` 리스너에서 `flutter_local_notifications`를 사용하여 직접 알림을 `show()` 하고 있는지 확인하세요. 둘 중 하나의 방법은 반드시 구현되어야 합니다.
-
Q: iOS에서
FirebaseAppDelegateProxyEnabled
를NO
로 설정했는데도 문제가 계속돼요.Info.plist
에서 키의 타입이 정말 'String'으로 되어 있는지 다시 한번 확인하세요 ('Boolean'이 아닙니다).- `AppDelegate.swift`에 `Messaging.messaging().apnsToken = deviceToken` 코드가 제대로 추가되었는지 확인하세요.
- 변경 후
ios
폴더에서pod deintegrate
후pod install
을 다시 실행하여 Pod을 깨끗하게 재설치해보세요. - 그래도 문제가 해결되지 않는다면 Xcode에서 프로젝트 클린(Clean Build Folder) 후 다시 빌드해보세요.
마치며: 안정적인 푸시 알림 시스템을 향한 여정
지금까지 Flutter에서 Firebase Cloud Messaging과 Flutter Local Notifications를 연동하여 강력하고 유연한 푸시 알림 시스템을 구축하는 전 과정을 상세히 살펴보았습니다. 처음에는 복잡해 보일 수 있지만, 각 단계의 '왜'를 이해하고 나면 전체 구조가 명확해집니다.
핵심은 FCM은 메시지 전달자, 로컬 알림은 메시지 표현자라는 역할 분담을 명확히 하는 것입니다. 그리고 iOS의 스위즐링 비활성화와 수동 설정, Android의 알림 채널과 아이콘/권한 설정이라는 플랫폼별 특성을 꼼꼼하게 챙기는 것이 안정적인 구현의 열쇠입니다.
이 가이드를 통해 여러분은 단순한 알림 수신을 넘어, 사용자의 앱 사용 경험을 한 단계 끌어올릴 수 있는 상호작용 가능한 알림 시스템의 기반을 다졌습니다. 이제 여기서 더 나아가 이미지나 버튼이 포함된 리치 알림, 주제(Topic) 구독을 통한 그룹 메시징, 데이터 메시지를 활용한 사일런트 푸시 등 다양한 고급 기능을 탐색하며 여러분의 앱을 더욱 풍성하게 만들어 보시길 바랍니다.
0 개의 댓글:
Post a Comment