<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Flutter_local_notifications on monkshark.dev</title><link>https://monkshark.github.io/tags/flutter_local_notifications/</link><description>Recent content in Flutter_local_notifications on monkshark.dev</description><generator>Hugo -- gohugo.io</generator><language>ko</language><lastBuildDate>Tue, 07 Apr 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://monkshark.github.io/tags/flutter_local_notifications/index.xml" rel="self" type="application/rss+xml"/><item><title>#3 - 급식 알림의 삽질기</title><link>https://monkshark.github.io/p/meal-notification/</link><pubDate>Tue, 07 Apr 2026 00:00:00 +0000</pubDate><guid>https://monkshark.github.io/p/meal-notification/</guid><description>&lt;h2 id="단순해-보였던-기능"&gt;&lt;a href="#%eb%8b%a8%ec%88%9c%ed%95%b4-%eb%b3%b4%ec%98%80%eb%8d%98-%ea%b8%b0%eb%8a%a5" class="header-anchor"&gt;&lt;/a&gt;단순해 보였던 기능
&lt;/h2&gt;&lt;p&gt;&amp;ldquo;매일 아침에 오늘 급식 메뉴를 알림으로 보내준다.&amp;rdquo;&lt;/p&gt;
&lt;p&gt;한 줄로 설명 가능한 기능이다. 사용자 입장에서는 당연히 있어야 할 것 같고, 구현도 간단해 보인다. 알림 예약하고, 시간 되면 보내면 되는 거 아닌가?&lt;/p&gt;
&lt;p&gt;이 &amp;ldquo;간단한&amp;rdquo; 기능을 제대로 동작하게 만드는 데 1년이 걸렸다. 2023년 12월부터 2024년 12월까지, 접근 방식을 네 번 바꾸고, 파일을 만들었다 지우기를 반복하고, 결국 처음과 전혀 다른 구조로 끝났다.&lt;/p&gt;
&lt;h2 id="1차-시도-notificationmanager-2023년-12월"&gt;&lt;a href="#1%ec%b0%a8-%ec%8b%9c%eb%8f%84-notificationmanager-2023%eb%85%84-12%ec%9b%94" class="header-anchor"&gt;&lt;/a&gt;1차 시도: NotificationManager (2023년 12월)
&lt;/h2&gt;&lt;p&gt;Flutter로 전환한 직후, 가장 먼저 만들고 싶었던 기능이 급식 알림이었다. Java 프로토타입에서도 알림 기능이 있었으니까, Flutter에서도 금방 만들 수 있을 거라고 생각했다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;NotificationManager.dart&lt;/code&gt;를 만들었다. 165줄. &lt;code&gt;flutter_local_notifications&lt;/code&gt; 패키지를 사용해서 로컬 알림을 예약하는 구조였다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-dart" data-lang="dart"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// 2023-12-12, 첫 번째 시도
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// NotificationManager.dart — 165줄
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;같은 커밋에서 &lt;code&gt;FirebaseCloudMessaging.dart&lt;/code&gt;라는 빈 파일도 만들었다. 이름에서 알 수 있듯이, FCM(Firebase Cloud Messaging)으로 서버에서 푸시를 보내는 것도 고려하고 있었다.&lt;/p&gt;
&lt;h2 id="2차-시도-fcm을-넣었다-뺐다-같은-주"&gt;&lt;a href="#2%ec%b0%a8-%ec%8b%9c%eb%8f%84-fcm%ec%9d%84-%eb%84%a3%ec%97%88%eb%8b%a4-%eb%ba%90%eb%8b%a4-%ea%b0%99%ec%9d%80-%ec%a3%bc" class="header-anchor"&gt;&lt;/a&gt;2차 시도: FCM을 넣었다 뺐다 (같은 주)
&lt;/h2&gt;&lt;p&gt;다음 날, &lt;code&gt;FirebaseCloudMessaging.dart&lt;/code&gt;에 60줄의 코드를 채워 넣었다. FCM 토큰을 받고, 메시지를 수신하는 기본 구조를 작성했다. 동시에 &lt;code&gt;NotificationManager.dart&lt;/code&gt;도 대폭 수정했다. 90줄 분량의 변경.&lt;/p&gt;
&lt;p&gt;그리고 &lt;strong&gt;같은 날 밤&lt;/strong&gt;, FCM 코드 60줄을 통째로 삭제했다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;2023-12-13 20:05 FirebaseCloudMessaging.dart +60줄
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;2023-12-13 23:53 FirebaseCloudMessaging.dart -60줄
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;4시간 만에 되돌린 것이다. 이유는 단순했다. 급식 알림은 &lt;strong&gt;서버가 보내는 게 아니라 기기가 스스로 보내야 하는 알림&lt;/strong&gt;이었다. FCM은 서버에서 클라이언트로 푸시를 보내는 도구인데, 매일 아침 급식 메뉴를 보내려면 서버 측에서 스케줄러를 돌려야 한다. Firebase Functions를 쓰면 가능하지만, 당시에는 무료 플랜(Spark)을 쓰고 있었고, Functions 배포가 불가능했다.&lt;/p&gt;
&lt;p&gt;그래서 방향을 틀었다. &lt;code&gt;ScheduledNotification.dart&lt;/code&gt;를 새로 만들고, 기기 로컬에서 알림을 예약하는 방식으로 갔다.&lt;/p&gt;
&lt;p&gt;이때까지만 해도 &amp;ldquo;방향만 잡으면 금방 끝나겠지&amp;quot;라고 생각했다.&lt;/p&gt;
&lt;h2 id="7개월의-공백-그리고-다시-시작-2024년-7월"&gt;&lt;a href="#7%ea%b0%9c%ec%9b%94%ec%9d%98-%ea%b3%b5%eb%b0%b1-%ea%b7%b8%eb%a6%ac%ea%b3%a0-%eb%8b%a4%ec%8b%9c-%ec%8b%9c%ec%9e%91-2024%eb%85%84-7%ec%9b%94" class="header-anchor"&gt;&lt;/a&gt;7개월의 공백, 그리고 다시 시작 (2024년 7월)
&lt;/h2&gt;&lt;p&gt;급식 알림은 한동안 손을 대지 못했다. 게시판, 채팅, 로그인 같은 핵심 기능들이 우선이었고, 알림은 &amp;ldquo;나중에 제대로 하자&amp;rdquo; 목록에 들어갔다.&lt;/p&gt;
&lt;p&gt;2024년 7월, 다시 &lt;code&gt;notification_manager.dart&lt;/code&gt;를 열었다. 150줄 이상을 수정하는 대규모 리팩토링이었다. 7개월 전에 작성한 코드를 다시 보니, 당시에는 이해가 부족해서 엉성하게 작성한 부분이 많았다. 알림 채널 설정, 권한 요청, 스케줄링 로직을 전부 다시 썼다.&lt;/p&gt;
&lt;p&gt;하지만 근본적인 문제가 남아 있었다. &lt;strong&gt;앱이 꺼져 있을 때 알림이 안 왔다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;flutter_local_notifications&lt;/code&gt;의 &lt;code&gt;zonedSchedule&lt;/code&gt;은 앱이 살아 있거나 백그라운드에 있을 때는 잘 동작한다. 하지만 사용자가 앱을 강제 종료하거나, 시스템이 메모리 부족으로 앱을 죽이면? 예약된 알림도 같이 사라진다. 특히 Android 제조사(삼성, 샤오미 등)의 배터리 최적화가 공격적으로 백그라운드 프로세스를 죽이는 환경에서, Flutter 앱의 로컬 알림은 불안정했다.&lt;/p&gt;
&lt;p&gt;매일 아침 급식 알림이 &lt;strong&gt;가끔&lt;/strong&gt; 오고 &lt;strong&gt;가끔&lt;/strong&gt; 안 온다. 이건 없느니만 못한 기능이었다.&lt;/p&gt;
&lt;h2 id="3차-시도-kotlin-네이티브로-2024년-8월"&gt;&lt;a href="#3%ec%b0%a8-%ec%8b%9c%eb%8f%84-kotlin-%eb%84%a4%ec%9d%b4%ed%8b%b0%eb%b8%8c%eb%a1%9c-2024%eb%85%84-8%ec%9b%94" class="header-anchor"&gt;&lt;/a&gt;3차 시도: Kotlin 네이티브로 (2024년 8월)
&lt;/h2&gt;&lt;p&gt;&amp;ldquo;Flutter 레벨에서는 한계가 있다. 네이티브로 내려가자.&amp;rdquo;&lt;/p&gt;
&lt;p&gt;2024년 8월 1일, 결정을 내렸다. Android의 네이티브 알림 시스템을 직접 사용하기로 했다. 하루 만에 세 개의 파일을 새로 만들었다:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;MealNotificationReceiver.kt&lt;/code&gt;&lt;/strong&gt; (78줄) — &lt;code&gt;BroadcastReceiver&lt;/code&gt;를 상속. AlarmManager에서 트리거되면 실제 알림을 생성하는 역할&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;MealWorker.kt&lt;/code&gt;&lt;/strong&gt; (61줄) — &lt;code&gt;Worker&lt;/code&gt;를 상속. WorkManager에 등록되어, 시스템이 적절한 시점에 급식 데이터를 가져오고 알림을 예약&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;MainActivity.kt&lt;/code&gt;&lt;/strong&gt; — Flutter와 네이티브 코드를 연결하는 MethodChannel 설정 (+61줄)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;동시에 &lt;code&gt;notification_manager.dart&lt;/code&gt;에서 160줄을 삭제했다. Flutter 측의 알림 로직을 대부분 걷어내고, Kotlin 네이티브에 위임하는 구조로 바꾼 것이다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;2024-08-01 커밋 stat:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; MealNotificationReceiver.kt +78줄 (신규)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; MealWorker.kt +61줄 (신규)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; MainActivity.kt +61줄
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; notification_manager.dart -160줄
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;이 접근의 핵심 아이디어는 이랬다:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;WorkManager&lt;/strong&gt;가 시스템 수준에서 작업을 스케줄링한다. 앱이 죽어도 시스템이 살려서 실행한다&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;MealWorker&lt;/strong&gt;가 NEIS API를 호출해서 급식 데이터를 가져온다&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;MealNotificationReceiver&lt;/strong&gt;가 실제 알림을 사용자에게 보여준다&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Android의 WorkManager는 앱이 종료되어도 시스템이 보장하는 백그라운드 작업이다. 이론적으로는 완벽한 해법이었다.&lt;/p&gt;
&lt;h3 id="그런데-문제가-또-터졌다"&gt;&lt;a href="#%ea%b7%b8%eb%9f%b0%eb%8d%b0-%eb%ac%b8%ec%a0%9c%ea%b0%80-%eb%98%90-%ed%84%b0%ec%a1%8c%eb%8b%a4" class="header-anchor"&gt;&lt;/a&gt;그런데 문제가 또 터졌다
&lt;/h3&gt;&lt;p&gt;이후 일주일간의 커밋 기록을 보면 상황이 얼마나 힘들었는지 알 수 있다:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;2024-08-04 Migration (MealWorker, Receiver 수정)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;2024-08-06 Migration (notification_manager +105줄)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;2024-08-07 Migration (Kotlin + notification_manager 동시 수정)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;2024-08-07 commit (같은 날 또 수정)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;일주일 동안 거의 매일 커밋이 있었고, 같은 날 두 번 커밋한 날도 있었다. 네이티브 코드와 Flutter 코드를 동시에 수정하고 있다는 건, 둘 사이의 통신이 제대로 안 된다는 뜻이었다.&lt;/p&gt;
&lt;p&gt;문제들:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;MethodChannel 통신 불안정&lt;/strong&gt; — Flutter에서 Kotlin으로, Kotlin에서 Flutter로 데이터를 주고받는 과정에서 타이밍 이슈가 발생. 앱이 cold start 상태일 때 채널이 준비되기 전에 호출이 가는 경우가 있었다&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;NEIS API를 Kotlin에서 직접 호출해야 하는 문제&lt;/strong&gt; — Flutter 쪽에 이미 잘 동작하는 MealDataApi가 있는데, 같은 로직을 Kotlin으로 다시 작성해야 했다. 코드 중복에 버그 가능성까지 두 배&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;iOS는?&lt;/strong&gt; — Kotlin으로 네이티브를 작성하면 iOS에서는 별도로 Swift 코드를 작성해야 한다. AppDelegate.swift도 78줄이 추가되었지만, 플랫폼별로 다른 코드를 유지보수하는 건 크로스플랫폼의 이점을 스스로 버리는 것이었다&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;8월 27일, &lt;code&gt;MealNotificationReceiver.kt&lt;/code&gt;에서 줄을 빼기 시작했다. 동시에 Flutter 쪽에 &lt;code&gt;meal_notification_worker.dart&lt;/code&gt;(33줄)를 새로 만들었다. 네이티브에서 다시 Flutter로 로직을 옮기기 시작한 것이다.&lt;/p&gt;
&lt;h2 id="kotlin-포기-2024년-9월"&gt;&lt;a href="#kotlin-%ed%8f%ac%ea%b8%b0-2024%eb%85%84-9%ec%9b%94" class="header-anchor"&gt;&lt;/a&gt;Kotlin 포기 (2024년 9월)
&lt;/h2&gt;&lt;p&gt;2024년 9월 25일의 커밋이 결정적이었다:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;MealNotificationReceiver.kt -65줄 (삭제)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;MealWorker.kt -63줄 (삭제)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;notification_manager.dart +215줄
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;meal_notification_worker.dart 수정
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;Kotlin 네이티브 파일 두 개를 완전히 삭제했다. 2개월 전에 &amp;ldquo;이게 정답이다&amp;quot;라고 확신하며 작성한 코드를 통째로 버렸다. 동시에 &lt;code&gt;notification_manager.dart&lt;/code&gt;에 215줄을 추가하며, 알림 로직을 전부 Flutter로 되돌렸다.&lt;/p&gt;
&lt;p&gt;Java 프로토타입 159커밋을 버릴 때도 그랬지만, 코드를 버리는 건 매번 아프다. 특히 이번에는 &amp;ldquo;Flutter로는 안 되니까 네이티브로 가야 한다&amp;quot;는 나름의 기술적 판단을 했던 것이라 더 그랬다. 그 판단이 틀렸다는 걸 인정하는 것이기도 했으니까.&lt;/p&gt;
&lt;p&gt;하지만 돌아보면 틀린 게 아니라 &lt;strong&gt;맞는 방향을 찾아가는 과정&lt;/strong&gt;이었다. 네이티브로 내려가봤기 때문에 네이티브의 한계와 복잡성을 직접 체감했고, &amp;ldquo;Flutter 안에서 해결하되, 더 똑똑하게 하자&amp;quot;라는 결론에 도달할 수 있었다.&lt;/p&gt;
&lt;h2 id="구조-전환-dailymealnotification-2024년-12월"&gt;&lt;a href="#%ea%b5%ac%ec%a1%b0-%ec%a0%84%ed%99%98-dailymealnotification-2024%eb%85%84-12%ec%9b%94" class="header-anchor"&gt;&lt;/a&gt;구조 전환: DailyMealNotification (2024년 12월)
&lt;/h2&gt;&lt;p&gt;2024년 12월 2일, 대규모 구조 전환을 했다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;notification_manager.dart -253줄 (삭제)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;meal_notification_worker.dart -33줄 (삭제)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;daily_meal_notification.dart +259줄 (신규)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;daily_alarm_notification.dart +82줄 (신규)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;MainActivity.kt -155줄
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;AppDelegate.swift -92줄
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;기존의 &lt;code&gt;notification_manager.dart&lt;/code&gt;(253줄)를 삭제하고, &lt;code&gt;daily_meal_notification.dart&lt;/code&gt;(259줄)를 새로 만들었다. 동시에 &lt;code&gt;MainActivity.kt&lt;/code&gt;에서 155줄, &lt;code&gt;AppDelegate.swift&lt;/code&gt;에서 92줄을 제거했다. 네이티브 쪽의 알림 관련 코드를 전부 걷어낸 것이다.&lt;/p&gt;
&lt;h3 id="matchdatetimecomponents의-발견"&gt;&lt;a href="#matchdatetimecomponents%ec%9d%98-%eb%b0%9c%ea%b2%ac" class="header-anchor"&gt;&lt;/a&gt;&lt;code&gt;matchDateTimeComponents&lt;/code&gt;의 발견
&lt;/h3&gt;&lt;p&gt;이 구조 전환에서 가장 결정적이었던 건 &lt;code&gt;flutter_local_notifications&lt;/code&gt;의 이 옵션이다:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-dart" data-lang="dart"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_localNotificationsPlugin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;zonedSchedule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nl"&gt;matchDateTimeComponents:&lt;/span&gt; &lt;span class="n"&gt;DateTimeComponents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dayOfWeekAndTime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;&lt;code&gt;dayOfWeekAndTime&lt;/code&gt;으로 설정하면, &lt;strong&gt;매주 같은 요일 같은 시간에 반복&lt;/strong&gt;되는 알림을 시스템이 직접 관리한다. 앱이 살아 있든 죽어 있든, 시스템 알람 스케줄러가 처리하기 때문에 안정적이다. &lt;code&gt;exactAllowWhileIdle&lt;/code&gt; 모드와 결합하면 Doze 모드에서도 동작한다.&lt;/p&gt;
&lt;p&gt;1년 전에 이 옵션을 알았으면 Kotlin 네이티브로 내려갈 필요가 없었을지도 모른다. 하지만 이 옵션이 &amp;ldquo;정답&amp;quot;이라는 걸 확신하려면, 다른 방법들이 왜 안 되는지를 직접 경험해봐야 했다.&lt;/p&gt;
&lt;h3 id="그래도-남은-문제"&gt;&lt;a href="#%ea%b7%b8%eb%9e%98%eb%8f%84-%eb%82%a8%ec%9d%80-%eb%ac%b8%ec%a0%9c" class="header-anchor"&gt;&lt;/a&gt;그래도 남은 문제
&lt;/h3&gt;&lt;p&gt;구조는 잡혔지만, 이 버전에는 치명적인 문제가 있었다. &lt;strong&gt;앱을 일정 기간 열지 않으면 알림이 오지 않았다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;원인은 알림 스케줄링 시점에 있었다. 당시 코드는 스케줄링할 때 &lt;strong&gt;미래의 특정 날짜&lt;/strong&gt; 급식을 미리 가져와서 알림 내용에 박아넣는 구조였다:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-dart" data-lang="dart"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// 2024-12 버전 — 스케줄링 시점에 특정 날짜의 급식을 가져옴
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="n"&gt;bigText&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;await&lt;/span&gt; &lt;span class="n"&gt;MealDataApi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;getMeal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nl"&gt;date:&lt;/span&gt; &lt;span class="n"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scheduledDate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;year&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;scheduledDate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;month&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;scheduledDate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;day&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nl"&gt;mealType:&lt;/span&gt; &lt;span class="n"&gt;mealType&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nl"&gt;type:&lt;/span&gt; &lt;span class="n"&gt;MealDataApi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MENU&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;meal&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;급식 정보가 없습니다&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;&lt;code&gt;matchDateTimeComponents: DateTimeComponents.dayOfWeekAndTime&lt;/code&gt;은 매주 반복 알림을 시스템에 등록한다. 하지만 &lt;strong&gt;알림의 내용은 등록 시점에 고정&lt;/strong&gt;된다. 월요일에 스케줄링하면 그 주 월요일 급식 메뉴가 알림에 박히고, 다음 주 월요일에도 똑같은 내용이 표시된다.&lt;/p&gt;
&lt;p&gt;그래서 앱을 열 때마다 &lt;code&gt;scheduleDailyNotifications()&lt;/code&gt;를 호출해서 알림을 새로 등록하는 방식으로 우회했는데, 문제는 &lt;strong&gt;앱을 오래 안 열면 갱신이 안 된다&lt;/strong&gt;는 것이었다. 2주 동안 앱을 안 열면 2주 전 급식 메뉴가 계속 뜨거나, NEIS API에 해당 날짜 데이터가 없어서 &amp;ldquo;급식 정보가 없습니다&amp;quot;만 반복되었다.&lt;/p&gt;
&lt;p&gt;알림이 오긴 오는데 내용이 엉뚱하거나, 아예 의미 없는 메시지가 뜨니까 사용자 입장에서는 &amp;ldquo;알림이 안 온다&amp;quot;와 다름없었다.&lt;/p&gt;
&lt;h2 id="현재-버전으로의-진화-2026년-34월"&gt;&lt;a href="#%ed%98%84%ec%9e%ac-%eb%b2%84%ec%a0%84%ec%9c%bc%eb%a1%9c%ec%9d%98-%ec%a7%84%ed%99%94-2026%eb%85%84-34%ec%9b%94" class="header-anchor"&gt;&lt;/a&gt;현재 버전으로의 진화 (2026년 3~4월)
&lt;/h2&gt;&lt;p&gt;이 문제를 해결하면서 동시에 여러 개선을 적용한 게 현재 버전이다.&lt;/p&gt;
&lt;h3 id="알림-내용-갱신-방식-변경"&gt;&lt;a href="#%ec%95%8c%eb%a6%bc-%eb%82%b4%ec%9a%a9-%ea%b0%b1%ec%8b%a0-%eb%b0%a9%ec%8b%9d-%eb%b3%80%ea%b2%bd" class="header-anchor"&gt;&lt;/a&gt;알림 내용 갱신 방식 변경
&lt;/h3&gt;&lt;p&gt;가장 핵심적인 변경은 급식 데이터를 가져오는 방식이다:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;span class="lnt"&gt;7
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-dart" data-lang="dart"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// 현재 버전 — 항상 오늘 날짜 기준으로 가져옴
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;meal&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;await&lt;/span&gt; &lt;span class="n"&gt;MealDataApi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;getMeal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nl"&gt;date:&lt;/span&gt; &lt;span class="n"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nl"&gt;mealType:&lt;/span&gt; &lt;span class="n"&gt;mealType&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nl"&gt;type:&lt;/span&gt; &lt;span class="n"&gt;MealDataApi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MENU&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;menuPreview&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_cleanMenu&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;meal&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;meal&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;&lt;code&gt;scheduledDate&lt;/code&gt; 대신 &lt;code&gt;DateTime.now()&lt;/code&gt;를 사용한다. 어차피 앱이 열릴 때마다 &lt;code&gt;scheduleDailyNotifications()&lt;/code&gt;가 호출되면서 알림을 전부 취소하고 다시 등록하기 때문에, &lt;strong&gt;가장 최신 급식 데이터로 갱신&lt;/strong&gt;된다. 그리고 데이터를 못 가져왔을 때의 폴백도 달라졌다:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-dart" data-lang="dart"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// 2024-12 버전
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt;&amp;#39;급식 정보가 없습니다&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// 현재 버전
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt;&amp;#39;오늘의 &lt;/span&gt;&lt;span class="si"&gt;$&lt;/span&gt;&lt;span class="n"&gt;mealLabel&lt;/span&gt;&lt;span class="s1"&gt; 메뉴를 확인하세요&amp;#39;&lt;/span&gt; &lt;span class="c1"&gt;// (i18n 적용 후: l.noti_mealConfirm(mealLabel))
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;데이터가 없을 때 &amp;ldquo;정보가 없다&amp;quot;고 보여주는 대신, 앱을 열어보도록 유도하는 문구로 바꿨다. 월이 바뀔 때 데이터가 없거나, 예약 시점에 인터넷이 연결되지 않았을 수 있기 때문이다.&lt;/p&gt;
&lt;h3 id="알러지-정보-정리"&gt;&lt;a href="#%ec%95%8c%eb%9f%ac%ec%a7%80-%ec%a0%95%eb%b3%b4-%ec%a0%95%eb%a6%ac" class="header-anchor"&gt;&lt;/a&gt;알러지 정보 정리
&lt;/h3&gt;&lt;p&gt;NEIS API에서 내려오는 급식 메뉴에는 알러지 번호가 붙어 있다. &lt;code&gt;비빔밥(5.6.13)&lt;/code&gt; 이런 식으로. 알림에 이게 그대로 들어가면 지저분하다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;span class="lnt"&gt;7
&lt;/span&gt;&lt;span class="lnt"&gt;8
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-dart" data-lang="dart"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="n"&gt;_cleanMenu&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;menu&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;menu&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;menu&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isEmpty&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;menu&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;replaceAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;RegExp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;r&amp;#39;\([0-9.,\s]+\)&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;where&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isNotEmpty&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39; · &amp;#39;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;&lt;code&gt;_cleanMenu()&lt;/code&gt;가 알러지 괄호를 제거하고, 줄바꿈을 &lt;code&gt;·&lt;/code&gt;로 연결해서 한 줄 미리보기를 만든다. &amp;ldquo;비빔밥 · 미역국 · 배추김치 · 요구르트&amp;rdquo; 이런 깔끔한 형태로.&lt;/p&gt;
&lt;h3 id="알림-탭--급식-화면-이동"&gt;&lt;a href="#%ec%95%8c%eb%a6%bc-%ed%83%ad--%ea%b8%89%ec%8b%9d-%ed%99%94%eb%a9%b4-%ec%9d%b4%eb%8f%99" class="header-anchor"&gt;&lt;/a&gt;알림 탭 → 급식 화면 이동
&lt;/h3&gt;&lt;p&gt;2024-12 버전에서는 알림을 탭해도 아무 일도 안 일어났다. 그냥 앱이 열리거나, 아예 반응이 없었다.&lt;/p&gt;
&lt;p&gt;현재 버전은 &lt;code&gt;payload&lt;/code&gt;를 활용한다:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-dart" data-lang="dart"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_localNotificationsPlugin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;zonedSchedule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nl"&gt;payload:&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;meal_screen&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;알림을 탭하면 &lt;code&gt;notificationStream&lt;/code&gt;에 &lt;code&gt;'meal_screen'&lt;/code&gt;이 전달되고, 앱이 이를 받아서 급식 화면으로 바로 이동한다. 앱이 꺼져 있었더라도 cold start 후 급식 화면까지 자동으로 네비게이션된다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-dart" data-lang="dart"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;_onNotificationTap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NotificationResponse&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;Notification tapped: &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;notificationStream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;h3 id="권한-요청-분리"&gt;&lt;a href="#%ea%b6%8c%ed%95%9c-%ec%9a%94%ec%b2%ad-%eb%b6%84%eb%a6%ac" class="header-anchor"&gt;&lt;/a&gt;권한 요청 분리
&lt;/h3&gt;&lt;p&gt;2024-12 버전에서는 알림 초기화 과정에서 바로 권한을 요청했다. 앱을 처음 열자마자 &amp;ldquo;알림을 허용하시겠습니까?&amp;rdquo; 팝업이 뜨는 방식이었는데, 맥락 없이 갑자기 뜨는 권한 요청은 거부율이 높다.&lt;/p&gt;
&lt;p&gt;현재 버전에서는 &lt;code&gt;_requestPermissions()&lt;/code&gt;를 &lt;code&gt;DailyMealNotification&lt;/code&gt; 클래스에서 제거하고, &lt;strong&gt;설정 화면에서 알림을 켤 때 바텀시트로 권한을 요청&lt;/strong&gt;하는 방식으로 바꿨다. 사용자가 &amp;ldquo;급식 알림을 받겠다&amp;quot;는 의도를 먼저 표현한 상태에서 권한을 요청하니까 허용율이 훨씬 높다.&lt;/p&gt;
&lt;h3 id="다국어-지원"&gt;&lt;a href="#%eb%8b%a4%ea%b5%ad%ec%96%b4-%ec%a7%80%ec%9b%90" class="header-anchor"&gt;&lt;/a&gt;다국어 지원
&lt;/h3&gt;&lt;p&gt;하드코딩되어 있던 한국어 문자열을 전부 &lt;code&gt;AppLocalizations&lt;/code&gt;로 교체했다:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-dart" data-lang="dart"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// 2024-12 버전
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt;&amp;#39;🍽️ &lt;/span&gt;&lt;span class="si"&gt;$&lt;/span&gt;&lt;span class="n"&gt;mealLabel&lt;/span&gt;&lt;span class="s1"&gt; 알림&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt;&amp;#39;급식 정보 알림을 제공합니다.&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt;&amp;#39;한솔고등학교&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// 현재 버전
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;noti_mealBreakfast&lt;/span&gt; &lt;span class="c1"&gt;// 알림 제목
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;noti_mealChannelName&lt;/span&gt; &lt;span class="c1"&gt;// 채널 이름
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;noti_mealChannelDesc&lt;/span&gt; &lt;span class="c1"&gt;// 채널 설명
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;noti_schoolName&lt;/span&gt; &lt;span class="c1"&gt;// 학교 이름
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;영어를 사용하는 학생도 알림을 이해할 수 있게 되었다.&lt;/p&gt;
&lt;h3 id="조식중식석식-개별-설정"&gt;&lt;a href="#%ec%a1%b0%ec%8b%9d%ec%a4%91%ec%8b%9d%ec%84%9d%ec%8b%9d-%ea%b0%9c%eb%b3%84-%ec%84%a4%ec%a0%95" class="header-anchor"&gt;&lt;/a&gt;조식·중식·석식 개별 설정
&lt;/h3&gt;&lt;p&gt;사용자가 원하는 끼니만 골라서 알림을 받을 수 있다. 기숙사생은 조식·중식·석식 전부, 통학생은 중식만. 시간도 각각 다르게 설정 가능하다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;span class="lnt"&gt;7
&lt;/span&gt;&lt;span class="lnt"&gt;8
&lt;/span&gt;&lt;span class="lnt"&gt;9
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-dart" data-lang="dart"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isBreakfastNotificationOn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_scheduleWeeklyNotification&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nl"&gt;id:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nl"&gt;mealLabel:&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;meal_breakfast&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nl"&gt;notiTitle:&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;noti_mealBreakfast&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nl"&gt;time:&lt;/span&gt; &lt;span class="n"&gt;_parseTimeOfDay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;breakfastTime&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nl"&gt;weekdays:&lt;/span&gt; &lt;span class="n"&gt;weekdays&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;월~금 각 요일별로 별도의 알림 ID를 부여해서 (조식×5 + 중식×5 + 석식×5 = 최대 15개) 개별적으로 관리한다. 주말에는 급식이 없으니까 토·일은 제외.&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;📎 알림 기능의 전체 구조는 &lt;a class="link" href="https://monkshark.github.io/hansol_hs_flutter_app/#notification/daily_meal_notification.md" target="_blank" rel="noopener"
 &gt;DailyMealNotification 문서&lt;/a&gt;에서 확인할 수 있다.&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;h3 id="2024-12-버전-vs-현재-버전"&gt;&lt;a href="#2024-12-%eb%b2%84%ec%a0%84-vs-%ed%98%84%ec%9e%ac-%eb%b2%84%ec%a0%84" class="header-anchor"&gt;&lt;/a&gt;2024-12 버전 vs 현재 버전
