Windows 앱의 작업 트레이 상주와 토스트 알림 ── NotifyIcon의 함정과 AppNotification 선택 기준
· Go Komura · 작업 트레이, NotifyIcon, 토스트 알림, AppNotification, Windows App SDK, WinForms, WPF, C#, .NET, 상주 애플리케이션, Windows 개발, 기술 상담
“작업이 끝나면 사용자에게 은근히 알리고 싶다”, “창을 닫아도 모니터링은 계속하고 싶다” ── 업무용 앱에서는 작업 트레이(알림 영역) 상주와 토스트 알림이 세트로 등장하는 단골 요구사항입니다. 그런데 막상 구현해 보면, 닫혔어야 할 앱이 종료되지 않는다거나, 탐색기(Explorer)를 재시작했더니 아이콘이 사라진다거나, 개발 중에는 나오던 토스트가 배포처에서는 표시되지 않는다는 등 자잘한 문제가 계속해서 나타납니다.
게다가 토스트 알림은 UWP 시절부터 API 세대교체가 계속되어 온 영역입니다. 검색해서 나오는 샘플 코드가 지금 시점에서는 유지보수만 이루어지는 오래된 API였다는 일도 드물지 않습니다. 이 글에서는 NotifyIcon을 이용한 트레이 상주 설계와, 2026년 시점의 토스트 알림 API 선택 기준을, 실무에서 마주치는 함정과 함께 정리합니다.
1. 먼저 결론부터
- 트레이 상주의 기본은 지금도
System.Windows.Forms.NotifyIcon입니다.Icon을 설정하고Visible = true로 하지 않으면 표시되지 않습니다(기본값은false). 툴팁의Text는 .NET 6 이후 최대 127자입니다(그 이전에는 63자).12 - WPF에는 NotifyIcon에 해당하는 컨트롤이 없습니다. 공식 대조표에도 “동등한 컨트롤 없음”이라고 명시되어 있습니다. csproj에서
UseWPF와UseWindowsForms를 함께 사용해 WinForms의 NotifyIcon을 쓰는 것이 최소 구성입니다.34 - “닫기 버튼을 눌러도 트레이에 남긴다”는
FormClosing에서e.Cancel = true로 설정하고Hide()를 호출하는 설계입니다. 반드시 명시적인 종료 수단(트레이 메뉴의 “종료”)을 별도로 마련하고,CloseReason을 확인해 OS의 종료(셧다운)까지 방해하지 않도록 합니다.5 - 토스트 알림 API는 2026년 시점의 공식 권장이 명확합니다. WinForms / WPF / 패키징되지 않은 앱 어느 쪽이든 Windows App SDK의
AppNotificationManager(NuGet: Microsoft.WindowsAppSDK)가 권장됩니다.Windows.UI.Notifications는 “유지보수 전용”, Community Toolkit의 알림 컴포넌트는 보관 처리되었습니다.67 - Windows App SDK의 AppNotification은 패키지 ID 없는 일반 exe에서도 사용할 수 있습니다.
AppNotificationManager.Default.Register()가 COM 서버 등록과 AUMID에 해당하는 처리를 자동으로 수행하기 때문에, 이전 세대에서 필요했던 시작 메뉴 바로가기 준비가 필요 없습니다. 다만 관리자 권한으로 상승된 프로세스에서의 알림은 지원되지 않습니다.8910 - 토스트는 “반드시 사용자 눈에 들어오는” 수단이 아닙니다. 방해 금지(Focus Assist) 중에는 배너로 표시되지 않고 곧바로 알림 센터로 이동하며, 설정에서 알림 자체를 끌 수도 있습니다. “알림이 표시되었다”를 업무 흐름의 전제로 삼지 않는 설계가 필요합니다.1112
- Windows 서비스에서는 토스트를 보낼 수 없습니다(세션 0에는 UI가 없습니다). 사용자 세션 쪽에 트레이 상주 에이전트를 두고 IPC로 연계하는 것이 정석입니다.13
2. NotifyIcon을 이용한 트레이 상주의 기본
작업 표시줄 오른쪽 끝의 알림 영역에 아이콘을 표시하는 컴포넌트가 System.Windows.Forms.NotifyIcon입니다. 백그라운드에서 계속 동작하는 앱이 상태 표시와 조작 메뉴의 진입점으로 사용합니다.1
private NotifyIcon _trayIcon = null!;
private void InitializeTrayIcon()
{
var menu = new ContextMenuStrip();
menu.Items.Add("열기", null, (_, _) => ShowMainWindow());
menu.Items.Add(new ToolStripSeparator());
menu.Items.Add("종료", null, (_, _) => ExitApplication());
_trayIcon = new NotifyIcon
{
Icon = Properties.Resources.AppIcon, // Icon은 필수입니다. 설정하지 않으면 표시되지 않습니다
Text = "수주 모니터링 도구 ── 실행 중", // 툴팁(.NET 6+에서 최대 127자)
ContextMenuStrip = menu,
Visible = true // 기본값은 false입니다. true로 설정해야 표시됩니다
};
_trayIcon.DoubleClick += (_, _) => ShowMainWindow();
}
짚고 넘어가야 할 포인트는 세 가지입니다.
Icon과Visible = true가 모두 갖춰져야 비로소 표시됩니다.Visible의 기본값은false입니다.1Text에는 길이 제한이 있습니다. .NET 6 이후에는 최대 127자, .NET 5 이전과 .NET Framework는 63자이며, 초과하면ArgumentException이 발생합니다. 상태 정보를 툴팁에 너무 많이 욱여넣는 구현은 어느 순간 갑자기 예외로 죽는 원인이 됩니다.214- 오른쪽 클릭 메뉴는
ContextMenuStrip으로 연결합니다. 트레이 아이콘은 사용자에게 보이는 유일한 진입점이 되는 경우가 많으므로, “열기”와 “종료”는 최소한 마련해 둡니다.15
한 가지 더, 트레이 아이콘은 알림의 진입점인 동시에 항상 보이는 상태 표시이기도 합니다. Icon과 Text는 실행 중에도 바꿀 수 있으므로, “정상일 때는 일반 아이콘, 이상 발생 시에는 경고 아이콘”, “툴팁에 마지막 처리 시각을 표시” 처럼 현재 상태를 반영해 두면, 사용자가 토스트를 놓치더라도 아이콘을 보면 지금 상태를 알 수 있습니다. 토스트가 “그 순간의 사건을 알리는” 수단이라면, 트레이 아이콘은 “현재 상태를 계속 보여주는” 수단이라는 역할 분담입니다.
3. “닫아도 트레이에 남는다” 설계와 올바른 종료 경로
상주 앱의 단골 동작이 “× 버튼으로 닫아도 프로세스는 종료되지 않고 트레이에 남는다”는 것입니다. 구현 자체는 FormClosing의 취소로 간단히 작성할 수 있지만, 대충 작성하면 앱을 종료할 수단이 없어진다, Windows의 종료(셧다운)를 방해한다는 두 가지 사고로 이어집니다.
private bool _exitRequested;
protected override void OnFormClosing(FormClosingEventArgs e)
{
// 사용자가 ×를 눌렀을 뿐이라면 닫지 않고 숨긴다
if (!_exitRequested && e.CloseReason == CloseReason.UserClosing)
{
e.Cancel = true;
Hide();
return;
}
// '종료' 메뉴를 통하거나 OS 종료 등의 경우는 그대로 닫는다
base.OnFormClosing(e);
}
private void ExitApplication()
{
_exitRequested = true;
_trayIcon.Visible = false;
_trayIcon.Dispose();
Close();
}
FormClosing은 폼이 닫히기 전에 발생하며, e.Cancel = true로 닫기를 취소할 수 있습니다. 이때 반드시 CloseReason을 확인하세요. UserClosing(사용자 조작)일 때만 취소하고, WindowsShutDown 등 OS에 의한 종료는 취소하지 않는 것이 올바른 동작입니다.516
WPF에서도 사고방식은 같아서, Window.Closing에서 e.Cancel = true로 설정하고 Hide()를 호출하는 패턴이 공식 문서에도 그대로 실려 있습니다.17
또한 상주 앱은 그 성격상 “중복 실행되면 같은 이벤트를 이중으로 처리한다”는 문제와 항상 맞닿아 있습니다. 중복 실행 방지는 “Windows 앱의 중복 실행 방지 ── 명명된 뮤텍스와 이중 실행 시 활성화“에서 정리했습니다. 또한 상주 앱의 내부(모니터링 루프나 주기적 처리)를 만드는 방법은 “.NET Generic Host와 BackgroundService를 데스크톱 앱에서 사용하는 이유“가 토대가 됩니다.
4. WPF 앱에서의 트레이 상주
WPF에는 NotifyIcon에 해당하는 컨트롤이 없습니다. 이는 공식 “WinForms 컨트롤과 동등한 WPF 컨트롤” 대조표에 “동등한 컨트롤 없음(No equivalent control)”이라고 명시되어 있는, 사양상의 공백입니다.3
현실적인 선택지는 두 가지입니다.
| 선택지 | 개요 | 판단 기준 |
|---|---|---|
| WinForms의 NotifyIcon 병용 | csproj에서 UseWPF + UseWindowsForms를 모두 활성화 |
추가 의존성 없음. 아이콘과 메뉴만이면 충분 |
| 서드파티(H.NotifyIcon 등) | WPF/WinUI용 트레이 아이콘 구현 | XAML로 작성하고 싶거나 화려한 팝업이 필요할 때. Microsoft 비공식이라는 점은 고려 |
전자는 SDK 스타일 프로젝트 파일에 두 속성을 함께 적어 주기만 하면 됩니다.4
<PropertyGroup>
<TargetFramework>net8.0-windows</TargetFramework>
<UseWPF>true</UseWPF>
<UseWindowsForms>true</UseWindowsForms>
</PropertyGroup>
System.Windows.Forms.NotifyIcon을 WPF의 Application에서 그대로 new해서 사용할 수 있습니다. 메뉴만 WinForms(ContextMenuStrip)가 되지만, 트레이 메뉴 정도라면 실무상 문제가 되는 경우는 거의 없습니다.
5. 트레이 아이콘의 함정 3가지
5.1. 아이콘은 기본값으로 “숨겨진 아이콘”에 들어간다 ── 강제 표시는 불가능
자신의 아이콘을 작업 표시줄에 항상 표시하고 싶다는 요청은 반드시 나오지만, 프로그램으로는 실현할 수 없습니다. 공식 문서에 아이콘은 기본값으로 오버플로(숨겨진 아이콘) 영역에 추가되며, 알림 영역으로의 승격(항상 표시)을 결정할 수 있는 것은 사용자뿐이라고 명시되어 있습니다.18
Windows 11에서는 “설정 → 개인 설정 → 작업 표시줄 → 기타 시스템 트레이 아이콘”에서 사용자가 앱별로 켜고 끕니다. 항상 표시가 운영상 중요하다면, 설치 안내서나 도움말에 “이 설정을 켜 주세요”라고 적는 것이 올바른 대응입니다.
5.2. 탐색기가 재시작되면 아이콘이 사라진다 ── TaskbarCreated
탐색기가 크래시나 재시작으로 작업 표시줄을 다시 만들면, 등록되어 있던 트레이 아이콘은 사라집니다. 셸은 작업 표시줄 생성 시 TaskbarCreated 메시지를 모든 최상위 창에 브로드캐스트하므로, 이를 받으면 아이콘을 다시 등록하는 것이 Win32 수준의 규칙입니다.19
WinForms의 NotifyIcon은 이 메시지를 내부적으로 처리해 자동으로 다시 등록하므로, 보통은 신경 쓸 필요가 없습니다. 직접 Shell_NotifyIcon을 P/Invoke하는 경우(또는 트레이 아이콘을 가진 네이티브 앱을 유지보수하는 경우)는 이 재등록 처리가 들어가 있는지 확인하세요. “탐색기를 재시작하면 아이콘만 사라지고, 프로세스는 살아 있다”는 증상은 대부분 이것입니다.
5.3. Dispose를 잊으면 아이콘이 남는다
Win32적으로는 트레이 아이콘이 필요 없어지면 명시적으로 삭제(NIM_DELETE)해야 하며, WinForms에서는 이를 NotifyIcon.Dispose()가 담당합니다.18 종료 경로에서 Visible = false → Dispose()를 확실히 거치도록 하세요. 프로세스가 비정상 종료되는 등의 이유로 삭제되지 않은 채 남으면, 마우스 커서를 올릴 때까지 유령 아이콘이 계속 남는 광경은 상주 앱 개발자라면 한 번쯤 보게 됩니다.
3장의 코드처럼, “종료” 메뉴 처리에서 트레이 아이콘을 정리한 뒤 Close()하는 형태로 만들어 두는 것이 안전합니다.
6. 토스트 알림 ── 어떤 API를 사용할 것인가
토스트 알림은 화면 오른쪽 아래에 배너로 표시되고, 알림 센터에 남는 알림입니다. 데스크톱 앱에서 이를 보내는 방법은 지난 10년 동안 계속 세대교체가 이루어져 왔고, 검색 결과에는 오래된 샘플이 대량으로 남아 있습니다. 2026년 시점의 정리는 다음과 같습니다.6
| API | NuGet / 네임스페이스 | 위치(2026년 기준) | 판단 |
|---|---|---|---|
| Windows App SDK AppNotification | Microsoft.WindowsAppSDK / Microsoft.Windows.AppNotifications |
WinForms・WPF・패키징되지 않은 Win32 모두에 공식 권장 | 신규는 이것 |
| Community Toolkit Notifications | Microsoft.Toolkit.Uwp.Notifications | 보관(archive) 처리됨. 현재의 Community Toolkit에는 포함되어 있지 않으며, 마지막 업데이트는 2022년 | 기존 코드 유지만. 신규 채택 금지 |
| WinRT 직접 사용 | Windows.UI.Notifications(Windows TFM 경유) |
유지보수 전용. 권장 대상은 UWP | 신규 채택 금지 |
| NotifyIcon.ShowBalloonTip | System.Windows.Forms |
풍선 알림 API. Windows 10에서는 토스트로 표시되지만, Windows 11에서는 일시적으로 표시되고 알림 센터에는 남지 않음 | 간단한 용도라면 가능. 동작이 OS에 따라 달라지는 점에 주의 |
보충 사항이 두 가지 있습니다.
ShowBalloonTip은 “오래된 API지만 사실 살아 있는” 부류입니다. Windows 10에서는 풍선이 토스트로 렌더링되어 닫을 때까지 알림 센터에 남습니다. 반면 Windows 11에서는 기존 풍선에 가까운 일시적인 표시가 되며, 사라진 후에는 알림 센터에 남지 않습니다. 표시 시간(timeout) 지정도 현재는 무시되며, OS 쪽의 접근성 설정을 따릅니다. 트레이 상주 앱이 자신의 상태 변화를 가볍게 알리는 정도라면 충분하지만, “알림 센터에 이력을 남기고 싶다”면 Windows 11에서는 도움이 되지 않는다는 점을 기억해 두세요.20- 순수한
Windows.UI.Notifications를 데스크톱 앱에서 사용하는 고전적인 방법에는, AUMID(AppUserModelID)가 붙은 바로가기를 시작 메뉴에 등록해 두지 않으면 토스트가 표시되지 않는다는 유명한 제약이 있습니다.21 이 번거로움을 해소한 것이 다음 장의 AppNotification입니다.
7. Windows App SDK AppNotification의 최소 구현
Windows App SDK의 앱 알림 API는 패키징되지 않은 일반 exe에서도, 패키지 ID 없이 동작합니다.8 프로젝트 측 전제 조건은 Windows 전용 TFM(예: net8.0-windows10.0.19041.0), <WindowsPackageType>None</WindowsPackageType>(패키징하지 않는 경우), 그리고 배포처에 Windows App SDK 런타임이 설치되어 있는 것입니다.9
using Microsoft.Windows.AppNotifications;
using Microsoft.Windows.AppNotifications.Builder;
// 시작 시 한 번만. 핸들러 등록 → Register() 순서가 중요
AppNotificationManager.Default.NotificationInvoked += (_, args) =>
{
// 토스트 클릭이나 버튼 누름이 여기로 전달된다(인수는 args.Arguments로 판별)
if (args.Arguments.TryGetValue("action", out var action) && action == "openLog")
{
ShowLogWindow();
}
};
AppNotificationManager.Default.Register();
// 알림 표시
var notification = new AppNotificationBuilder()
.AddText("수주 가져오기가 완료되었습니다")
.AddText("신규 12건, 오류 1건")
.AddButton(new AppNotificationButton("로그 열기")
.AddArgument("action", "openLog"))
.BuildNotification();
AppNotificationManager.Default.Show(notification);
실무상 주의할 점은 다음과 같습니다.
NotificationInvoked구독은Register()보다 먼저 합니다. 순서를 잘못하면, 토스트 클릭으로 앱이 시작되었을 때 이벤트를 놓칠 수 있습니다.9- 앱 종료 전에는
AppNotificationManager.Default.Unregister()를 호출합니다. 공식 문서에 종료 전 Unregister로 리소스를 정리해 두면 이후의 알림 클릭 시 앱이 올바르게 시작된다고 명시되어 있습니다. 3장의 “종료” 메뉴 처리에NotifyIcon.Dispose()와 나란히 넣어 두세요(알림 기능을 앞으로 전혀 사용하지 않는 앱을 제거하는 경우 등에는, 등록 정보까지 지우는UnregisterAll()이 별도로 마련되어 있습니다).22 - 패키징되지 않은 앱에서는
Register()가 COM 서버 등록・AUMID에 해당하는 처리를 자동으로 수행합니다. 이전 세대(순수 WinRT)에서 필요했던 바로가기 설치나 레지스트리 등록은 필요 없습니다. MSIX로 패키징하는 경우에는 반대로 Package.appxmanifest에 COM 액티베이터 기재가 필요해집니다.9 - Windows App SDK 런타임을 동봉하는 self-contained 배포를 선택하는 경우에는 주의가 필요합니다.
AppNotificationManager는 Windows App SDK의 Singleton 패키지에 의존하고 있으며, self-contained 앱에서 호출할 때의 고려 사항이 공식적으로 안내되어 있습니다. 배포 방식을 결정하기 전에 확인하세요.23 - 관리자 권한으로 상승된 프로세스에서의 알림은 지원되지 않습니다.
Show는 예외를 발생시키지 않고 조용히 실패하므로, 원인을 알아차리기 어려운 문제입니다. 권한 상승이 필요한 앱은, 알림만 비상승 프로세스에 맡기는 분리를 검토하세요(이 분리 방식에 관한 사고방식은 “Windows 앱에서 「관리자 권한이 필요한 처리만」을 분리하는 구체적인 방법“에서 다루었습니다).10 - 토스트가 클릭되었을 때, 앱이 실행 중이지 않으면 프로세스가 새로 시작됩니다. 트레이 상주 앱에서는 중복 실행 방지(3장 참조)와 조합해, 기존 인스턴스로 처리를 넘기는 설계로 만들어 둘 필요가 있습니다.
8. 한 걸음 더 나아간 사용법 ── 진행률 표시줄이 있는 토스트와 알림 정리
8.1. 진행률을 토스트 안에서 업데이트하기 ── UpdateAsync
“주문 데이터 120건을 가져오는 중”과 같은 긴 처리의 진행 상황은, 알림을 계속 새로 내보내는 것이 아니라 한 장의 토스트 안에 있는 진행률 표시줄을 다시 쓰는 것이 올바른 방법입니다. AppNotification에는 이를 위한 데이터 바인딩과 업데이트 메커니즘이 있습니다.24
구성은 3단계입니다. 먼저 진행률 표시줄의 각 값을 Bind 계열 메서드로 플레이스홀더로 만들고, Tag(필요하면 Group도)로 알림을 식별할 수 있게 해서 표시합니다. 그 후에는 AppNotificationProgressData를 UpdateAsync에 전달하기만 하면, 이미 표시된 토스트의 내용만 다시 쓰입니다.
const string tag = "import-progress";
const string group = "batch";
// 최초 표시: 값을 바인딩하고, 초기값을 Progress에 설정
var notification = new AppNotificationBuilder()
.AddText("수주 데이터를 가져오는 중입니다")
.AddProgressBar(new AppNotificationProgressBar()
.BindStatus().BindValue().BindValueStringOverride())
.SetTag(tag)
.SetGroup(group)
.BuildNotification();
notification.Progress = new AppNotificationProgressData(1) // 시퀀스 번호
{
Status = "가져오는 중...", Value = 0, ValueStringOverride = "0/120건"
};
AppNotificationManager.Default.Show(notification);
// 진행 상황이 갱신될 때마다: 시퀀스 번호를 증가시켜 업데이트
var data = new AppNotificationProgressData(2)
{
Status = "가져오는 중...", Value = 45 / 120.0, ValueStringOverride = "45/120건"
};
await AppNotificationManager.Default.UpdateAsync(data, tag, group);
시퀀스 번호는 업데이트 순서를 나타내는 0이 아닌 정수이며, 여러 업데이트가 뒤섞여 도착한 경우 가장 큰 번호가 표시됩니다. 비동기 처리에서 병행하여 업데이트를 보내더라도 오래된 값으로 되돌아가지 않도록 하기 위한 장치이므로, 단조 증가하도록 하세요.25
8.2. “업데이트”와 “교체”의 구분
같은 Tag로 새 알림을 Show하는 “교체”로도 겉모습은 업데이트할 수 있지만, 동작이 다릅니다.24
| 교체(같은 Tag로 재Show) | 업데이트(UpdateAsync) | |
|---|---|---|
| 알림 센터 내 위치 | 맨 위로 이동 | 그 자리에 머무름 |
| 변경 가능한 내용 | 레이아웃을 포함한 전체 | 바인딩한 진행률 표시줄과 상단 텍스트만 |
| 팝업 | 다시 표시 가능 | 다시 표시되지 않음(조용히 다시 씀) |
| 사용자가 이미 닫은 경우 | 항상 전송됨 | 실패함 |
공식 문서의 권장 사항도 명확합니다. 중간 경과는 업데이트, 완료는 교체입니다. 진행률이 50%에서 65%가 된 것을 굳이 팝업으로 알릴 필요는 없지만, “가져오기 완료(오류 3건)”는 사용자가 진행률 알림을 이미 닫았더라도 전달되기를 원하기 때문입니다. 완료 시에는 진행률 표시줄을 없애고 버튼(“오류 내용 보기” 등)을 붙인 다른 레이아웃으로 교체하는 형태가 실무에서 흔히 쓰입니다.
8.3. 알림 센터에 남은 오래된 알림 정리하기
토스트는 사용자가 지울 때까지 알림 센터에 쌓입니다. 상주 앱이 매일 여러 건의 알림을 내보내면, 알림 센터가 자사 앱의 오래된 알림으로 가득 차서 오히려 중요한 알림이 묻혀 버립니다.
Tag / Group을 붙여 두면, 자사 앱의 알림을 코드에서 삭제할 수 있습니다(태그 지정・그룹 지정・전체 삭제). 유효기간 설정도 가능하므로, “진행률 계열은 완료 시 지운다”, “정보성은 유효기간을 짧게 한다”와 같은 정리 규칙을, 알림을 내보내는 설계와 동시에 정해 두는 것을 추천합니다.26
9. 알림이 “도착하지 않는” 경우를 설계에 반영하기
토스트 알림은 확실한 전달 수단이 아닙니다. 표시되지 않거나 알아채지 못하는 경로가 정상적인 경우로서 존재합니다.
| 경우 | 동작 |
|---|---|
| 방해 금지(Focus Assist / 다른 사람 방해 금지)가 활성화됨 | 배너는 표시되지 않고, 알림은 곧바로 알림 센터로 이동 |
| 앱 단위・시스템 전체에서 알림이 꺼져 있음 | 표시되지 않음 |
| 관리자 권한 상승 프로세스에서의 AppNotification | 지원되지 않음. 표시되지 않음 |
| 전체 화면 앱 실행 중 등 | OS의 판단으로 억제되는 경우가 있음 |
방해 금지는 Windows 10에서는 “집중 지원(Focus assist)”, Windows 11에서는 “설정 → 시스템 → 알림”의 “방해 금지” 토글로, 사용자 또는 그룹 정책이 제어합니다. OS 업그레이드 직후 1시간 동안은 자동으로 활성화되는 사양까지 있습니다.11 집중 모드의 동작은 “알림을 내보내지 않는다”가 아니라 “배너 표시를 억제하고 알림 센터로 보낸다”이므로, 사용자가 나중에 알림 센터를 열면 볼 수 있습니다.12
설계에 대한 함의는 단순합니다.
- 토스트는 “알아차리면 빠르게 대응할 수 있는” 보조 채널로 사용하고, 업무상 필수 정보는 앱 안의 화면・목록에서 반드시 확인할 수 있도록 합니다. “토스트가 나왔을 것”을 업무 흐름의 전제로 삼지 마세요.
- 아무래도 강제로 끼어들어야 하는 알림(긴급 정지 경고 등)은,
AppNotificationScenario.Urgent(중요 알림)로 사용자 허가 하에 방해 금지를 돌파하는 장치가 있지만,27 남용하면 사용자는 앱의 알림 자체를 꺼 버립니다. 정말로 긴급한 것에만 한정해야 합니다.
외부 장치의 이상 등을 화면에 어떻게 표시할 것인가 하는 더 넓은 주제는 “외부 기기 상태 확인과 표시의 모범 사례“도 참고하세요.
10. Windows 서비스에서 알림을 보내고 싶을 때
“상주 서비스가 이상을 감지하면, 로그온 중인 사용자에게 토스트로 알리고 싶다”는 요구사항은 자연스럽게 나오지만, 서비스에서 직접 토스트를 보낼 수는 없습니다. Windows Vista 이후 서비스는 세션 0이라는 전용 세션에서 동작하며, 사용자와 상호작용하는 UI를 가질 수 없기 때문입니다.13
정석은 UI를 사용자 세션 쪽 프로세스로 분리하는 구성입니다.
[세션 0] [사용자 세션]
Windows 서비스 <-- IPC --> 트레이 상주 에이전트
(모니터링・업무 처리) 명명된 파이프 등 (NotifyIcon + AppNotification)
- 서비스는 모니터링・업무 처리에 전념하고, 알리고 싶은 이벤트를 IPC로 에이전트에 전송합니다.
- 에이전트는 사용자 로그온 시 시작되는 트레이 상주 앱(이 글의 2~8장 그 자체)으로, 받은 이벤트를 토스트나 아이콘 변화로 표시합니다.
- IPC 수단 선택은 “Windows 프로세스 간 통신을 어떻게 선택할 것인가”, 서비스 쪽 구현 방법은 “Windows 서비스 만드는 법과 운영”, 세션 구조 자체는 “Windows 세션 분리를 어떻게 이해할 것인가 ── 세션 0・RDP・다중 사용자 동시 실행“에서 정리했습니다.
여러 사용자가 동시에 로그온하는 환경(RDP・공유 단말)에서는 에이전트가 세션마다 하나씩 동작하게 됩니다. “누구에게 알려야 하는가”(전원인지, 특정 사용자만인지)를 요구사항으로 먼저 정해 두면 설계가 흔들리지 않습니다.
11. 정리
- 트레이 상주는
NotifyIcon이 기본입니다.Icon+Visible = true로 표시하고,Text는 127자 제한(.NET 6+), 종료 경로에서는Dispose()를 확실히 거치도록 합니다. 아이콘과 툴팁은 “현재 상태”를 나타내는 상시 표시로도 사용합니다. - “×로 닫아도 트레이에 남긴다”는
FormClosing의 취소로 구현하지만,CloseReason.UserClosing일 때만 취소하고, 명시적인 “종료” 메뉴를 반드시 마련합니다. - 아이콘의 항상 표시는 사용자만이 결정할 수 있습니다. 탐색기 재시작 시의 소실은
TaskbarCreated의 재등록으로 대응합니다(WinForms는 자동). - 토스트 알림의 신규 구현은 Windows App SDK의
AppNotificationManager하나뿐입니다. 패키징되지 않은 exe에서도 패키지 ID 없이 동작하며,Register()가 번거로운 등록을 자동화합니다. Community Toolkit 알림은 보관 처리되었고, 순수 WinRT는 유지보수 전용입니다. - 긴 처리의 중간 경과는
UpdateAsync로 한 장의 토스트 진행률 표시줄을 조용히 다시 쓰고, 완료는 교체로 팝업시키는 것이 정석입니다.Tag/Group을 붙여, 오래된 알림 삭제까지 설계에 포함합니다. - 토스트는 방해 금지나 알림 끄기로 도착하지 않을 수 있는 보조 채널입니다. 필수 정보는 앱 안에서 확인할 수 있는 설계로 하고, 상승 프로세스에서의 알림이 지원되지 않는다는 점에도 주의하세요.
- 서비스로부터의 알림은, 사용자 세션 쪽 트레이 상주 에이전트 + IPC라는 분리 구성으로 실현합니다.
관련 글
- Windows 앱의 중복 실행 방지 ── 명명된 뮤텍스와 이중 실행 시 활성화
- Windows 서비스 만드는 법과 운영 ── 작업 스케줄러와의 구분 사용부터 BackgroundService의 서비스화까지
- Windows 프로세스 간 통신을 어떻게 선택할 것인가 ── 명명된 파이프 / TCP / gRPC / 공유 메모리 / COM 판단표
- Windows 세션 분리를 어떻게 이해할 것인가 ── 세션 0・RDP・다중 사용자 동시 실행
- .NET Generic Host와 BackgroundService를 데스크톱 앱에서 사용하는 이유
- Windows 앱에서 「관리자 권한이 필요한 처리만」을 분리하는 구체적인 방법
- 외부 기기 상태 확인과 표시의 모범 사례 - ‘연결됨’만으로 끝내지 않는 설계
관련 상담 영역
합동회사 코무라소프트에서는 트레이 상주형 모니터링 도구와 알림 에이전트의 설계・구현, Windows 서비스와 결합한 상주 시스템 구성 검토, 기존 상주 앱의 오류 조사를 다루고 있습니다.
참고 링크
-
Microsoft Learn, NotifyIcon Class / NotifyIcon.Visible Property. 알림 영역에 아이콘을 표시하는 컴포넌트라는 점, Icon 속성 설정이 필요하다는 점, Visible의 기본값이 false라는 점에 대해. ↩ ↩2 ↩3
-
Microsoft Learn, NotifyIcon.Text Property. 툴팁 텍스트의 상한과, 초과 시 ArgumentException이 발생한다는 점에 대해. ↩ ↩2
-
Microsoft Learn, Windows Forms Controls and Equivalent WPF Controls. WPF에 NotifyIcon과 동등한 컨트롤이 존재하지 않는다는 점에 대해. ↩ ↩2
-
Microsoft Learn, MSBuild properties for .NET desktop projects. UseWPF와 UseWindowsForms를 함께 사용할 수 있다는 점에 대해. ↩ ↩2
-
Microsoft Learn, Form.FormClosing Event. 폼이 닫히기 전에 발생하며, Cancel 속성으로 닫기를 취소할 수 있다는 점에 대해. ↩ ↩2
-
Microsoft Learn, Windows notifications overview. 앱 종류별 권장 알림 API 대응표. WinForms/WPF/패키징되지 않은 Win32에 대한 Windows App SDK 권장과, Windows.UI.Notifications의 유지보수 전용 위치에 대해. ↩ ↩2
-
Microsoft Learn, Windows Community Toolkit — Notifications (archive). Notifications 컴포넌트가 보관 처리되어 Windows App SDK의 앱 알림으로 대체되었다는 점에 대해. ↩
-
Microsoft Learn, Use the Windows App SDK in an existing project. Windows App SDK의 앱 알림 API가 패키징・비패키징 양쪽을 모두 지원하며, 패키지 ID를 필요로 하지 않는다는 점에 대해. ↩ ↩2
-
Microsoft Learn, Quickstart: App notifications in the Windows App SDK. AppNotificationManager/AppNotificationBuilder의 사용법, 비패키징 앱에서의 Register()에 의한 자동 등록, NotificationInvoked를 Register() 이전에 구독해야 한다는 점, 패키징 앱에서의 매니페스트 기재에 대해. ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, App notifications overview. 관리자 권한으로 상승된 앱에서의 알림 전송이 지원되지 않는다는 점에 대해. ↩ ↩2
-
Microsoft Learn, Notifications don’t appear or aren’t displayed. 방해 금지(Quiet Hours / Focus Assist)에 의한 알림 억제, OS 업그레이드 후 1시간 동안의 자동 활성화, 알림이 표시되지 않을 때의 확인 절차에 대해. ↩ ↩2
-
Microsoft Learn, Focus assist. 집중 모드가 알림의 배너 표시를 억제하고 알림 센터로 직접 보내는 구조라는 점에 대해. ↩ ↩2
-
Microsoft Learn, Service Changes for Windows Vista / Interactive Services. 세션 0이 서비스 전용이며 사용자와의 상호작용을 지원하지 않는다는 점, 사용자에게 표시하려면 CreateProcessAsUser로 사용자 세션에 프로세스를 만들어 IPC로 연계하는 방법이 권장된다는 점에 대해. ↩ ↩2
-
Microsoft Learn, NotifyIcon.Text maximum text length increased. .NET 6에서 Text의 최대 길이가 63자에서 127자로 확장된 호환성 변경 사항에 대해. ↩
-
Microsoft Learn, FormClosingEventArgs Class. CloseReason으로 폼이 닫히는 이유를 판별할 수 있다는 점에 대해. ↩
-
Microsoft Learn, How to close a window or dialog box (WPF). WPF의 Closing 이벤트에서 e.Cancel = true로 설정하고 Hide()를 호출하는 숨김 처리 패턴에 대해. ↩
-
Microsoft Learn, Notification Area (Windows Shell). 아이콘이 기본값으로 오버플로 영역에 추가된다는 점, 알림 영역으로의 승격은 사용자만 결정할 수 있다는 점, 필요 없어진 아이콘은 NIM_DELETE로 삭제해야 한다는 점에 대해. ↩ ↩2
-
Microsoft Learn, The Taskbar. 작업 표시줄 생성 시 TaskbarCreated 메시지가 브로드캐스트되며, 이를 수신하면 트레이 아이콘을 다시 등록해야 한다는 점에 대해. ↩
-
Microsoft Learn, Shell_NotifyIconW function. 풍선 알림이 Windows 10에서는 토스트(배너)로 표시되어 알림 센터에 남는다는 점, Windows 11에서는 일시적인 표시가 되어 알림 센터에 남지 않는다는 점, 표시 시간이 접근성 설정을 따른다는 점에 대해. ↩
-
Microsoft Learn, Send a local toast notification from a C# app (Win32/클래식 API). 순수 ToastNotificationManager를 사용하는 경우, AUMID가 붙은 바로가기의 시작 메뉴 등록이 필수라는 점에 대해. ↩
-
Microsoft Learn, AppNotificationManager.Register Method. 앱 종료 전에 Unregister를 호출해 리소스를 정리하면 이후의 알림 실행이 올바르게 이루어진다는 점, 알림 기능을 앞으로 사용하지 않는 경우에는 UnregisterAll로 등록 정보를 지워야 한다는 점에 대해. ↩
-
Microsoft Learn, AppNotificationManager.UpdateAsync Method / Windows App SDK deployment architecture. AppNotificationManager 클래스가 Windows App SDK의 Singleton 패키지에 의존하고 있으며, self-contained 배포 앱에서 호출할 때 고려 사항이 있다는 점에 대해. ↩
-
Microsoft Learn, App notification progress bar and data binding. Bind 메서드를 이용한 데이터 바인딩, AppNotificationProgressData와 UpdateAsync에 의한 업데이트, 업데이트와 교체의 차이(알림 센터 내 위치・변경 가능 범위・팝업 재표시・사용자가 닫은 후의 동작), 완료 시에는 교체를 검토해야 한다는 점에 대해. ↩ ↩2
-
Microsoft Learn, AppNotificationProgressData(UInt32) Constructor. 시퀀스 번호가 업데이트 순서를 지정하는 0이 아닌 정수이며, 여러 업데이트 데이터를 수신한 경우 가장 큰 번호의 데이터가 표시된다는 점에 대해. ↩
-
Microsoft Learn, Remove app notifications. 태그・그룹 부여, 표시된 알림의 삭제, 유효기간 설정에 대해. ↩
-
Microsoft Learn, App notification content. scenario=”urgent” 중요 알림이 사용자 허가 하에 방해 금지 설정보다 우선된다는 점에 대해. ↩
관련 기사
같은 태그를 공유하는 최신 기사입니다. 더 가까운 주제로 지식을 넓힐 수 있습니다.
WinForms/WPF 앱 다국어화 ── resx·새틀라이트 어셈블리·컬처 전환 실무
Windows 데스크톱 앱의 다국어화를 CurrentCulture와 CurrentUICulture의 차이, resx와 새틀라이트 어셈블리를 통한 리소스 구조, WinForms의 Localizable 속성, WPF에서의 현실적인 방식 선택, 실행 ...
Windows 업무 앱의 인쇄와 PDF 출력 - System.Drawing.Printing / WPF / 보고서 라이브러리 구분 사용법
PrintDocument를 이용한 WinForms 인쇄, WPF의 FlowDocument/FixedDocument 인쇄, PDF 출력의 선택지를 요구사항별 판단표로 정리합니다. 페이지 나눔 제어나 DPI 차이 같은 구현의 함정, Windows 서...
WinForms/WPF 앱에 Entra ID 인증 넣기 ── MSAL.NET과 WAM 브로커 실무 구성
WinForms/WPF 데스크톱 앱에 Entra ID(구 Azure AD) 인증을 도입하는 절차를 실무 관점에서 정리합니다. 퍼블릭 클라이언트의 개념, ROPC 폐지 현황, 앱 등록, MSAL.NET의 AcquireTokenSilent 패턴, W...
Windows 앱 외주·수탁 개발을 의뢰하기 전에 정리해야 할 것
Windows 앱의 외주·수탁 개발을 의뢰하기 전에, 기존 소프트웨어 개수, 장치 연계, COM/ActiveX, 배포·업데이트, 보수와 관련해 정리해야 할 포인트를 설명합니다.
Generic Host / BackgroundService를 데스크톱 앱에 가지고 들어오는 이유 - 기동・수명・graceful shutdown의 정리가 꽤 편해진다
WPF나 WinForms 같은 데스크톱 앱에서 Generic Host와 BackgroundService를 도입해 기동, 상주 처리, graceful shutdown, DI, 로그, 설정의 입구를 한 곳으로 모으는 설계 정리법과 안티패턴을 실무 시...
관련 토픽
이 기사와 가까운 토픽 페이지입니다. 기사를 출발점 삼아 관련 서비스와 다른 기사로 이어집니다.
Windows 기술 토픽
Windows 개발, 장애 조사, 기존 자산 활용에 관한 KomuraSoft LLC 기사를 모은 토픽 허브입니다.
UI 스레드 & 타이머
WPF / WinForms UI 스레드, async 흐름, Dispatcher 사용, 타이머 판단을 정리한 토픽 페이지입니다.
이 주제와 연결되는 서비스
이 기사는 다음 서비스 페이지로 이어집니다. 가까운 입구부터 확인해 주세요.
Windows 앱 개발
상주 처리, 장비 연동, 운영 로그, 유지 보수 가능한 구조가 필요한 Windows 데스크톱 애플리케이션을 지원합니다.
기술 상담 & 설계 리뷰
설계 방향, 아키텍처 경계, 수명 관리, 기존 Windows 자산 처리 방법을 정리하는 데 도움을 드립니다.
자주 묻는 질문
이 기사 주제에 대해 상담 시 자주 나오는 질문을 모았습니다.
- WPF로 작업 트레이 상주 앱을 만들려면 어떻게 해야 하나요?
- WPF에는 NotifyIcon에 해당하는 컨트롤이 존재하지 않기 때문에, csproj에서 UseWPF와 UseWindowsForms를 함께 사용해 WinForms의 NotifyIcon을 쓰는 것이 최소 구성입니다. XAML 친화적인 작성 방식을 원한다면 H.NotifyIcon 같은 서드파티 라이브러리도 선택지가 될 수 있지만, Microsoft 공식이 아니라는 점을 감안해 채택 여부를 판단하세요.
- 토스트 알림 API는 어떤 것을 사용해야 하나요?
- 2026년 시점의 신규 구현이라면 WinForms/WPF/패키징되지 않은 앱 어느 쪽이든 Windows App SDK의 AppNotificationManager가 공식적으로 권장됩니다. Microsoft.Toolkit.Uwp.Notifications는 보관(archive) 처리되어 현재의 Community Toolkit에는 포함되어 있지 않고, WinRT의 ToastNotificationManager는 유지보수만 이루어지는 위치입니다. 기존 코드가 이 두 가지로 동작하고 있다면 서둘러 다시 작성할 필요는 없지만, 새로 선택할 이유는 없습니다.
- 토스트 알림이 표시되지 않을 때는 무엇을 확인해야 하나요?
- 먼저 OS 쪽 설정입니다. 방해 금지(Focus Assist)가 활성화되어 있으면 알림은 배너로 표시되지 않고 곧바로 알림 센터로 이동하며, 설정 앱에서 앱 단위 또는 시스템 전체의 알림이 꺼져 있는 경우에도 표시되지 않습니다. 구현 측면에서는 관리자 권한으로 상승된 프로세스에서의 AppNotification은 지원되지 않아 표시에 실패한다는 점, 순수한 Windows.UI.Notifications를 AUMID 없는 데스크톱 앱에서 호출해도 표시되지 않는다는 점이 대표적인 원인입니다.
- Windows 서비스에서 토스트 알림을 보낼 수 있나요?
- 보낼 수 없습니다. Windows Vista 이후 서비스는 세션 0에서 동작하며, 사용자와 상호작용하는 UI를 가질 수 없기 때문입니다. 사용자에게 알리고 싶다면 사용자 세션 쪽에 트레이 상주 에이전트 프로세스를 두고, 서비스와는 명명된 파이프 등의 IPC로 연계하여 알림 표시는 에이전트가 담당하는 구성이 정석입니다.
저자 프로필
기사 저자의 프로필 페이지입니다.
Go Komura
합동회사 코무라소프트 대표
Windows 소프트웨어 개발, 기술 상담, 장애 조사를 중심으로 재현이 어려운 장애 조사와 기존 자산이 남아 있는 프로젝트에 강점이 있습니다.
공개 링크