[Flutter] 앱이 나에게 말을 걸 때, Flutter Local Notification

서연·2025년 10월 28일
post-thumbnail

📖 Local Notification

  • 모바일 애플리케이션 내에서 기기 자체적으로 생성 및 표시되는 알림을 의미한다.
  • 서버나 외부 서비스 없이 앱 내부에서 알림을 직접 스케줄링하고 관리할 수 있는 것이 특징이다.

📦 주요 패키지

flutter_local_notifications

💡 플랫폼 지원

  • Android / iOS 모두 지원

⚙️ 초기 설정

Android

  • AndroidManifest.xml에 권한을 추가한다.
  • 알림 아이콘을 설정한다.
  • 중요도, 소리, 진동 패턴 등 알림 채널을 생성한다.

iOS

  • AppDelegate.swift 또는 main.dart 내에서 알림 권한 요청 코드를 추가한다.
  • 배너 • 소리 • 배지 표시 권한을 명시한다.

🪄 기본 동작 방식

  • FlutterLocalNotificationPlugin 인스턴스를 생성한다.
  • initialize() 메서드로 초기화한다.
  • 플랫폼별 설정을 적용한다.

💥 알림 표시 방법 (Display Methods)

show

  • 즉시 알림 표시

schedule()

  • 특정 시간에 알림 예약

PeriodicallyShow()

  • 주기적으로 반복 알림 표시

각 알림은 다음 속성을 포함한다.

ID고유 식별자
제목title
본문body
payload데이터 전달용
플랫폼별 세부 설정Android / iOS 스타일 등

🎨 알림 커스터마이징(customization)

Android

  • 중요도, 소리, LED 색상 제어 등 알림 채널을 설정할 수 있다.
  • BigTextStyle (긴 텍스트 스타일), BigPictureStyle (이미지 포함 알림) 스타일을 지정할 수 있다.

iOS

  • 배너 표시 방식, 소리 및 진동 제어, 배지 카운트 조정을 설정할 수 있다.

🚀 알림 클릭 시 동작 (Interaction)

  • 알림 클릭 시 특정 동작 수행 가능하며 초기화 시 onDidReceiveNotificationResponse 콜백을 통해 처리한다.
  • Navigator.push 사용하여 payload 데이터로 특정 화면으로 이동한다.
  • 읽음 처리, 새로고침 등 특정 기능을 실행한다.

🌏 시간대 기반 스케줄링 (TimeZone Scheduling)

  • timeZone 패키지와 함께 사용하여 특정 날짜 • 시간대에 정확하게 알림 예약이 가능하다.

🌟 장점 (Advantages)

서버 통신이 불필요하다.

  • 네트워크 지연 없이 즉시 알림이 가능하다.

개인화가 가능하다.

  • 사용자 맞춤 알림 스케줄링이 가능하다.

앱 참여율이 향상된다.

  • 이벤트나 리마인드 알림으로 사용자 재참여가 유도된다.

비용이 절감된다.

  • 서버 유지비 없이 알림이 운영된다.

💬 Tip

  • 실무에서는 flutter_local_notifications를 FCM 푸시와 함께 하이브리드로 사용하는 경우가 많다.
  • 예를 들어 iOS에서 포그라운드 푸시가 표시되지 않을 때 로컬 알림으로 대체 표시하는 식이다.

🧠 코드

class LocalNotificationsService {
// 싱글톤 패턴 : 앱 전체에서 하나의 인스턴스만 존재하도록 만드는 디자인 패턴
// 알림 서비스는 여러 개 만들 필요가 없고 하나만 있으면 되기 때문에 이렇게 구현
// _internal()은 외부에서 직접 호출할 수 없는 프라이빗 생성자
  LocalNotificationsService._internal();
  // _instance는 이 클래스의 유일한 인스턴스를 저장하는 변수
  static final LocalNotificationsService _instance =
      LocalNotificationsService._internal();
      // instance() 메서드를 통해서만 이 인스턴스에 접근 가능
  factory LocalNotificationsService.instance() => _instance;
  late FlutterLocalNotificationsPlugin _flutterLocalNotificationsPlugin;

  // 안드로이드 알림 아이콘 설정
  final _androidInitializationSettings = const AndroidInitializationSettings(
    '@mipmap/ic_launcher',
  );