&lt;/h3&gt;&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;항목&lt;/th&gt;
 &lt;th&gt;2024-12 버전&lt;/th&gt;
 &lt;th&gt;현재 버전&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;급식 데이터&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;scheduledDate&lt;/code&gt; 기준 (고정)&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;DateTime.now()&lt;/code&gt; 기준 (갱신)&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;데이터 없을 때&lt;/td&gt;
 &lt;td&gt;&amp;ldquo;급식 정보가 없습니다&amp;rdquo;&lt;/td&gt;
 &lt;td&gt;&amp;ldquo;메뉴를 확인하세요&amp;rdquo; (유도)&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;알러지 정보&lt;/td&gt;
 &lt;td&gt;그대로 노출&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;_cleanMenu()&lt;/code&gt;로 제거&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;알림 탭&lt;/td&gt;
 &lt;td&gt;반응 없음&lt;/td&gt;
 &lt;td&gt;급식 화면으로 딥링크&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;권한 요청&lt;/td&gt;
 &lt;td&gt;앱 시작 시 즉시&lt;/td&gt;
 &lt;td&gt;설정 바텀시트에서 맥락적으로&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;언어&lt;/td&gt;
 &lt;td&gt;한국어 하드코딩&lt;/td&gt;
 &lt;td&gt;i18n (한국어 + 영어)&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;디버깅&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;print()&lt;/code&gt; 남발&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;log()&lt;/code&gt; 정리&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;테스트&lt;/td&gt;
 &lt;td&gt;없음&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;sendTestNotification()&lt;/code&gt; 제공&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;코드 줄 수는 259줄에서 239줄로 오히려 줄었다. 기능은 훨씬 많아졌는데 코드가 줄어든 건, 권한 요청 로직을 분리하고 &lt;code&gt;print()&lt;/code&gt; 디버깅 코드를 정리한 덕분이다.&lt;/p&gt;
