담당 Core: 임도협
- Notification이 작동하는 원리 배우기
- flutter_local_notifications를 사용해 notification 구현하기
- 플랫폼별 알림 권한 설정하기
- 알림 초기화하기
- 즉시 알림 구현하기
- 예약 알림 구현하기
알림은 앱이 직접 띄우는 것이 아니라, OS(Android / iOS)가 대신 관리하고 표시하는데요, 그래서 앱이 꺼져 있어도 알림 표시되거나, 시스템 설정에서 알림 ON/OFF를 할 수 있는 것입니다!
Android 8.0(API 26)+부터는 모든 알림이 Channel 기반입니다.
App → Notification → Channel → OS → User
Channel이란 "알림을 그룹화하는 단위"라고 할 수 있는데요, Channel은 한 번 생성되면 수정 불가하고, importance, sound 변경도 불가합니다.
Channel을 쓰는 이유: 사용자 입장에서 채팅, 댓글, 광고 등 서로 다른 알림 종류에 대해 정교하게 제어할 수 있도록 구분하기 위함입니다!
![]()
그래서 안드로이드에는 오늘 구현할 알림 두 종류가 각각 표시되는 것을 확인할 수 있습니다!
반대로 iOS는 Channel 개념이 없고, UNUserNotificationCenter라는 클래스에서 처리하도록 만들어졌습니다!
App → UNUserNotificationCenter → OS → User
그래서 iOS에서는 알림 제어가 단순한데, 앱 단위 ON/OFF하거나 소리 / 배너 / 배지 설정만 할 수 있습니다.
flutter create --empty noti
code .VSCode에서
lib/main.dart를 열고, 화면 하단 상태바에서 사용할 디바이스(시뮬레이터)를 선택한 후, 창 상단 Start Debugging을 눌러 앱을 실행합니다.
다음 패키지를 설치합니다.
flutter_local_notifications: ^21.0.0
timezone: ^0.11.0VSCode에서
cmd+shift+p키를 누르고,Dart: Add Dependency를 입력한 후 enter![]()
flutter_local_notifications를 입력한 후 enter![]()
timezone을 입력한 후 enter![]()
pubspec.yaml에 설치된 모습![]()
푸쉬 알림을 사용하려면 플랫폼별 권한 설정이 필요한데요, 특히 iOS는 반드시 권한 요청 코드가 필요하고, Android도 최신 버전에서는 알림 권한을 명시해야 합니다!
android/app/src/main/AndroidManifest.xml파일에 아래 권한을 추가합니다.
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.ACCESS_NOTIFICATION_POLICY" />그리고 <application ...> 태그 내부에 다음 receiver도 추가합니다.
<receiver
android:exported="false"
android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationReceiver" />
<receiver
android:exported="false"
android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationBootReceiver">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED"/>
<action android:name="android.intent.action.MY_PACKAGE_REPLACED"/>
<action android:name="android.intent.action.QUICKBOOT_POWERON" />
<action android:name="com.htc.intent.action.QUICKBOOT_POWERON"/>
</intent-filter>
</receiver>
<receiver
android:exported="false"
android:name="com.dexterous.flutterlocalnotifications.FlutterLocalNotificationsReceiver"/>
ios/Runner/Info.plist에 아래 내용을 추가합니다.
<key>NSUserNotificationUsageDescription</key>
<string>푸쉬 알림을 수신하기 위해 권한이 필요합니다.</string>
그 다음, ios/Runner/AppDelegate.swift를 아래로 바꿔줍니다.
기본적으로 iOS는 앱이 실행 중이면 알림을 받아도 표시하지 않는데요,
UNUserNotificationCenter.current().delegate = self로 설정함으로서 알림을 항상 수신할 수 있게 하는 과정입니다!
import Flutter
import UIKit
import flutter_local_notifications
@main
@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
UNUserNotificationCenter.current().delegate = self as UNUserNotificationCenterDelegate
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) {
FlutterLocalNotificationsPlugin.setPluginRegistrantCallback { (registry) in
GeneratedPluginRegistrant.register(with: registry)
}
GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry)
}
@available(iOS 12.0, *)
override func userNotificationCenter(
_ center: UNUserNotificationCenter,
openSettingsFor notification: UNNotification?
) {
let controller = window?.rootViewController as! FlutterViewController
let channel = FlutterMethodChannel(
name: "com.example.noti/settings",
binaryMessenger: controller.binaryMessenger)
channel.invokeMethod("showNotificationSettings", arguments: nil)
}
}lib/main.dart를 아래처럼 작성합니다.
import 'package:flutter/material.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:timezone/data/latest.dart' as tz;
import 'package:timezone/timezone.dart' as tz;
final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
FlutterLocalNotificationsPlugin();
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
tz.initializeTimeZones();
tz.setLocalLocation(tz.getLocation('Asia/Seoul'));
const androidSettings = AndroidInitializationSettings('@mipmap/ic_launcher');
const darwinSettings = DarwinInitializationSettings();
const initializationSettings = InitializationSettings(
android: androidSettings,
iOS: darwinSettings,
);
await flutterLocalNotificationsPlugin.initialize(
settings: initializationSettings,
);
await requestNotificationPermissions();
runApp(const MyApp());
}
Future<void> requestNotificationPermissions() async {
await flutterLocalNotificationsPlugin
.resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin
>()
?.requestNotificationsPermission();
await flutterLocalNotificationsPlugin
.resolvePlatformSpecificImplementation<
IOSFlutterLocalNotificationsPlugin
>()
?.requestPermissions(alert: true, badge: true, sound: true);
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Notification Demo',
home: const NotificationPage(),
debugShowCheckedModeBanner: false,
);
}
}
class NotificationPage extends StatelessWidget {
const NotificationPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('9주차 세션')),
body: const Center(),
);
}
}
runApp(..)을 호출하기 전,await requestNotificationPermissions()으로 플랫폼별 알림 권한을 직접 요청하고 있습니다! 구현과 기기 버전에 따라 알림 권한 정책이 다르므로, 이처럼 직접 요청을 하는 것이 중요합니다.
이제 앱을 다시 빌드하면 다음과 같이 알림 권한을 묻는 것을 확인할 수 있습니다.
먼저 버튼을 눌렀을 때 바로 알림이 오도록 해보겠습니다.
showSimpleNotification() 펑션을 lib/main.dart에 추가해주세요.
Future<void> showSimpleNotification() async {
const androidDetails = AndroidNotificationDetails(
'basic_channel',
'Basic Notifications',
channelDescription: 'Basic noti channel',
importance: Importance.max,
priority: Priority.high,
);
const notificationDetails = NotificationDetails(
android: androidDetails,
iOS: DarwinNotificationDetails(),
);
await flutterLocalNotificationsPlugin.show(
id: 0,
title: 'GDGoC 알림',
body: '알림이 도착했습니다!.',
payload: 'payload',
notificationDetails: notificationDetails,
);
}그리고 이를 호출하는 버튼을 메인 화면에 추가해주면 됩니다!
body: Center(
child: ElevatedButton(
onPressed: showSimpleNotification,
child: const Text('즉시 알림 보내기'),
),
),
이번에는 몇 초 뒤에 알림이 오도록 예약해보겠습니다.
이 기능을 위해 timezone 패키지를 함께 사용합니다.
Future<void> scheduleNotification() async {
const androidDetails = AndroidNotificationDetails(
'schedule_channel',
'Scheduled Notifications',
channelDescription: 'Scheduled noti channel',
importance: Importance.max,
priority: Priority.high,
);
const notificationDetails = NotificationDetails(
android: androidDetails,
iOS: DarwinNotificationDetails(),
);
await flutterLocalNotificationsPlugin.zonedSchedule(
id: 1,
title: 'GDGoC 예약 알림',
body: '5초 뒤에 도착한 알림입니다.',
payload: 'payload',
scheduledDate: tz.TZDateTime.now(tz.local).add(const Duration(seconds: 5)),
notificationDetails: notificationDetails,
androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle,
);
}그리고 이를 호출하는 버튼을 즉시 알림 버튼 아래에 추가해줍니다!
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: showSimpleNotification,
child: const Text('즉시 알림 보내기'),
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: scheduleNotification,
child: const Text('5초 뒤 알림 보내기'),
),
],
),
),
import 'package:flutter/material.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:timezone/data/latest.dart' as tz;
import 'package:timezone/timezone.dart' as tz;
final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
FlutterLocalNotificationsPlugin();
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
tz.initializeTimeZones();
tz.setLocalLocation(tz.getLocation('Asia/Seoul'));
const androidSettings = AndroidInitializationSettings('@mipmap/ic_launcher');
const darwinSettings = DarwinInitializationSettings();
const initializationSettings = InitializationSettings(
android: androidSettings,
iOS: darwinSettings,
);
await flutterLocalNotificationsPlugin.initialize(
settings: initializationSettings,
);
await requestNotificationPermissions();
runApp(const MyApp());
}
Future<void> requestNotificationPermissions() async {
await flutterLocalNotificationsPlugin
.resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin
>()
?.requestNotificationsPermission();
await flutterLocalNotificationsPlugin
.resolvePlatformSpecificImplementation<
IOSFlutterLocalNotificationsPlugin
>()
?.requestPermissions(alert: true, badge: true, sound: true);
}
Future<void> showSimpleNotification() async {
const androidDetails = AndroidNotificationDetails(
'basic_channel',
'Basic Notifications',
channelDescription: 'Basic noti channel',
importance: Importance.max,
priority: Priority.high,
);
const notificationDetails = NotificationDetails(
android: androidDetails,
iOS: DarwinNotificationDetails(),
);
await flutterLocalNotificationsPlugin.show(
id: 0,
title: 'GDGoC 알림',
body: '알림이 도착했습니다!.',
payload: 'payload',
notificationDetails: notificationDetails,
);
}
Future<void> scheduleNotification() async {
const androidDetails = AndroidNotificationDetails(
'schedule_channel',
'Scheduled Notifications',
channelDescription: 'Scheduled noti channel',
importance: Importance.max,
priority: Priority.high,
);
const notificationDetails = NotificationDetails(
android: androidDetails,
iOS: DarwinNotificationDetails(),
);
await flutterLocalNotificationsPlugin.zonedSchedule(
id: 1,
title: 'GDGoC 예약 알림',
body: '5초 뒤에 도착한 알림입니다.',
payload: 'payload',
scheduledDate: tz.TZDateTime.now(tz.local).add(const Duration(seconds: 5)),
notificationDetails: notificationDetails,
androidScheduleMode: AndroidScheduleMode.inexactAllowWhileIdle,
);
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Notification Demo',
home: const NotificationPage(),
debugShowCheckedModeBanner: false,
);
}
}
class NotificationPage extends StatelessWidget {
const NotificationPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('9주차 세션')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: showSimpleNotification,
child: const Text('즉시 알림 보내기'),
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: scheduleNotification,
child: const Text('5초 뒤 알림 보내기'),
),
],
),
),
);
}
}




