Flutter 개발

Flutter 푸시 노티피케이션 완벽 가이드 3편: 생명주기와 상태 관리

앱 생명주기별 노티피케이션 처리와 포괄적인 상태 관리 시스템 구축

2025년 9월 20일
22분 읽기
Flutter 푸시 노티피케이션 완벽 가이드 3편: 생명주기와 상태 관리

앱 생명주기와 노티피케이션

Flutter 앱의 생명주기는 노티피케이션 처리에 중요한 영향을 미칩니다. 각 상태별로 다른 접근 방식이 필요하며, 사용자 경험을 최적화하기 위해서는 이를 체계적으로 관리해야 합니다.

앱 생명주기 상태:
- 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. 사용자 피드백: 알림 관련 사용자 의견 수집 및 반영

이 시리즈를 통해 구축한 노티피케이션 시스템은 실무에서 바로 활용할 수 있는 수준의 완성도를 가지고 있습니다. 각 프로젝트의 요구사항에 맞게 커스터마이징하여 사용하시기 바랍니다.

시리즈 완료

Flutter 푸시 노티피케이션 완벽 가이드 시리즈 요약:

1편 - 기초 설정과 구현
- Firebase 프로젝트 설정 및 초기화
- Android/iOS 플랫폼별 설정
- 기본적인 메시지 수신 및 처리
- 로컬 노티피케이션 통합

2편 - 라우팅과 딥링크 처리
- GoRouter를 활용한 체계적인 라우팅
- 노티피케이션 데이터 구조 설계
- 복잡한 라우팅 시나리오 처리
- 보안 및 권한 기반 라우팅

3편 - 생명주기와 상태 관리
- 앱 생명주기별 최적화된 처리
- 포괄적인 노티피케이션 상태 관리
- 사용자 설정 및 커스터마이징
- 성능 모니터링 및 최적화

이 시리즈를 통해 Flutter에서 엔터프라이즈급 푸시 노티피케이션 시스템을 완성할 수 있습니다. 각 편의 코드를 조합하여 프로젝트에 맞는 최적의 솔루션을 구축하시기 바랍니다.
#푸시노티피케이션
#생명주기
#상태관리
#AppLifecycleState
#백그라운드처리