&lt;h2 id="1년-반의-기록"&gt;&lt;a href="#1%eb%85%84-%eb%b0%98%ec%9d%98-%ea%b8%b0%eb%a1%9d" class="header-anchor"&gt;&lt;/a&gt;1년 반의 기록
&lt;/h2&gt;&lt;p&gt;급식 알림 하나를 만드는 데 거친 경로를 정리하면:&lt;/p&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;시기&lt;/th&gt;
 &lt;th&gt;접근 방식&lt;/th&gt;
 &lt;th&gt;결과&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;2023-12&lt;/td&gt;
 &lt;td&gt;NotificationManager + FCM&lt;/td&gt;
 &lt;td&gt;FCM은 서버 필요 → 당일 삭제&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;2023-12&lt;/td&gt;
 &lt;td&gt;ScheduledNotification (로컬)&lt;/td&gt;
 &lt;td&gt;기본 동작은 하지만 불안정&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;2024-07&lt;/td&gt;
 &lt;td&gt;notification_manager 리팩토링&lt;/td&gt;
 &lt;td&gt;앱 종료 시 알림 누락&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;2024-08&lt;/td&gt;
 &lt;td&gt;Kotlin 네이티브 (WorkManager)&lt;/td&gt;
 &lt;td&gt;플랫폼별 코드 중복, 통신 복잡&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;2024-09&lt;/td&gt;
 &lt;td&gt;Kotlin 삭제, Flutter 복귀&lt;/td&gt;
 &lt;td&gt;구조 정리 시작&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;2024-12&lt;/td&gt;
 &lt;td&gt;DailyMealNotification 구조 전환&lt;/td&gt;
 &lt;td&gt;동작하지만 장기 미접속 시 내용 갱신 불가&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;2026-03~04&lt;/td&gt;
 &lt;td&gt;현재 버전&lt;/td&gt;
 &lt;td&gt;딥링크, i18n, 메뉴 프리뷰, 권한 UX 개선&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;파일 생성과 삭제 횟수를 세면:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;FirebaseCloudMessaging.dart&lt;/code&gt; → 생성 → 삭제&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ScheduledNotification.dart&lt;/code&gt; → 생성 → 삭제&lt;/li&gt;
