푸시 알림 지원
푸시 알림을 통해 사용자 상호작용을 추적하여 리인게이지먼트 캠페인과 어트리뷰션 전환을 정확하게 측정할 수 있도록 Firebase 클라우드 메시징(FCM)을 Singular SDK와 연동하세요.
아래 구현 가이드라인에 따라 알림 데이터가 적절한 어트리뷰션을 위해 Singular SDK에 올바르게 전달되도록 하세요.
푸시 알림을 추적해야 하는 이유: 푸시 알림은 리인게이지먼트를 유도하지만 추적을 위해서는 올바른 연동이 필요합니다. Singular는 알림과 상호작용하는 사용자의 어트리뷰션을 올바르게 추적하여 마케팅 캠페인과 참여 전략을 최적화합니다.
구현 가이드
Firebase 클라우드 메시징 설정
Flutter 애플리케이션에서 푸시 알림 지원을 위해 Firebase 패키지를 설치하고 플랫폼별 설정을 구성합니다.
Firebase 패키지 설치
핵심 기능 및 메시징 지원을 위해 pubspec.yaml 파일에 Firebase 종속성을 추가합니다.
dependencies:
flutter:
sdk: flutter
firebase_core: ^2.24.2
firebase_messaging: ^14.7.10
singular_flutter_sdk: ^1.8.0
종속성을 추가한 후 flutter pub get 을 실행하여 패키지를 설치합니다.
iOS 구성
Firebase에 iOS 앱을 등록하고 Xcode에서 푸시 알림 기능을 구성합니다.
- iOS 앱 등록하기: Firebase 콘솔 프로젝트에서 iOS 앱을 만듭니다.
-
구성 파일 추가:
GoogleService-Info.plist을 다운로드하여 Xcode Runner 폴더에 추가합니다. - 기능 활성화: Xcode 프로젝트 설정에서 푸시 알림 기능을 활성화합니다.
- 백그라운드 모드를 활성화합니다: 백그라운드 모드 활성화: 백그라운드 모드를 활성화하고 원격 알림을 확인합니다.
Android 구성
Firebase에 Android 앱을 등록하고 프로젝트에 구성 파일을 추가합니다.
- Android 앱 등록하기: Firebase 콘솔 프로젝트에서 Android 앱을 만듭니다.
-
구성 파일 추가:
google-services.json을 다운로드하여android/app/에 배치합니다. - 종속성 확인: Firebase 메시징 종속성이 추가되고 AndroidManifest.xml에 권한이 부여되었는지 확인합니다.
Flutter에서 Firebase 초기화
Flutter 앱을 실행하기 전에 초기화하도록 Firebase를 구성하고 앱이 포그라운드에 있지 않을 때 수신되는 알림에 대한 백그라운드 메시지 핸들러를 설정합니다.
import 'package:flutter/material.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
// Background message handler (must be top-level function)
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
await Firebase.initializeApp();
print('Background message: ${message.messageId}');
print('Data: ${message.data}');
}
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// Initialize Firebase
await Firebase.initializeApp();
// Set background message handler
FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);
runApp(MyApp());
}
푸시 링크 경로 구성
푸시 알림 페이로드 구조 내에서 Singular 추적 링크가 위치하는 JSON 경로를 정의합니다.
알림 데이터 구조에서 Singular 링크의 키 경로를 지정하는 문자열 배열을 전달하여 푸시 링크 경로를 구성합니다. 각 경로는 키의 중첩 구조를 나타내는 배열입니다.
import 'package:singular_flutter_sdk/singular.dart';
import 'package:singular_flutter_sdk/singular_config.dart';
SingularConfig config = SingularConfig(
'YOUR_SDK_KEY',
'YOUR_SDK_SECRET'
);
// Configure paths where Singular links are located in push payload
config.pushNotificationsLinkPaths = [
['sng_link'], // Top-level key
['path', 'to', 'url'], // Nested path
['rootObj', 'nestedObj', 'singularLink'] // Deep nested path
];
Singular.start(config);
경로 구성 예시:
-
단순 키: 페이로드의 최상위 키에
['sng_link']사용 -
중첩된 키: 중첩된 JSON 구조를 횡단하려면
['rootObj', 'nestedObj', 'key']사용 - 다중 경로: 여러 경로 배열을 정의하여 Singular 링크에 대해 가능한 여러 위치를 확인합니다.
구성 속성:
List<List<String>>? pushNotificationsLinkPaths
전체 구성 문서는 pushNotificationsLinkPaths 참조를 참조하세요.
플랫폼별 처리
Flutter에서 푸시 알림 처리하기
Firebase 메시지 리스너를 구현하여 포그라운드 및 백그라운드 상태에서 알림 데이터를 캡처한 다음 어트리뷰션 추적을 위해 데이터를 Singular로 전달합니다.
import 'package:flutter/material.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:singular_flutter_sdk/singular.dart';
class MyApp extends StatefulWidget {
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
final FirebaseMessaging _firebaseMessaging = FirebaseMessaging.instance;
@override
void initState() {
super.initState();
setupPushNotifications();
}
void setupPushNotifications() async {
// Request permission for iOS
NotificationSettings settings = await _firebaseMessaging.requestPermission(
alert: true,
badge: true,
sound: true,
);
print('User granted permission: ${settings.authorizationStatus}');
// Handle foreground notifications
FirebaseMessaging.onMessage.listen((RemoteMessage message) {
print('Foreground message received: ${message.messageId}');
handleForegroundNotification(message);
});
// Handle notifications that opened the app from background
FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) {
print('Notification opened app from background: ${message.messageId}');
handleBackgroundNotification(message);
});
// Check for notification that launched the app (terminated state)
RemoteMessage? initialMessage = await _firebaseMessaging.getInitialMessage();
if (initialMessage != null) {
print('App launched from notification: ${initialMessage.messageId}');
handleTerminatedNotification(initialMessage);
}
}
void handleForegroundNotification(RemoteMessage message) {
String title = message.notification?.title ?? '';
String body = message.notification?.body ?? '';
Map<String, dynamic> data = message.data;
print('Title: $title');
print('Body: $body');
print('Data: $data');
// Pass notification data to Singular
if (data.isNotEmpty) {
Singular.handlePushNotification(data);
}
// Display local notification or custom UI
displayLocalNotification(title, body, data);
}
void handleBackgroundNotification(RemoteMessage message) {
print('Processing background notification: ${message.messageId}');
// Pass notification data to Singular
if (message.data.isNotEmpty) {
Singular.handlePushNotification(message.data);
}
// Navigate to specific screen based on notification data
navigateFromNotification(message.data);
}
void handleTerminatedNotification(RemoteMessage message) {
print('Processing terminated state notification: ${message.messageId}');
// Pass notification data to Singular
if (message.data.isNotEmpty) {
Singular.handlePushNotification(message.data);
}
// Navigate to specific screen
navigateFromNotification(message.data);
}
void displayLocalNotification(
String title,
String body,
Map<String, dynamic> data
) {
// Your notification display logic
print('Displaying notification: $title - $body');
}
void navigateFromNotification(Map<String, dynamic> data) {
// Your navigation logic based on notification data
final route = data['route'];
print('Navigating to: $route');
}
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
home: MyHomePage(),
);
}
}
메서드 서명:
static void handlePushNotification(Map<String, dynamic> notificationData)
전체 메서드 설명서는 handlePushNotification 참조를 참조하십시오.
iOS 네이티브 구성
종료된 상태의 앱
앱이 종료된 상태에서 열릴 때 자동 푸시 추적을 위해 실행 옵션을 Singular SDK에 전달하도록 iOS 앱디렉티브를 구성하세요.
AppDelegate.m 또는 AppDelegate.swift 에서 실행 옵션을 Singular SDK에 전달합니다:
Objective-C 구현
// Import at the top of the file
#import "SingularAppDelegate.h"
- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
[GeneratedPluginRegistrant registerWithRegistry:self];
// Pass launch options to Singular for push tracking
[SingularAppDelegate shared].launchOptions = launchOptions;
return [super application:application
didFinishLaunchingWithOptions:launchOptions];
}
신속한 구현
import singular_flutter_sdk
override func application(_ application: UIApplication,
didFinishLaunchingWithOptions
launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
GeneratedPluginRegistrant.register(with: self)
// Pass launch options to Singular for push tracking
if let singularAppDelegate = SingularAppDelegate.shared() {
singularAppDelegate.launchOptions = launchOptions
}
return super.application(application,
didFinishLaunchingWithOptions: launchOptions)
}
자동 처리: 앱이 실행 중이 아닌 상태에서 사용자가 푸시 알림을 탭하면, 실행 옵션을 통해 앱 실행 중에 Singular가 자동으로 알림 페이로드를 캡처합니다.
안드로이드 네이티브 구성
백그라운드 또는 포그라운드의 앱
앱이 백그라운드 또는 포그라운드 상태일 때 알림 인텐트를 Singular SDK에 전달하도록 Android 메인 액티비티를 구성하세요.
메인 액티비티에서 onNewIntent 을 재정의하여 인텐트를 Singular로 전달합니다:
Java 구현
// Add imports at the top
import android.content.Intent;
import com.singular.flutter_sdk.SingularBridge;
// Add to MainActivity class
@Override
public void onNewIntent(Intent intent) {
if(intent.getData() != null) {
setIntent(intent);
super.onNewIntent(intent);
// Pass intent to Singular for push tracking
SingularBridge.onNewIntent(intent);
}
}
Kotlin 구현
// Add imports at the top
import android.content.Intent
import com.singular.flutter_sdk.SingularBridge
// Add to MainActivity class
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
if (intent.data != null) {
setIntent(intent)
// Pass intent to Singular for push tracking
SingularBridge.onNewIntent(intent)
}
}
종료된 상태의 앱
종료된 상태의 Android 앱에는 추가 구성이 필요하지 않습니다. Flutter 브리지 레이어는 사용자가 알림을 탭할 때 이 시나리오를 자동으로 처리합니다.
자동 처리: 앱이 실행 중이 아닌 상태에서 사용자가 푸시 알림을 탭하면 Singular는 네이티브 브리지 연동을 통해 알림 데이터를 자동으로 캡처합니다.
유효성 검사 가이드
시작 세션에서 페이로드 확인
시작 세션 API 호출을 검사하여 푸시 알림 링크가 Singular에 올바르게 전달되었는지 확인합니다.
Singular SDK는 사용자가 알림을 탭할 때 시작 세션 요청의 singular_link 매개변수 아래에 푸시 알림 페이로드를 포함합니다.
세션 시작 요청 예시:
https://sdk-api-v1.singular.net/api/v1/start?
a=<SDK-Key>
&singular_link=https://singularassist2.sng.link/C4nw9/r1m0?_dl=singular%3A%2F%2Ftest&_smtype=3
&i=net.singular.sampleapp
&s=1740905574084
&sdk=Singular/Flutter-v1.8.0
대체 인증: Singular SDK 콘솔을 사용하여 푸시 알림 추적을 확인합니다. 딥링크 URL 필드를 확인하여 추적 링크가 올바르게 캡처되었는지 확인합니다.
고급 구성
ESP 도메인 구성
이메일 서비스 제공업체(ESP) 또는 기타 타사 도메인 내에서 Singular 링크를 래핑하는 경우 외부 도메인을 구성합니다.
import 'package:singular_flutter_sdk/singular.dart';
import 'package:singular_flutter_sdk/singular_config.dart';
// Configure ESP domains for wrapped Singular links
SingularConfig config = SingularConfig(
'YOUR_SDK_KEY',
'YOUR_SDK_SECRET'
);
config.espDomains = ['sl.esp.link', 'custom.domain.com'];
Singular.start(config);
구성 속성:
List<String>? espDomains
보안 참고: 기본적으로 Singular 링크 관리 페이지에 미리 정의된 sng.link도메인만 허용됩니다. 래핑된 링크를 사용하는 경우 ESP 도메인을 명시적으로 구성하세요.
전체 구성 문서는 espDomains 참조를 참조하세요.
동적 딥링크 라우팅
동적 리디렉션 재정의가 있는 하나의 Singular 추적 링크를 구성하여 Singular 알림에서 여러 딥링크 대상을 구현하세요.
사용 사례 예시: 여러 작업 옵션이 포함된 뉴스 속보 알림
-
최신 뉴스 읽기:
newsapp://article?id=12345 -
인기 토픽:
newsapp://trending -
스포츠:
newsapp://sports
여러 개의 추적 링크를 만드는 대신 하나의 Singular 링크를 사용하고 사용자 선택에 따라 리디렉션을 동적으로 재정의하세요. 구현에 대한 자세한 내용은 Singular 추적 링크에서 리디렉션 재 정의하기를 참조하세요.
전체 구현 예시
Firebase 설정, Singular 구성 및 Flutter 앱용 플랫폼별 핸들러를 사용한 포괄적인 푸시 알림 구현.
import 'package:flutter/material.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:singular_flutter_sdk/singular.dart';
import 'package:singular_flutter_sdk/singular_config.dart';
// Background message handler (top-level function)
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
await Firebase.initializeApp();
print('Background message: ${message.messageId}');
// Singular handles background notifications automatically
if (message.data.isNotEmpty) {
print('Background notification data: ${message.data}');
}
}
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// Initialize Firebase
await Firebase.initializeApp();
// Set background message handler
FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);
runApp(MyApp());
}
class MyApp extends StatefulWidget {
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
final FirebaseMessaging _firebaseMessaging = FirebaseMessaging.instance;
@override
void initState() {
super.initState();
initializeSingular();
setupPushNotifications();
}
void initializeSingular() {
// Configure Singular SDK
SingularConfig config = SingularConfig(
'YOUR_SDK_KEY',
'YOUR_SDK_SECRET'
);
// Configure push link paths
config.pushNotificationsLinkPaths = [
['sng_link'],
['data', 'url'],
['rootObj', 'nestedObj', 'singularLink']
];
// Configure ESP domains if needed
config.espDomains = ['sl.esp.link'];
// Enable logging for debugging
config.enableLogging = true;
// Initialize SDK
Singular.start(config);
}
void setupPushNotifications() async {
// Request permission (iOS)
NotificationSettings settings = await _firebaseMessaging.requestPermission(
alert: true,
badge: true,
sound: true,
);
if (settings.authorizationStatus == AuthorizationStatus.authorized) {
print('User granted permission');
}
// Get FCM token
String? token = await _firebaseMessaging.getToken();
print('FCM Token: $token');
// Handle foreground notifications
FirebaseMessaging.onMessage.listen((RemoteMessage message) {
print('Foreground notification: ${message.messageId}');
handleForegroundNotification(message);
});
// Handle notification that opened app from background
FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) {
print('Notification opened app: ${message.messageId}');
handleBackgroundNotification(message);
});
// Check for notification that launched the app
RemoteMessage? initialMessage = await _firebaseMessaging.getInitialMessage();
if (initialMessage != null) {
print('App launched from notification: ${initialMessage.messageId}');
handleTerminatedNotification(initialMessage);
}
}
void handleForegroundNotification(RemoteMessage message) {
final title = message.notification?.title ?? '';
final body = message.notification?.body ?? '';
final data = message.data;
print('Foreground - Title: $title, Body: $body');
// Pass notification data to Singular
if (data.isNotEmpty) {
Singular.handlePushNotification(data);
}
// Display notification to user
showNotificationDialog(title, body, data);
}
void handleBackgroundNotification(RemoteMessage message) {
print('Processing background notification');
// Pass notification data to Singular
if (message.data.isNotEmpty) {
Singular.handlePushNotification(message.data);
}
// Navigate based on notification data
navigateFromNotification(message.data);
}
void handleTerminatedNotification(RemoteMessage message) {
print('Processing terminated state notification');
// Pass notification data to Singular
if (message.data.isNotEmpty) {
Singular.handlePushNotification(message.data);
}
// Navigate based on notification data
navigateFromNotification(message.data);
}
void showNotificationDialog(
String title,
String body,
Map<String, dynamic> data
) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text(title),
content: Text(body),
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop();
navigateFromNotification(data);
},
child: Text('Open'),
),
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text('Dismiss'),
),
],
),
);
}
void navigateFromNotification(Map<String, dynamic> data) {
// Parse notification data for routing
final route = data['route'];
final productId = data['product_id'];
print('Navigating to: $route');
// Navigate to appropriate screen
if (route == 'product' && productId != null) {
navigatorKey.currentState?.pushNamed('/product/$productId');
} else if (route == 'promo') {
navigatorKey.currentState?.pushNamed('/promo');
} else {
navigatorKey.currentState?.pushNamed('/');
}
}
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
navigatorKey: navigatorKey,
initialRoute: '/',
routes: {
'/': (context) => HomeScreen(),
'/product': (context) => ProductScreen(),
'/promo': (context) => PromoScreen(),
},
);
}
}
// Placeholder screens
class HomeScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Home')),
body: Center(child: Text('Home Screen')),
);
}
}
class ProductScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Product')),
body: Center(child: Text('Product Screen')),
);
}
}
class PromoScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Promo')),
body: Center(child: Text('Promo Screen')),
);
}
}
중요 고려 사항
구현 참고 사항
-
콜백 핸들러 없음:
singularLinksHandler과 달리 푸시 알림 기능은 페이로드 콜백을 제공하지 않습니다. 사용자를 앱 내의 특정 콘텐츠로 라우팅하는 자체 딥링킹 로직을 구현하세요. - 어트리뷰션 흐름: 사용자가 알림을 탭하면 Singular는 페이로드를 검색하여 SDK 초기화에 의해 트리거되는 시작 세션 이벤트에 포함합니다. 백엔드는 이 데이터를 처리하여 푸시 알림 터치포인트를 어트리뷰션하고 리인게이지먼트 추적을 등록합니다.
-
도메인 제한: 링크 관리 페이지의 Singular 링크 도메인(
sng.link)만 기본적으로 허용됩니다.espDomains을 사용하여 래핑된 링크에 대해 명시적으로 ESP 도메인을 구성하세요. - 플랫폼 차이점: iOS는 종료된 상태에 대한 AppDelegate 구성이 필요하지만, 안드로이드는 브릿지 모듈을 통해 자동으로 처리합니다.
- 테스트: 개발 중에 SDK 로깅을 활성화하여 푸시 알림 데이터가 올바르게 캡처 및 처리되는지 확인합니다.
성공: 이 단계를 수행하면 이제 앱에서 Singular와 푸시 알림 상호작용을 추적하여 캠페인 성과 인사이트를 개선하고 정확한 리인게이지먼트 어트리뷰션을 보장할 수 있습니다.