앱 생명주기와 노티피케이션
Flutter 앱의 생명주기는 노티피케이션 처리에 중요한 영향을 미칩니다. 각 상태별로 다른 접근 방식이 필요하며, 사용자 경험을 최적화하기 위해서는 이를 체계적으로 관리해야 합니다.
앱 생명주기 상태:
- Resumed (포그라운드): 앱이 활성 상태이고 사용자가 상호작용 중
- Inactive: 앱이 비활성 상태 (전화 수신, 알림 패널 등)
- Paused (백그라운드): 앱이 백그라운드에 있지만 실행 중
- Detached (종료): 앱이 완전히 종료된 상태
생명주기별 노티피케이션 처리 전략:
- 포그라운드: 인앱 알림이나 스낵바로 표시
- 백그라운드: 시스템 노티피케이션으로 표시
- 종료 상태: 앱 재시작 시 초기 메시지 처리
앱 생명주기 상태:
- Resumed (포그라운드): 앱이 활성 상태이고 사용자가 상호작용 중
- Inactive: 앱이 비활성 상태 (전화 수신, 알림 패널 등)
- Paused (백그라운드): 앱이 백그라운드에 있지만 실행 중
- Detached (종료): 앱이 완전히 종료된 상태
생명주기별 노티피케이션 처리 전략:
- 포그라운드: 인앱 알림이나 스낵바로 표시
- 백그라운드: 시스템 노티피케이션으로 표시
- 종료 상태: 앱 재시작 시 초기 메시지 처리
생명주기 관리 서비스 구현
앱 생명주기를 추적하고 노티피케이션 처리를 최적화하는 서비스를 구현합니다.
Dart
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
class AppLifecycleService with WidgetsBindingObserver {
static final AppLifecycleService _instance = AppLifecycleService._internal();
factory AppLifecycleService() => _instance;
AppLifecycleService._internal();
AppLifecycleState _currentState = AppLifecycleState.resumed;
DateTime? _backgroundTime;
DateTime? _foregroundTime;
final List<Function(AppLifecycleState)> _listeners = [];
AppLifecycleState get currentState => _currentState;
bool get isInForeground => _currentState == AppLifecycleState.resumed;
bool get isInBackground => _currentState == AppLifecycleState.paused;
void initialize() {
WidgetsBinding.instance.addObserver(this);
_currentState = WidgetsBinding.instance.lifecycleState ?? AppLifecycleState.resumed;
}
void dispose() {
WidgetsBinding.instance.removeObserver(this);
_listeners.clear();
}
void addListener(Function(AppLifecycleState) listener) {
_listeners.add(listener);
}
void removeListener(Function(AppLifecycleState) listener) {
_listeners.remove(listener);
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
super.didChangeAppLifecycleState(state);
final previousState = _currentState;
_currentState = state;
_handleLifecycleTransition(previousState, state);
for (final listener in _listeners) {
listener(state);
}
}
void _handleLifecycleTransition(
AppLifecycleState from,
AppLifecycleState to,
) {
switch (to) {
case AppLifecycleState.resumed:
_onAppResumed(from);
break;
case AppLifecycleState.paused:
_onAppPaused();
break;
case AppLifecycleState.inactive:
_onAppInactive();
break;
case AppLifecycleState.detached:
_onAppDetached();
break;
case AppLifecycleState.hidden:
_onAppHidden();
break;
}
}
void _onAppResumed(AppLifecycleState from) {
_foregroundTime = DateTime.now();
print('앱이 포그라운드로 전환됨');
if (from == AppLifecycleState.paused && _backgroundTime != null) {
final backgroundDuration = _foregroundTime!.difference(_backgroundTime!);
_handleBackgroundReturn(backgroundDuration);
}
}
void _onAppPaused() {
_backgroundTime = DateTime.now();
print('앱이 백그라운드로 전환됨');
_prepareBackgroundTasks();
}
void _onAppInactive() {
print('앱이 비활성 상태로 전환됨');
}
void _onAppHidden() {
print('앱이 숨겨짐');
}
void _onAppDetached() {
print('앱이 종료됨');
_saveAppState();
}
void _handleBackgroundReturn(Duration backgroundDuration) {
print('백그라운드 시간: \${backgroundDuration.inSeconds}초');
if (backgroundDuration.inMinutes > 5) {
_refreshAppData();
}
NotificationQueueService().processPendingNotifications();
}
void _prepareBackgroundTasks() {
// 불필요한 리소스 정리
}
void _saveAppState() {
// SharedPreferences 등에 현재 상태 저장
}
void _refreshAppData() {
print('앱 데이터 새로고침 중...');
}
}
노티피케이션 상태 관리
노티피케이션의 읽음/안읽음 상태, 히스토리, 설정 등을 체계적으로 관리하는 시스템을 구현합니다.
Dart
import 'package:flutter_riverpod/flutter_riverpod.dart';
class NotificationItem {
final String id;
final String title;
final String body;
final NotificationType type;
final Map<String, dynamic> data;
final DateTime receivedAt;
final bool isRead;
final bool isArchived;
final String? imageUrl;
const NotificationItem({
required this.id,
required this.title,
required this.body,
required this.type,
required this.data,
required this.receivedAt,
this.isRead = false,
this.isArchived = false,
this.imageUrl,
});
NotificationItem copyWith({
String? id,
String? title,
String? body,
NotificationType? type,
Map<String, dynamic>? data,
DateTime? receivedAt,
bool? isRead,
bool? isArchived,
String? imageUrl,
}) {
return NotificationItem(
id: id ?? this.id,
title: title ?? this.title,
body: body ?? this.body,
type: type ?? this.type,
data: data ?? this.data,
receivedAt: receivedAt ?? this.receivedAt,
isRead: isRead ?? this.isRead,
isArchived: isArchived ?? this.isArchived,
imageUrl: imageUrl ?? this.imageUrl,
);
}
Map<String, dynamic> toMap() {
return {
'id': id,
'title': title,
'body': body,
'type': type.name,
'data': data,
'receivedAt': receivedAt.toIso8601String(),
'isRead': isRead,
'isArchived': isArchived,
'imageUrl': imageUrl,
};
}
factory NotificationItem.fromMap(Map<String, dynamic> map) {
return NotificationItem(
id: map['id'],
title: map['title'],
body: map['body'],
type: NotificationType.fromString(map['type']),
data: Map<String, dynamic>.from(map['data']),
receivedAt: DateTime.parse(map['receivedAt']),
isRead: map['isRead'] ?? false,
isArchived: map['isArchived'] ?? false,
imageUrl: map['imageUrl'],
);
}
}
class NotificationState {
final List<NotificationItem> notifications;
final int unreadCount;
final bool isLoading;
final String? error;
const NotificationState({
this.notifications = const [],
this.unreadCount = 0,
this.isLoading = false,
this.error,
});
NotificationState copyWith({
List<NotificationItem>? notifications,
int? unreadCount,
bool? isLoading,
String? error,
}) {
return NotificationState(
notifications: notifications ?? this.notifications,
unreadCount: unreadCount ?? this.unreadCount,
isLoading: isLoading ?? this.isLoading,
error: error ?? this.error,
);
}
}
노티피케이션 저장소 구현
노티피케이션 데이터를 로컬에 저장하고 관리하는 저장소를 구현합니다.
Dart
import 'package:shared_preferences/shared_preferences.dart';
import 'dart:convert';
class NotificationStorage {
static final NotificationStorage _instance = NotificationStorage._internal();
factory NotificationStorage() => _instance;
NotificationStorage._internal();
static const String _notificationsKey = 'stored_notifications';
static const int _maxStorageCount = 100;
Future<void> saveNotification(NotificationItem notification) async {
final prefs = await SharedPreferences.getInstance();
final notifications = await getNotifications();
final updatedNotifications = [notification, ...notifications];
if (updatedNotifications.length > _maxStorageCount) {
updatedNotifications.removeRange(_maxStorageCount, updatedNotifications.length);
}
final jsonList = updatedNotifications.map((n) => n.toMap()).toList();
await prefs.setString(_notificationsKey, json.encode(jsonList));
}
Future<List<NotificationItem>> getNotifications() async {
final prefs = await SharedPreferences.getInstance();
final jsonString = prefs.getString(_notificationsKey);
if (jsonString == null) return [];
try {
final jsonList = json.decode(jsonString) as List;
return jsonList.map((json) => NotificationItem.fromMap(json)).toList();
} catch (e) {
print('노티피케이션 로드 오류: $e');
return [];
}
}
Future<void> markAsRead(String id) async {
final notifications = await getNotifications();
final updatedNotifications = notifications.map((n) {
return n.id == id ? n.copyWith(isRead: true) : n;
}).toList();
await _saveNotifications(updatedNotifications);
}
Future<void> markAllAsRead() async {
final notifications = await getNotifications();
final updatedNotifications = notifications.map((n) {
return n.copyWith(isRead: true);
}).toList();
await _saveNotifications(updatedNotifications);
}
Future<void> deleteNotification(String id) async {
final notifications = await getNotifications();
final updatedNotifications = notifications.where((n) => n.id != id).toList();
await _saveNotifications(updatedNotifications);
}
Future<void> cleanupOldNotifications({int daysToKeep = 30}) async {
final notifications = await getNotifications();
final cutoffDate = DateTime.now().subtract(Duration(days: daysToKeep));
final filteredNotifications = notifications.where((n) {
return n.receivedAt.isAfter(cutoffDate);
}).toList();
await _saveNotifications(filteredNotifications);
}
Future<void> _saveNotifications(List<NotificationItem> notifications) async {
final prefs = await SharedPreferences.getInstance();
final jsonList = notifications.map((n) => n.toMap()).toList();
await prefs.setString(_notificationsKey, json.encode(jsonList));
}
Future<void> clearAll() async {
final prefs = await SharedPreferences.getInstance();
await prefs.remove(_notificationsKey);
}
}
노티피케이션 큐 서비스
여러 노티피케이션을 효율적으로 처리하기 위한 큐 시스템을 구현합니다.
Dart
import 'dart:collection';
import 'dart:async';
class NotificationQueueService {
static final NotificationQueueService _instance = NotificationQueueService._internal();
factory NotificationQueueService() => _instance;
NotificationQueueService._internal();
final Queue<NotificationItem> _queue = Queue<NotificationItem>();
final Queue<NotificationItem> _priorityQueue = Queue<NotificationItem>();
Timer? _processingTimer;
bool _isProcessing = false;
static const Duration _processingInterval = Duration(seconds: 2);
void start() {
if (_processingTimer?.isActive == true) return;
_processingTimer = Timer.periodic(_processingInterval, (_) {
if (!_isProcessing) {
_processQueue();
}
});
}
void stop() {
_processingTimer?.cancel();
_processingTimer = null;
}
void enqueue(NotificationItem notification) {
_queue.add(notification);
print('노티피케이션 큐에 추가: \${notification.title}');
}
void enqueuePriority(NotificationItem notification) {
_priorityQueue.add(notification);
print('우선순위 큐에 추가: \${notification.title}');
}
Future<void> processPendingNotifications() async {
if (_isProcessing) return;
await _processQueue();
}
Future<void> _processQueue() async {
if (_isProcessing) return;
_isProcessing = true;
try {
while (_priorityQueue.isNotEmpty) {
final notification = _priorityQueue.removeFirst();
await _processNotification(notification);
await Future.delayed(const Duration(milliseconds: 500));
}
while (_queue.isNotEmpty) {
final notification = _queue.removeFirst();
await _processNotification(notification);
await Future.delayed(const Duration(milliseconds: 500));
}
} finally {
_isProcessing = false;
}
}
Future<void> _processNotification(NotificationItem notification) async {
try {
final appLifecycle = AppLifecycleService();
if (appLifecycle.isInForeground) {
await _showInAppNotification(notification);
} else {
await _showSystemNotification(notification);
}
print('노티피케이션 처리 완료: \${notification.title}');
} catch (e) {
print('노티피케이션 처리 오류: $e');
}
}
Future<void> _showInAppNotification(NotificationItem notification) async {
print('인앱 노티피케이션 표시: \${notification.title}');
}
Future<void> _showSystemNotification(NotificationItem notification) async {
print('시스템 노티피케이션 표시: \${notification.title}');
}
Map<String, int> getQueueStatus() {
return {
'normal': _queue.length,
'priority': _priorityQueue.length,
'total': _queue.length + _priorityQueue.length,
};
}
void clearQueue() {
_queue.clear();
_priorityQueue.clear();
}
}
사용자 설정 관리
사용자가 노티피케이션 설정을 커스터마이징할 수 있는 시스템을 구현합니다.
Dart
class NotificationSettings {
final bool globalEnabled;
final Map<NotificationType, bool> typeSettings;
final TimeOfDay? quietTimeStart;
final TimeOfDay? quietTimeEnd;
final bool vibrationEnabled;
final bool soundEnabled;
final String? customSoundPath;
final bool showPreview;
final int maxDailyNotifications;
const NotificationSettings({
this.globalEnabled = true,
this.typeSettings = const {},
this.quietTimeStart,
this.quietTimeEnd,
this.vibrationEnabled = true,
this.soundEnabled = true,
this.customSoundPath,
this.showPreview = true,
this.maxDailyNotifications = 50,
});
NotificationSettings copyWith({
bool? globalEnabled,
Map<NotificationType, bool>? typeSettings,
TimeOfDay? quietTimeStart,
TimeOfDay? quietTimeEnd,
bool? vibrationEnabled,
bool? soundEnabled,
String? customSoundPath,
bool? showPreview,
int? maxDailyNotifications,
}) {
return NotificationSettings(
globalEnabled: globalEnabled ?? this.globalEnabled,
typeSettings: typeSettings ?? this.typeSettings,
quietTimeStart: quietTimeStart ?? this.quietTimeStart,
quietTimeEnd: quietTimeEnd ?? this.quietTimeEnd,
vibrationEnabled: vibrationEnabled ?? this.vibrationEnabled,
soundEnabled: soundEnabled ?? this.soundEnabled,
customSoundPath: customSoundPath ?? this.customSoundPath,
showPreview: showPreview ?? this.showPreview,
maxDailyNotifications: maxDailyNotifications ?? this.maxDailyNotifications,
);
}
bool isTypeEnabled(NotificationType type) {
if (!globalEnabled) return false;
return typeSettings[type] ?? true;
}
bool isQuietTime() {
if (quietTimeStart == null || quietTimeEnd == null) return false;
final now = TimeOfDay.now();
return _isTimeBetween(now, quietTimeStart!, quietTimeEnd!);
}
bool _isTimeBetween(TimeOfDay time, TimeOfDay start, TimeOfDay end) {
final timeMinutes = time.hour * 60 + time.minute;
final startMinutes = start.hour * 60 + start.minute;
final endMinutes = end.hour * 60 + end.minute;
if (startMinutes <= endMinutes) {
return timeMinutes >= startMinutes && timeMinutes <= endMinutes;
} else {
return timeMinutes >= startMinutes || timeMinutes <= endMinutes;
}
}
Map<String, dynamic> toMap() {
return {
'globalEnabled': globalEnabled,
'typeSettings': typeSettings.map((k, v) => MapEntry(k.name, v)),
'quietTimeStart': quietTimeStart != null
? '\${quietTimeStart!.hour}:\${quietTimeStart!.minute}'
: null,
'quietTimeEnd': quietTimeEnd != null
? '\${quietTimeEnd!.hour}:\${quietTimeEnd!.minute}'
: null,
'vibrationEnabled': vibrationEnabled,
'soundEnabled': soundEnabled,
'customSoundPath': customSoundPath,
'showPreview': showPreview,
'maxDailyNotifications': maxDailyNotifications,
};
}
factory NotificationSettings.fromMap(Map<String, dynamic> map) {
return NotificationSettings(
globalEnabled: map['globalEnabled'] ?? true,
typeSettings: Map<NotificationType, bool>.fromEntries(
(map['typeSettings'] as Map<String, dynamic>? ?? {}).entries.map(
(e) => MapEntry(NotificationType.fromString(e.key), e.value),
),
),
quietTimeStart: map['quietTimeStart'] != null
? _parseTimeOfDay(map['quietTimeStart'])
: null,
quietTimeEnd: map['quietTimeEnd'] != null
? _parseTimeOfDay(map['quietTimeEnd'])
: null,
vibrationEnabled: map['vibrationEnabled'] ?? true,
soundEnabled: map['soundEnabled'] ?? true,
customSoundPath: map['customSoundPath'],
showPreview: map['showPreview'] ?? true,
maxDailyNotifications: map['maxDailyNotifications'] ?? 50,
);
}
static TimeOfDay _parseTimeOfDay(String timeString) {
final parts = timeString.split(':');
return TimeOfDay(hour: int.parse(parts[0]), minute: int.parse(parts[1]));
}
}
노티피케이션 UI 컴포넌트
사용자가 노티피케이션을 관리할 수 있는 UI 컴포넌트들을 구현합니다.
Dart
class NotificationListScreen extends ConsumerWidget {
const NotificationListScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final notificationState = ref.watch(notificationStateProvider);
final notificationNotifier = ref.read(notificationStateProvider.notifier);
return Scaffold(
appBar: AppBar(
title: const Text('알림'),
actions: [
if (notificationState.unreadCount > 0)
TextButton(
onPressed: () => notificationNotifier.markAllAsRead(),
child: const Text('모두 읽음'),
),
],
),
body: notificationState.isLoading
? const Center(child: CircularProgressIndicator())
: notificationState.notifications.isEmpty
? const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.notifications_off, size: 64, color: Colors.grey),
SizedBox(height: 16),
Text('받은 알림이 없습니다', style: TextStyle(color: Colors.grey)),
],
),
)
: ListView.builder(
itemCount: notificationState.notifications.length,
itemBuilder: (context, index) {
final notification = notificationState.notifications[index];
return NotificationListItem(
notification: notification,
onTap: () => _handleNotificationTap(context, ref, notification),
onDismiss: () => notificationNotifier.deleteNotification(notification.id),
);
},
),
);
}
void _handleNotificationTap(
BuildContext context,
WidgetRef ref,
NotificationItem notification,
) {
if (!notification.isRead) {
ref.read(notificationStateProvider.notifier).markAsRead(notification.id);
}
NotificationRoutingService().handleNotificationRoute(notification.data);
}
}
class NotificationListItem extends StatelessWidget {
final NotificationItem notification;
final VoidCallback onTap;
final VoidCallback onDismiss;
const NotificationListItem({
required this.notification,
required this.onTap,
required this.onDismiss,
super.key,
});
@override
Widget build(BuildContext context) {
return Dismissible(
key: Key(notification.id),
direction: DismissDirection.endToStart,
onDismissed: (_) => onDismiss(),
background: Container(
color: Colors.red,
alignment: Alignment.centerRight,
padding: const EdgeInsets.only(right: 16),
child: const Icon(Icons.delete, color: Colors.white),
),
child: ListTile(
leading: _buildLeadingIcon(),
title: Text(
notification.title,
style: TextStyle(
fontWeight: notification.isRead ? FontWeight.normal : FontWeight.bold,
),
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(notification.body),
const SizedBox(height: 4),
Text(
_formatTime(notification.receivedAt),
style: Theme.of(context).textTheme.bodySmall,
),
],
),
trailing: notification.isRead
? null
: Container(
width: 8,
height: 8,
decoration: const BoxDecoration(
color: Colors.blue,
shape: BoxShape.circle,
),
),
onTap: onTap,
),
);
}
Widget _buildLeadingIcon() {
switch (notification.type) {
case NotificationType.chat:
return const CircleAvatar(
backgroundColor: Colors.green,
child: Icon(Icons.chat, color: Colors.white),
);
case NotificationType.post:
return const CircleAvatar(
backgroundColor: Colors.blue,
child: Icon(Icons.article, color: Colors.white),
);
default:
return const CircleAvatar(
backgroundColor: Colors.grey,
child: Icon(Icons.notifications, color: Colors.white),
);
}
}
String _formatTime(DateTime dateTime) {
final now = DateTime.now();
final difference = now.difference(dateTime);
if (difference.inDays > 0) {
return '\${difference.inDays}일 전';
} else if (difference.inHours > 0) {
return '\${difference.inHours}시간 전';
} else if (difference.inMinutes > 0) {
return '\${difference.inMinutes}분 전';
} else {
return '방금 전';
}
}
}
통합 노티피케이션 서비스
지금까지 구현한 모든 기능을 통합하여 완전한 노티피케이션 시스템을 만듭니다.
Dart
class EnhancedNotificationService extends NotificationService {
final AppLifecycleService _lifecycleService = AppLifecycleService();
final NotificationQueueService _queueService = NotificationQueueService();
@override
Future<void> initialize({GoRouter? router}) async {
await super.initialize(router: router);
_lifecycleService.initialize();
_lifecycleService.addListener(_onLifecycleChanged);
_queueService.start();
await _processPendingOnStartup();
}
void _onLifecycleChanged(AppLifecycleState state) {
switch (state) {
case AppLifecycleState.resumed:
_onAppResumed();
break;
case AppLifecycleState.paused:
_onAppPaused();
break;
default:
break;
}
}
void _onAppResumed() {
_queueService.processPendingNotifications();
_updateBadgeCount();
}
void _onAppPaused() {
NotificationStorage().cleanupOldNotifications();
}
@override
void _handleForegroundMessage(RemoteMessage message) {
print('포그라운드 메시지 수신: \${message.messageId}');
final notificationItem = _createNotificationItem(message);
if (_shouldShowNotification(notificationItem)) {
if (_lifecycleService.isInForeground) {
_queueService.enqueue(notificationItem);
} else {
_showLocalNotification(message);
}
}
}
@override
void _handleBackgroundMessage(RemoteMessage message) {
print('백그라운드 메시지 처리: \${message.messageId}');
final notificationItem = _createNotificationItem(message);
NotificationStorage().saveNotification(notificationItem);
_routingService.handleNotificationRoute(message.data);
}
NotificationItem _createNotificationItem(RemoteMessage message) {
return NotificationItem(
id: message.messageId ?? DateTime.now().millisecondsSinceEpoch.toString(),
title: message.notification?.title ?? '알림',
body: message.notification?.body ?? '',
type: NotificationType.fromString(message.data['type'] ?? 'general'),
data: message.data,
receivedAt: DateTime.now(),
imageUrl: message.notification?.android?.imageUrl ??
message.notification?.apple?.imageUrl,
);
}
bool _shouldShowNotification(NotificationItem notification) {
return true;
}
Future<void> _processPendingOnStartup() async {
final notifications = await NotificationStorage().getNotifications();
final unprocessedNotifications = notifications.where((n) => !n.isRead).toList();
for (final notification in unprocessedNotifications) {
_queueService.enqueue(notification);
}
}
void _updateBadgeCount() {
// 실제 구현에서는 flutter_app_badger 패키지 사용
}
@override
void dispose() {
_lifecycleService.removeListener(_onLifecycleChanged);
_lifecycleService.dispose();
_queueService.stop();
super.dispose();
}
}
성능 모니터링
노티피케이션 시스템의 성능을 모니터링하고 최적화하는 방법을 알아보겠습니다.
Dart
class NotificationPerformanceMonitor {
static final NotificationPerformanceMonitor _instance =
NotificationPerformanceMonitor._internal();
factory NotificationPerformanceMonitor() => _instance;
NotificationPerformanceMonitor._internal();
final Map<String, DateTime> _startTimes = {};
final List<PerformanceMetric> _metrics = [];
void startMeasure(String operationId) {
_startTimes[operationId] = DateTime.now();
}
void endMeasure(String operationId, {Map<String, dynamic>? metadata}) {
final startTime = _startTimes.remove(operationId);
if (startTime == null) return;
final duration = DateTime.now().difference(startTime);
final metric = PerformanceMetric(
operationId: operationId,
duration: duration,
timestamp: DateTime.now(),
metadata: metadata,
);
_metrics.add(metric);
_logMetric(metric);
if (_metrics.length > 1000) {
_metrics.removeRange(0, 100);
}
}
void _logMetric(PerformanceMetric metric) {
print('Performance: \${metric.operationId} took \${metric.duration.inMilliseconds}ms');
if (metric.duration.inMilliseconds > 1000) {
print('WARNING: Slow operation detected: \${metric.operationId}');
}
}
Map<String, dynamic> getPerformanceStats() {
if (_metrics.isEmpty) return {};
final durations = _metrics.map((m) => m.duration.inMilliseconds).toList();
durations.sort();
return {
'totalOperations': _metrics.length,
'averageMs': durations.reduce((a, b) => a + b) / durations.length,
'medianMs': durations[durations.length ~/ 2],
'p95Ms': durations[(durations.length * 0.95).floor()],
'maxMs': durations.last,
'minMs': durations.first,
};
}
void clearMetrics() {
_metrics.clear();
_startTimes.clear();
}
}
class PerformanceMetric {
final String operationId;
final Duration duration;
final DateTime timestamp;
final Map<String, dynamic>? metadata;
const PerformanceMetric({
required this.operationId,
required this.duration,
required this.timestamp,
this.metadata,
});
}
마무리 및 베스트 프랙티스
3편에 걸친 Flutter 푸시 노티피케이션 완벽 가이드를 마무리하며, 실무에서 활용할 수 있는 베스트 프랙티스를 정리하겠습니다.
시스템 설계 원칙:
1. 관심사 분리: 각 서비스(수신, 라우팅, 상태관리, 저장소)를 명확히 분리
2. 확장성: 새로운 노티피케이션 타입이나 기능을 쉽게 추가할 수 있도록 설계
3. 오류 처리: 네트워크 오류, 권한 문제 등에 대한 견고한 처리
4. 성능 최적화: 메모리 사용량과 배터리 소모 최소화
보안 고려사항:
1. 데이터 검증: 모든 노티피케이션 데이터의 유효성 검사
2. 권한 관리: 적절한 권한 확인 및 요청
3. 딥링크 보안: 허용된 라우트만 접근 가능하도록 제한
4. 개인정보 보호: 민감한 정보의 로컬 저장 시 암호화
사용자 경험 최적화:
1. 점진적 권한 요청: 필요한 시점에 권한 요청
2. 명확한 설정 UI: 사용자가 쉽게 이해하고 제어할 수 있는 인터페이스
3. 적절한 알림 빈도: 과도한 알림으로 인한 사용자 이탈 방지
4. 컨텍스트 인식: 앱 상태와 사용자 행동에 맞는 알림 표시
운영 및 유지보수:
1. 모니터링: 성능 지표와 오류 추적
2. A/B 테스트: 알림 효과성 측정 및 개선
3. 점진적 롤아웃: 새로운 기능의 단계적 배포
4. 사용자 피드백: 알림 관련 사용자 의견 수집 및 반영
이 시리즈를 통해 구축한 노티피케이션 시스템은 실무에서 바로 활용할 수 있는 수준의 완성도를 가지고 있습니다. 각 프로젝트의 요구사항에 맞게 커스터마이징하여 사용하시기 바랍니다.
시스템 설계 원칙:
1. 관심사 분리: 각 서비스(수신, 라우팅, 상태관리, 저장소)를 명확히 분리
2. 확장성: 새로운 노티피케이션 타입이나 기능을 쉽게 추가할 수 있도록 설계
3. 오류 처리: 네트워크 오류, 권한 문제 등에 대한 견고한 처리
4. 성능 최적화: 메모리 사용량과 배터리 소모 최소화
보안 고려사항:
1. 데이터 검증: 모든 노티피케이션 데이터의 유효성 검사
2. 권한 관리: 적절한 권한 확인 및 요청
3. 딥링크 보안: 허용된 라우트만 접근 가능하도록 제한
4. 개인정보 보호: 민감한 정보의 로컬 저장 시 암호화
사용자 경험 최적화:
1. 점진적 권한 요청: 필요한 시점에 권한 요청
2. 명확한 설정 UI: 사용자가 쉽게 이해하고 제어할 수 있는 인터페이스
3. 적절한 알림 빈도: 과도한 알림으로 인한 사용자 이탈 방지
4. 컨텍스트 인식: 앱 상태와 사용자 행동에 맞는 알림 표시
운영 및 유지보수:
1. 모니터링: 성능 지표와 오류 추적
2. A/B 테스트: 알림 효과성 측정 및 개선
3. 점진적 롤아웃: 새로운 기능의 단계적 배포
4. 사용자 피드백: 알림 관련 사용자 의견 수집 및 반영
이 시리즈를 통해 구축한 노티피케이션 시스템은 실무에서 바로 활용할 수 있는 수준의 완성도를 가지고 있습니다. 각 프로젝트의 요구사항에 맞게 커스터마이징하여 사용하시기 바랍니다.
시리즈 완료
Flutter 푸시 노티피케이션 완벽 가이드 시리즈 요약:
1편 - 기초 설정과 구현
- Firebase 프로젝트 설정 및 초기화
- Android/iOS 플랫폼별 설정
- 기본적인 메시지 수신 및 처리
- 로컬 노티피케이션 통합
2편 - 라우팅과 딥링크 처리
- GoRouter를 활용한 체계적인 라우팅
- 노티피케이션 데이터 구조 설계
- 복잡한 라우팅 시나리오 처리
- 보안 및 권한 기반 라우팅
3편 - 생명주기와 상태 관리
- 앱 생명주기별 최적화된 처리
- 포괄적인 노티피케이션 상태 관리
- 사용자 설정 및 커스터마이징
- 성능 모니터링 및 최적화
이 시리즈를 통해 Flutter에서 엔터프라이즈급 푸시 노티피케이션 시스템을 완성할 수 있습니다. 각 편의 코드를 조합하여 프로젝트에 맞는 최적의 솔루션을 구축하시기 바랍니다.
1편 - 기초 설정과 구현
- Firebase 프로젝트 설정 및 초기화
- Android/iOS 플랫폼별 설정
- 기본적인 메시지 수신 및 처리
- 로컬 노티피케이션 통합
2편 - 라우팅과 딥링크 처리
- GoRouter를 활용한 체계적인 라우팅
- 노티피케이션 데이터 구조 설계
- 복잡한 라우팅 시나리오 처리
- 보안 및 권한 기반 라우팅
3편 - 생명주기와 상태 관리
- 앱 생명주기별 최적화된 처리
- 포괄적인 노티피케이션 상태 관리
- 사용자 설정 및 커스터마이징
- 성능 모니터링 및 최적화
이 시리즈를 통해 Flutter에서 엔터프라이즈급 푸시 노티피케이션 시스템을 완성할 수 있습니다. 각 편의 코드를 조합하여 프로젝트에 맞는 최적의 솔루션을 구축하시기 바랍니다.