&lt;li&gt;&lt;code&gt;MealNotificationReceiver.kt&lt;/code&gt; → 생성 → 삭제&lt;/li&gt;
&lt;li&gt;&lt;code&gt;MealWorker.kt&lt;/code&gt; → 생성 → 삭제&lt;/li&gt;
&lt;li&gt;&lt;code&gt;meal_notification_worker.dart&lt;/code&gt; → 생성 → 삭제&lt;/li&gt;
&lt;li&gt;&lt;code&gt;notification_manager.dart&lt;/code&gt; → 생성 → 수차례 대규모 수정 → 삭제&lt;/li&gt;
&lt;li&gt;&lt;code&gt;daily_meal_notification.dart&lt;/code&gt; → 최종 생존&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;6개의 파일이 만들어졌다 사라졌고, 1개만 살아남았다.&lt;/p&gt;
&lt;h2 id="배운-것"&gt;&lt;a href="#%eb%b0%b0%ec%9a%b4-%ea%b2%83" class="header-anchor"&gt;&lt;/a&gt;배운 것
&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;&amp;ldquo;간단해 보이는 기능&amp;quot;은 없다.&lt;/strong&gt; 사용자에게 간단하게 보이는 기능일수록 뒤에서 처리해야 할 것이 많다. &amp;ldquo;매일 아침 알림 보내기&amp;quot;라는 한 문장 뒤에는 타임존, 배터리 최적화, 백그라운드 제약, 플랫폼별 차이, API 호출 타이밍 같은 문제들이 숨어 있었다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;네이티브로 내려가는 건 최후의 수단이어야 한다.&lt;/strong&gt; 크로스플랫폼 프레임워크를 쓰면서 네이티브 코드를 작성하는 순간, 유지보수 비용이 플랫폼 수만큼 곱해진다. 특히 1인 개발에서는 치명적이다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&amp;ldquo;동작한다&amp;quot;와 &amp;ldquo;제대로 동작한다&amp;quot;는 다르다.&lt;/strong&gt; 2024년 12월 버전은 동작했다. 알림이 왔다. 하지만 일정 기간 앱을 안 열면 내용이 갱신되지 않았고, 알림을 탭해도 아무 일도 안 일어났고, 권한 요청 타이밍이 나빴다. 기능이 &amp;ldquo;있는&amp;rdquo; 것과 &amp;ldquo;쓸 만한&amp;rdquo; 것 사이에는 이런 디테일들이 잔뜩 있다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;삽질은 낭비가 아니다.&lt;/strong&gt; FCM을 시도해봤기 때문에 서버 푸시와 로컬 알림의 차이를 이해했고, Kotlin 네이티브를 경험했기 때문에 Flutter의 한계와 가능성을 정확히 알게 되었다. 최종 코드 239줄에는 1년 반의 시행착오가 전부 녹아 있다.&lt;/p&gt;</description></item></channel></rss>