  // iOS 알림 권한 설정
  // iOS에서는 알림을 보내기 전에 사용자에게 권한을 요청해야 함
  final _iosInitializationSettings = const DarwinInitializationSettings(
    requestAlertPermission: true, // 알림 표시 권한
    requestBadgePermission: true, // 앱 아이콘 배지 권한
    requestSoundPermission: true, // 소리 재생 권한
  );

  // 안드로이드 알림 채널 정의
  final _androidChannel = const AndroidNotificationChannel(
    'Knockie',
    'Knockie',
    description: 'Knockie push notification channel',
    importance: Importance.high,
  );
  // 상태 관리 변수들
  // 초기화가 완료되었는지 추적하는 플래그
  bool _isFlutterLocalNotificationInitialized = false;
  // 각 알림에 고유한 ID를 부여하기 위한 카운터
  int _notificationIdCounter = 0;

  // 초기화 메서드는 앱이 시작될 때 한 번만 실행되어야 함
  // 이미 초기화했으면 그냥 종료
  // onTap 매개변수는 사용자가 알림을 탭했을 때 실행할 함수 받음
  Future<void> init({NotificationTapHandler? onTap}) async {
    if (_isFlutterLocalNotificationInitialized) {
      return;
    }

    // 알림 플러그인 객체 생성
    // 이 객체를 통해 모든 알림 기능 사용함
    _flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();

    // 안드로이드와 Ios 설정 하나로 묶음
    final initializationSettings = InitializationSettings(
      android: _androidInitializationSettings,
      iOS: _iosInitializationSettings,
    );

    // 플러그인 초기화 + 알림 탭 이벤트 처리
    await _flutterLocalNotificationsPlugin.initialize(
      initializationSettings,
      // 사용자가 알림을 탭했을 때 실행되는 함수
      onDidReceiveNotificationResponse: (NotificationResponse response) {
        // 특정 화면 이동시키는 처리
        final payload = response.payload;
        if (payload != null && onTap != null) {
        // payload (알림과 함께 전달된 데이터)를 JSON으로 파싱
          final message = RemoteMessage(data: jsonDecode(payload));
          // Firebase 메시징 서비스를 통해 딥 링크로 변환한 후 해당 링크로 이동시킴
          final link = FirebaseMessagingService.instance().mapMessageToDeepLink(message);
          if(link != null) onTap(link);
        }
      },
    );

    // 정의한 알림 채널을 시스템에 등록
    await _flutterLocalNotificationsPlugin
    // 안드로이드 전용 기능에 접근
    // 특정 플랫폼의 구현체를 가져오는 메서드
        .resolvePlatformSpecificImplementation<
          AndroidFlutterLocalNotificationsPlugin
()
        // 위에서 정의한 채널 생성
        // ?.는 null일 수도 있으니 안전하게 호출하겠다는 의미
        // iOS에서는 null 반환
        ?.createNotificationChannel(_androidChannel);

        // 초기화 완료
    _isFlutterLocalNotificationInitialized = true;
  }

  // 실제로 알림을 화면에 표시하는 역할을 함
  Future<void> showNotification(
    String? title, // 제목
    String? body, // 본문
    String? payload, // 나중에 알림을 탭했을 때 어떤 화면으로 이동할지 결정하는데 사용
  ) async {
    // 안드로이드에서 알림이 어떻게 표시될지 세부 설정
    // 어떤 채널을 사용할지, 중요도와 우선순위 어떻게 할지 정함
    AndroidNotificationDetails androidDetails = AndroidNotificationDetails(
      _androidChannel.id,
      _androidChannel.name,
      channelDescription: _androidChannel.description,
      importance: Importance.high, // 알림이 소리와 함께 화면 상단에 팝업으로 나타남
      priority: Priority.high, // 알림이 소리와 함께 화면 상단에 팝업으로 나타남
    );

    // ios 알림 설정
    const iosDetails = DarwinNotificationDetails();

    // 안드로이드와 ios 설정 하나로 통합
    final notificationDetails = NotificationDetails(
      android: androidDetails,
      iOS: iosDetails,
    );

    // 알림 표시
    // 같은 ID로 보내면 이전 알림이 업데이트되고 다른 ID면 새 알림이 생김
    await _flutterLocalNotificationsPlugin.show(
    // 현재 카운터 값을 사용하고 1 증가 시킴
    // 각 알림마다 고유한 ID 보유
      _notificationIdCounter++,
      title,
      body,
      notificationDetails,
      payload: payload,
    );
  }
}

0개의 댓글