WinRT는 COM이다 - IInspectable, .winmd, 언어 프로젝션, 그리고 WinUI가 지금도 바이너리 계약 위에 서 있는 이유
· 업데이트: · Go Komura · Windows, WinRT, COM, WinUI, Windows App SDK, Windows 개발
수정 이력(초판, 2026년 08월 29일 공개)
- 최초 공개
WPF나 WinForms 앱에 Windows의 새 기능을 더하고 싶다. 그런데 WinRT의 피커를 호출하면 예외가 난다. WinUI 이야기를 읽으면 이번에는 UI를 다시 만들어야 할 것처럼도 보인다. 이런 당혹감은 WinRT의 구조와 UI 프레임워크의 선택을 나누어 보면 정리됩니다.
이 글의 출발점은 WinRT도 COM의 바이너리 계약 위에 있다는 점입니다. Microsoft 자신도 「The Windows Runtime is based on COM(Windows Runtime은 COM에 기반한다)」이라고 명시하고 있습니다.1 지난 OLE 객체 글에서 본 Word에 Excel 표를 끼워 넣는 일도, 지금의 WinRT나 WinUI도, 뿌리에는 같은 IUnknown이 있습니다.
여기서는 먼저 WinRT를 이루는 세 가지 요소를 짚고, 다음으로 데스크톱 앱에서 쓸 때 유의할 점을, 마지막으로 기존 자산을 어떻게 다룰지를 생각합니다. 대상 독자는 COM 또는 Windows 데스크톱 개발 경험이 있는 개발자, 전제 환경은 Windows 10/11과 .NET 6 이상(C#) 또는 C++17(C++/WinRT), 난이도는 중급입니다.
1. 먼저 결론
짚어 둘 결론은 세 가지입니다.
- WinRT는 매니지드 런타임이 아니라, COM을 기반으로 한 ABI(바이너리 계약)입니다.그 계약에, 형식 정보를 전달하는
.winmd와, 각 언어에서 자연스럽게 호출하기 위한 언어 프로젝션이 더해져 있습니다.1234 - WinRT API의 대부분은 기존 WPF, WinForms, Win32 앱에서 쓸 수 있습니다.다만 HWND 전달, package identity, 스레드 초기화라는 세 가지 전제를 확인해야 합니다.5678
- WinUI로의 UI 전면 이행과, WinRT API의 부분 사용은 별개의 판단입니다.WinUI도 WinRT ABI 위에 있고, 기존 COM/ActiveX 자산과 WinRT는 같은 기반에서 공존할 수 있습니다.921
이후는 다음 순서로 읽으면 연결 고리를 따라가기 쉬워집니다.
| 알고 싶은 것 | 읽을 장 |
|---|---|
| 왜 「WinRT는 COM」이라고 말할 수 있는가 | 2~3장: COM과의 공통점과 IInspectable |
| 왜 C#이나 C++에서 자연스럽게 호출할 수 있는가 | 4~5장: .winmd와 언어 프로젝션 |
| 기존 앱에서 쓰려면 무엇을 확인하는가 | 6장: HWND, package identity, 아파트먼트 |
| WinUI로 이행해야 하는가 | 7~9장: WinUI의 발밑, 이행 판단, 알림의 등록 조건 |
아래 지식 맵은 각 요소의 관계를 되짚어 보기 위한 것입니다. 먼저 설명을 읽고 싶다면 2장부터 순서대로 진행해 주세요.
그림의 실선은 항상 성립하는 관계, 점선은 조건이 붙는 관계입니다(성립 조건은 상세 페이지의 관계별 설명에 적혀 있습니다). 관계 전체 목록(총 20건, 근거와 확신도 포함)과 주요 개념의 정의는 지식 맵 상세 페이지에 정리되어 있습니다(일본어). 데이터: JSON-LD / Turtle
2. OLE도 WinRT도, 뿌리는 같은 바이너리 계약
이 블로그에서는 지금까지 COM의 설계 사상, STA/MTA의 스레드 모델, ActiveX/OCX를 다루는 법, OLE 복합 문서처럼 고전 COM의 세계를 쫓아왔습니다. 이들은 모두 1990년대부터의 기술입니다.
한편 WinRT는 Windows 8(2012년)에서 도입된 API 기반입니다. 현재는 Windows.* 네임스페이스의 API군으로서 토스트 알림, 공유, 블루투스, OCR 등을 제공하며, WinUI와 Windows App SDK의 발밑이기도 합니다.10 신구가 전혀 다른 세계처럼 보이지만, 실체는 이어져 있습니다.
공통되는 것은, 인터페이스를 통해 호출한다는 약속
COM 컴포넌트와 WinRT 클래스는 둘 다 인터페이스를 통해 기능을 공개합니다. 기반이 되는 인터페이스를 비교하면 관계는 다음과 같습니다.1
| 고전 COM | WinRT |
|---|---|
인터페이스의 기반은 IUnknown |
인터페이스의 기반은 IInspectable. 그 기반이 IUnknown |
즉 WinRT는 COM과 무관한 구조로 갈아탄 것이 아니라, IUnknown 위에 한 단 더 쌓아 올린 것입니다. 참조 카운트도 QueryInterface도 HRESULT도 그대로 살아 있습니다.
C++/WinRT 문서도 WinRT API를 「COM의 진화(an evolution of COM)」라고 부르며, COM에 기반한 API를 언어 프로젝션을 통해 사용하는 설계라고 설명합니다.211
flowchart TB
accTitle: 고전 COM과 WinRT의 계보
accDescr: IUnknown과 vtable에 의한 COM의 바이너리 계약이라는 공통의 기반 위에, 1990년대부터의 OLE, ActiveX 등 고전 COM의 세계와, 2012년 이후의 WinRT와 그 위의 WinUI, Windows App SDK의 세계가 나란히 서 있으며, 양자는 배타적이 아니라 이어져 있다
base["COM의 바이너리 계약(IUnknown, vtable)"]
base --> classic["고전 COM(OLE, ActiveX, 직접 만든 COM)"]
base --> winrt["WinRT(IInspectable, .winmd)"]
winrt --> winui["WinUI / Windows App SDK"]
classic -.-> coexist["같은 기반 위에서 공존할 수 있다"]
winrt -.-> coexist
그림 1: 고전 COM과 WinRT는 다른 세계가 아니라, 같은 바이너리 계약 위의 신구 두 세대다.
그래서 이 글은 단순한 「새 API 소개」가 아닙니다. 이미 가지고 있는 COM 지식이 2026년의 Windows 개발 어디에 효력을 발휘하는지를 확인하는 글입니다.
3. IUnknown 위의 IInspectable
변하지 않는 기반과, 추가된 세 메서드
WinRT 형식 시스템의 공식 사양에서는 모든 WinRT 인터페이스는 암묵적으로 IInspectable을 요구하고, IInspectable은 IUnknown을 요구한다고 정하고 있습니다. IUnknown이 정의하는 것은 종전대로 QueryInterface, AddRef, Release의 세 메서드입니다.12
그 위에 IInspectable이 다음 세 메서드를 추가합니다.13
| 메서드 | 역할 |
|---|---|
GetIids |
이 객체가 구현하는 인터페이스의 IID 목록을 반환한다 |
GetRuntimeClassName |
정규화된 WinRT 형식 이름(Windows.Storage.StorageFile 등)을 HSTRING으로 반환한다 |
GetTrustLevel |
객체의 신뢰 수준을 반환한다 |
flowchart TB
accTitle: IUnknown 위에 얹히는 IInspectable
accDescr: 모든 WinRT 인터페이스는 IInspectable을 요구하고, IInspectable은 IUnknown을 요구한다. IUnknown이 QueryInterface, AddRef, Release를, IInspectable이 GetIids, GetRuntimeClassName, GetTrustLevel을 제공하고, 그 위에 각 WinRT 인터페이스의 메서드가 얹힌다
unk["IUnknown(QI, AddRef, Release)"]
insp["IInspectable(GetIids, 형식 이름, 신뢰 수준)"]
api["각 WinRT 인터페이스의 메서드"]
unk --> insp
insp --> api
그림 2: WinRT 객체는 IUnknown의 세 메서드 위에 IInspectable의 세 메서드를 쌓고, 그 위에 개별 API가 얹힌다.
형식 이름에서 메서드, 속성, 이벤트의 정의를 끌어낼 수 있다
중요한 것은 추가된 메서드의 개수보다도, 형식 이름과 메타데이터를 이어 붙일 수 있다는 점입니다.
고전 COM에서는 실행 시에 객체의 정체를 아는 표준적인 방법이 「IID를 알고 있고 QueryInterface로 물어보는」 것이었습니다. 스크립트 언어용으로는 IDispatch라는 별도 경로가 있었습니다.
WinRT에서는 GetRuntimeClassName으로 얻은 형식 이름을 다음 장에서 다룰 메타데이터 .winmd로 해석할 수 있습니다. 거기서 메서드, 속성, 이벤트의 완전한 정의를 얻을 수 있습니다. 사양 자체가 메타데이터로 해석 가능한 WinRT 형식 이름을 얻을 수 있다는 점이 「언어 프로젝션을 가능하게 한다(enables language projection)」고 서술하고 있습니다.12
flowchart TB
accTitle: GetRuntimeClassName에서 언어 프로젝션으로
accDescr: 호출하는 쪽이 객체의 GetRuntimeClassName을 호출하면 정규화된 WinRT 형식 이름이 반환되고, 그 형식 이름을 Windows Metadata로 해석하면 형식의 완전한 정의를 얻을 수 있으며, 이것이 각 언어로의 프로젝션을 가능하게 한다
obj["WinRT 객체"] --> name["형식 이름(GetRuntimeClassName)"]
name --> md[".winmd로 형식 정의를 해석"]
md --> proj["언어 프로젝션이 가능해진다"]
그림 3: 「실행 시에 형식 이름을 얻을 수 있고, 형식 이름에서 메타데이터를 끌어낼 수 있다」는 점이 WinRT라는 장치의 중심에 있다.
사용자 정의 인터페이스의 「상속」과 「요구」는 나누어 본다
COM에 익숙한 사람이 유의해야 할 차이도 있습니다. WinRT의 형식 시스템에는 사용자 정의 인터페이스끼리의 상속이 없습니다.고전 COM의 IFileSystemBindData2 : IFileSystemBindData 같은 파생을 의도적으로 두지 않고, 대신 「인터페이스 A는 인터페이스 B를 요구한다(requires)」는 선언으로 표현합니다.121
이는 지금까지 본 IUnknown → IInspectable이라는 기반 ABI 체인과는 별개의 이야기입니다. 이 기반은 모든 WinRT 인터페이스의 토대로 남아 있습니다.
사용자 정의 계약은 vtable의 상속 레이아웃에 의존하지 않는, 더 느슨한 형태로 옮겨 갔습니다. 그 한편으로 호출의 실체는 지금도 vtable을 거칩니다. 계약을 쓰는 방식의 차이와 호출 구조를 혼동하지 않는 것이 중요합니다.
4. .winmd가 해결한 것 - 형식 정보의 바인딩 지옥
고전 COM의 어려움은 「형식 정보를 배포하는 방식」에 있었다
고전 COM의 실무에서는 인터페이스 구현 자체보다 형식 정보를 어떻게 배포할 것인가에 품이 많이 들었습니다.
| 사용하는 쪽 | 형식 정보를 전달하는 경로 |
|---|---|
| C++ | IDL로 계약을 쓰고, MIDL로 헤더와 프록시/스텁을 생성한다 |
| VB6, 스크립트 | 형식 라이브러리(TLB)를 배포한다 |
| .NET | 상호 운용 어셈블리를 따로 만든다 |
TLB에는 자동화에 치우친 형식 제약이 있어, IDL에 쓸 수 있어도 TLB에 들어가지 않는 정보가 있습니다. 게다가 언어마다 경로가 갈라져 있으므로, 어느 하나가 낡으면 형식 불일치로 이어집니다. 이 고생은 형식 라이브러리와 dscom 글이나 DLL, COM 인터페이스의 하위 호환성 글에서 다룬 그대로입니다.
.winmd는, 모든 언어가 읽는 공통의 계약서
WinRT의 답이 Windows Metadata(.winmd)입니다. API를 기계 판독 가능한 메타데이터로 기술하고, 도구와 언어 프로젝션이 그것을 읽어 각 언어용 프로젝션을 생성합니다.3
Windows는 시스템이 제공하는 전체 WinRT API의 메타데이터를 함께 담고 있으며, 실행 시에 네임스페이스나 형식을 해석하는 API도 제공합니다. Windows SDK에는 컴파일 시점용 사본이 들어 있습니다. 서드파티도 자신의 WinRT 컴포넌트에 .winmd를 붙이면 시스템 API와 같은 구조로 언어 프로젝션에 참여할 수 있습니다.3
여기서 바뀐 것은 배포되는 형식 정보의 형태입니다. 언어마다 흩어져 있던 TLB, 헤더, 상호 운용 어셈블리 대신, 하나의 .winmd를 모든 언어의 프로젝션이 읽게 되었습니다.
다만 IDL이 필요 없어진 것은 아닙니다. WinRT 컴포넌트를 만들 때는 지금도 IDL(WinRT용으로 새로 다듬은 MIDL 3.0)로 계약을 기술하고, MIDL 컴파일러가 .winmd를 생성합니다.14
flowchart TB
accTitle: WinRT 컴포넌트의 제작 파이프라인
accDescr: WinRT 컴포넌트의 계약은 지금도 IDL 즉 MIDL 3.0으로 기술하고, MIDL 컴파일러가 그것을 .winmd로 컴파일한다. 배포되는 것은 이 .winmd이며, cppwinrt.exe나 cswinrt.exe 등 각 언어의 프로젝션이 이것을 읽어 프로젝션을 생성한다. 바뀐 것은 IDL이 아니라 배포되는 형식 정보의 형태다
idl2["계약을 기술(IDL, MIDL 3.0)"]
midl2["MIDL 컴파일러"]
winmd4[".winmd(배포되는 형식 정보)"]
proj3["각 언어의 프로젝션을 생성"]
idl2 --> midl2
midl2 --> winmd4
winmd4 --> proj3
그림 4: 계약의 입구(IDL)는 현역 그대로이고, 배포되는 형식 정보라는 출구가 .winmd로 통일되었다.
.NET과 같은 파일 형식이어도, 매니지드 런타임은 아니다
.winmd의 물리 형식은 CLR 어셈블리와 같은 ECMA-335 사양을 사용합니다. 다만 유효한 데이터 조합의 규칙은 CLR 어셈블리와 다릅니다. 형식을 빌린 것과, 실행에 CLR이 필요한 것은 별개입니다.3
이 부분은 시스템 API와 서드파티 컴포넌트를 나누어 읽어야 합니다.
| 대상 | .winmd와 구현의 관계 |
|---|---|
| 시스템이 제공하는 WinRT API | .winmd는 실행 코드를 포함하지 않는 순수한 메타데이터. 구현은 OS의 네이티브 DLL에 있고, 실행에 CLR은 불필요 |
| 서드파티의 WinRT 컴포넌트 | .winmd가 구현 코드를 포함하는 경우도 있다. 매니지드한(C#으로 쓴) 컴포넌트가 MSIL을 포함하면 실행에 해당하는 .NET 런타임이 필요 |
.winmd를 도구로 열면 .NET 어셈블리처럼 보이기 때문에 「WinRT는 매니지드」라고 오해하기 쉽습니다. 그러나 시스템이 제공하는 .winmd의 내용물은 COM 인터페이스의 계약서입니다.3
flowchart TB
accTitle: .winmd와 구현의 분리
accDescr: .winmd는 ECMA-335의 물리 형식을 빌린 메타데이터이며, 시스템이 제공하는 것은 실행 코드를 포함하지 않는 계약서이고, 시스템이 제공하는 WinRT API의 구현은 OS의 네이티브 DLL에 있다. 이 분리 덕분에 .winmd가 .NET 어셈블리처럼 보여도 시스템 WinRT API의 실행에 CLR은 불필요하다(서드파티의 매니지드 컴포넌트의 .winmd는 MSIL을 포함하며 .NET 런타임을 필요로 한다)
winmd3[".winmd(계약서, ECMA-335 형식)"]
impl["OS의 네이티브 DLL(구현)"]
winmd3 -.->|"시스템 제공은 코드 없음"| note3["시스템 API는 CLR 불필요"]
impl --> note3
winmd3 ---|"형식 정의와 구현이 대응"| impl
그림 5: 시스템이 제공하는 WinRT API에서는 .winmd가 계약서, 구현은 OS의 네이티브 DLL. 형식이 .NET 풍이어도 실행은 네이티브 COM.
flowchart TB
accTitle: 고전 COM의 형식 정보와 .winmd의 대비
accDescr: 고전 COM에서는 IDL에서 C++ 헤더로, 형식 라이브러리에서 VB6나 스크립트로, 상호 운용 어셈블리에서 .NET으로, 언어마다 형식 정보의 경로가 갈라져 편차의 원인이 되었던 것에 비해, WinRT에서는 하나의 .winmd를 모든 언어의 프로젝션이 공통으로 읽는다
subgraph old["고전 COM: 경로가 언어마다"]
idl["IDL에서 C++ 헤더로"]
tlb["TLB에서 VB6, 스크립트로"]
ia["상호 운용 어셈블리에서 .NET으로"]
end
winmd[".winmd(단일 메타데이터)"]
winmd --> all["모든 언어의 프로젝션이 공통으로 읽는다"]
그림 6: 언어마다 흩어져 있던 형식 정보의 배포 방식을, .winmd는 「하나의 메타데이터를 모두가 읽는」 형태로 접어 넣었다.
제약이 사라진 것이 아니라, 프로젝션하기 쉬움을 축으로 바뀌었다
고전 COM을 아는 사람에게 한마디로 정리하면, .winmd는 「형식 라이브러리의 다시 만들기」입니다. TLB가 하려던 역할을, ECMA-335라는 검증된 형식 위에서, 처음부터 모든 언어 공통의 정본으로 다시 설계한 것이라고 보면 위치가 깔끔해집니다.
다만 제약이 없어진 것은 아닙니다. TLB의 자동화에 치우친 제약이, 「모든 언어로 안전하게 프로젝션할 수 있을 것」을 축으로 한 WinRT 고유 형식 시스템의 제약으로 바뀐 것입니다. 3장에서 본, 사용자 정의 인터페이스끼리의 상속이 없다는 점도 그 한 예입니다.12
따라서 기존 COM/IDL 계약을 그대로 WinRT로 가져올 수 있다고는 할 수 없습니다. API의 재설계가 필요해지는 경우가 있습니다.
flowchart TB
accTitle: 형식 라이브러리의 제약에서 WinRT 형식 시스템의 제약으로 교체
accDescr: TLB(형식 라이브러리)가 가지고 있던 자동화에 치우친 표현력의 제약은 .winmd에서 걷힌 것이 아니라, 모든 언어로 안전하게 프로젝션할 수 있을 것을 축으로 한 WinRT 고유 형식 시스템의 제약으로 바뀌었다. 그 한 예가 사용자 정의 인터페이스 상속의 부재이며, 기존 COM/IDL 계약을 그대로 가져올 수 있다고는 할 수 없고 API의 재설계가 필요해지는 경우가 있다
tlb["TLB의 제약(자동화에 치우침)"] -->|"교체"| wrt["WinRT 형식 시스템의 제약(프로젝션 가능성 축)"]
wrt -.-> ex["예: 사용자 정의 상속은 불가"]
wrt -.-> re["기존 COM 계약은 설계 재검토가 필요한 경우 있음"]
그림 7: TLB의 제약은 「사라진」 것이 아니라, 모든 언어로의 프로젝션 가능성을 축으로 한 다른 제약으로 바뀌었다.
5. C++/WinRT, C#/WinRT는 「래퍼」가 아니라 프로젝션
공통의 계약을, 각 언어에 자연스러운 형태로 보여 준다
.winmd라는 공통의 계약서가 있으면, 각 언어에서의 표현 방식을 도구로 자동 생성할 수 있습니다. 이것이 언어 프로젝션(language projection)입니다. WinRT API를 각 언어의 관용에 맞게 공개하고 COM의 세부 사항을 감춤으로써, 그 언어에 자연스러운 프로그래밍 경험을 제공합니다.411
Microsoft가 현재 지원하는 프로젝션은 다음 두 가지입니다.4
| 프로젝션 | 생성하는 것 | 특징 |
|---|---|---|
| C++/WinRT | cppwinrt.exe가 .winmd에서 C++용 프로젝션 헤더를 생성 |
표준 C++17의 헤더 파일 기반 프로젝션. C++/CX 같은 언어 확장이 필요 없고, C++/CX와 WRL의 후속11 |
| C#/WinRT(CsWinRT) | cswinrt.exe가 .winmd에서 C# 코드를 생성해 상호 운용 어셈블리로 만든다 |
.NET용 프로젝션. 런타임에서 독립한 툴체인1516 |
C# 쪽 경위는 조금 헷갈리므로 구분해 둡니다. .NET Core 3.x까지는 WinRT/winmd의 사용을 .NET 런타임이 기본 제공으로 지원했습니다. .NET 5에서 그 기본 제공 지원이 제거되고 C#/WinRT로 옮겨 갔습니다.16 WinRT API를 쓸 수 없게 된 것이 아니라, 프로젝션을 담당하는 곳이 바뀐 것입니다.
지금 C#에서 net8.0-windows10.0.19041.0 같은 TFM을 지정하면 Windows SDK의 프로젝션 어셈블리가 자동 참조됩니다.10
flowchart TB
accTitle: .winmd에서 각 언어로의 프로젝션 생성
accDescr: 하나의 .winmd를 cppwinrt.exe가 읽으면 C++17용 프로젝션 헤더가, cswinrt.exe가 읽으면 C#용 상호 운용 어셈블리가 생성되어, 각 언어의 관용에 맞는 형태로 WinRT API가 공개된다
winmd2[".winmd(API의 계약서)"]
winmd2 --> cpp["cppwinrt.exe에서 C++17 헤더로"]
winmd2 --> cs["cswinrt.exe에서 C# 상호 운용 어셈블리로"]
cpp --> cppcode["C++의 관용으로 호출할 수 있다"]
cs --> cscode["C#의 관용으로 호출할 수 있다"]
그림 8: 프로젝션은 손으로 쓴 래퍼가 아니라, 계약서(.winmd)에서 도구가 기계적으로 생성한다.
자동 생성되어도, 호출의 실체는 COM 그대로
이 글에서 「래퍼」가 아니라 「프로젝션」이라고 부르는 것은, 사람이 API마다 뒤따라가는 번역 계층이 아니라 메타데이터에서 기계적으로 도출하는 구조임을 강조하기 위해서입니다. .winmd에 실려 있는 API를 처음부터 모든 지원 언어에서 쓸 수 있는 형태로 만들 수 있습니다.
그리고 어느 언어에서 호출하든, 프로젝션 아래에서 일어나는 일은 같은 COM 호출입니다. 예를 들어 C#에서 await picker.PickSingleFolderAsync()라고 쓰면, 프로젝션은 WinRT의 IAsyncOperation을 .NET의 Task 세계로 이어 줍니다. 그래도 ABI 쪽에서는 vtable을 거치는 메서드 호출과 HRESULT가 사용됩니다. 오류가 COM 예외(0x80070005 같은 HRESULT)의 형태로 나타나는 것은 이 때문입니다.
flowchart TB
accTitle: C# 코드에서 OS의 WinRT API까지의 계층
accDescr: 앱의 C#이나 C++ 코드는 언어 프로젝션을 통해 WinRT의 ABI 즉 IInspectable의 vtable 호출로 변환되어 OS가 구현하는 WinRT API에 도달한다. 프로젝션이 COM의 세부 사항을 감추고 있을 뿐, 호출의 실체는 COM이다
code["앱의 코드(C#, C++)"]
proj2["언어 프로젝션"]
abi["WinRT ABI(IInspectable의 vtable)"]
os["OS의 WinRT API 구현"]
code --> proj2
proj2 --> abi
abi --> os
그림 9: 프로젝션이 감추고 있는 것은 COM의 「세부 사항」이지, COM 그 자체가 아니다.
이 구조를 알고 있으면 문제를 두 계층으로 나눌 수 있습니다. 생성 코드의 버전 불일치나 TFM 설정 누락이라면 프로젝션 계층, HRESULT, 아파트먼트, 참조 카운트라면 ABI 계층입니다. 후자에서는 COM 개발 경험이 그대로 무기가 됩니다.
6. 데스크톱 앱에서 막히는 지점 - HWND, identity, 아파트먼트
먼저 「API를 참조할 수 있는 것」과 「동작 조건」을 나눈다
WinRT API의 대부분은 WPF, WinForms, Win32 데스크톱 앱에서 호출할 수 있습니다.5 호출하기 위한 입구는 C#과 C++에서 다음과 같이 다릅니다.10
| 환경 | 최초의 설정 |
|---|---|
| C#/.NET 6 이상 | TargetFramework를 net8.0-windows10.0.19041.0 같은 Windows OS 버전이 붙은 TFM으로 한다 |
| C++ | Microsoft.Windows.CppWinRT NuGet 패키지를 도입하고, C++17 이상에서 C++/WinRT를 사용한다 |
다만 참조할 수 있게 되었다고 해서 모든 API가 그대로 동작하는 것은 아닙니다. 다음 세 가지를 확인합니다. 토스트 알림의 등록 조건은 9장에서 따로 정리합니다.
| 확인할 것 | 주된 대처 |
|---|---|
| 표시 대상 창을 필요로 하는가 | UI에 대응하는 COM interop으로 HWND를 넘긴다 |
| package identity가 필요한가 | MSIX 또는 외부 위치를 가리키는 패키지로 identity를 부여한다 |
| 스레드가 WinRT용으로 초기화되어 있는가 | 네이티브 코드에서는 STA/MTA를 지정해 초기화한다. C#에서는 보통 런타임 쪽이 처리한다 |
막히는 지점 1: 피커 등의 UI에는 HWND를 넘긴다
일부 피커나 대화 상자, 공유 UI는 UWP의 CoreWindow를 표시 대상으로 상정하고 있습니다. 데스크톱 앱에는 CoreWindow가 없으므로, 표시하기 전에 소유자 창의 HWND를 명시적으로 넘겨야 합니다.6
피커 등에서 사용하는 입구가 IInitializeWithWindow라는 COM 인터페이스입니다. 이것은 IUnknown을 상속하며, 데스크톱 앱에서 쓰는 WinRT 객체에 소유자 창을 제공합니다.17
C#에서는 먼저 사용 중인 UI 프레임워크에 따라 HWND를 얻습니다.186
| 소유자 창 | HWND를 얻는 방법 |
|---|---|
WinUI의 Window |
WinRT.Interop.WindowNative.GetWindowHandle |
| WPF의 창 | WindowInteropHelper |
| WinForms의 폼 | 폼의 Handle 속성 |
다음으로 WinRT.Interop.InitializeWithWindow.Initialize(picker, hwnd)로 피커에 넘기고, 그 후에 표시합니다. C++/WinRT라면 객체를 as<IInitializeWithWindow>()로 얻은 뒤 Initialize(hwnd)를 호출합니다. 초기화를 생략하면 예외가 나거나, 조용히 실패합니다.1819
모던한 WinRT 객체에 고전 COM의 인터페이스를 QueryInterface 해서 초기화한다. 이 다리 놓기가 공식적인 작법이 되어 있다는 점에서도, WinRT가 COM이라는 사실이 드러납니다.
공유 UI는 다른 인터페이스입니다.DataTransferManager에 대해서는 IInitializeWithWindow를 쓰지 않습니다. 전용 IDataTransferManagerInterop을 사용해 ShowShareUIForWindow에 HWND를 넘깁니다.6
새로운 Windows App SDK의 피커도 별도 경로입니다.Microsoft.Windows.Storage.Pickers는 생성자에서 WindowId를 받으므로 InitializeWithWindow 패턴은 필요 없습니다. 다만 TFM 설정만으로 쓸 수 있는 API는 아닙니다. Windows App SDK 도입에 더해, unpackaged 앱에서는 배포 대상에 런타임을 배치하고 초기화하는 것이 전제입니다.19
sequenceDiagram
accTitle: 데스크톱 앱에서 피커를 표시하는 순서
accDescr: 데스크톱 앱이 피커를 생성한 뒤 그대로 PickSingleFolderAsync를 호출하면 예외나 조용한 실패가 되므로, 먼저 소유자 창의 HWND를 얻어 IInitializeWithWindow의 Initialize로 넘긴 다음에 표시해야 한다
participant A as 데스크톱 앱
participant P as 피커(WinRT)
A->>P: 생성
alt HWND를 넘기지 않고 표시
A->>P: PickSingleFolderAsync
P-->>A: 예외 또는 조용한 실패
else 먼저 HWND를 넘긴다
A->>P: IInitializeWithWindow로 HWND를 설정
A->>P: PickSingleFolderAsync
P-->>A: 피커가 표시된다
end
그림 10: 「데스크톱에는 CoreWindow가 없다」는 점을 HWND의 명시적 전달로 메우는 것이 공식 작법.
막히는 지점 2: package identity가 필요한 API가 있다
토스트 알림의 기록(ToastNotificationHistory), 점프 목록, 공유 대상 등 일부 WinRT API는 package identity를 가진 앱(패키징된 앱)에서만 동작합니다. 기존형 설치 관리자로 배포하는 unpackaged 앱에서 호출하면 실패합니다.7
대처는 두 가지입니다. MSIX로 패키징하거나, 기존 설치 관리자를 유지한 채 식별자를 부여하는 「외부 위치를 가리키는 패키지(이른바 sparse package)」를 사용합니다.20 어떤 API가 identity를 요구하는지는 공식 목록에서 확인할 수 있습니다.7
flowchart TB
accTitle: package identity가 필요한 API로 가는 두 경로
accDescr: package identity가 필수인 WinRT API를 unpackaged 앱에서 호출하면 실패하므로, MSIX로 패키징하거나 기존 설치 관리자를 유지한 채 식별자만 부여하는 외부 위치를 가리키는 패키지 중 하나로 package identity를 갖게 한다
need["identity가 필수인 API를 쓰고 싶다"]
need --> m1["MSIX로 패키징"]
need --> m2["외부 위치를 가리키는 패키지"]
m1 --> id2["package identity를 획득"]
m2 --> id2
id2 -.-> okid["알림 기록 등이 동작한다"]
그림 11: identity가 필수인 API에 대한 대처는 「MSIX」냐 「기존 설치 관리자 + 식별자 부여」냐의 둘 중 하나.
막히는 지점 3: 스레드 초기화와 STA/MTA를 확인한다
WinRT 객체를 다루는 스레드는 사전에 WinRT용 초기화가 필요합니다. 네이티브 코드에서는 RoInitialize나 winrt::init_apartment를 사용해 STA/MTA라는 동시성 모델을 지정합니다. C#의 WPF/WinForms 앱에서는 보통 런타임 쪽이 초기화를 처리합니다.8
RoInitialize는 COM의 CoInitializeEx와 같은 틀에 속하는, WinRT 세대의 입구입니다. CoInitialize의 문서 자체도 Windows Runtime을 사용하는 경우에는 대신 RoInitialize 또는 Windows::Foundation::Initialize를 호출하라고 안내하고 있습니다.21
WPF/WinForms의 UI 스레드는 STA일 것, UI 객체는 UI 스레드에서 다룰 것, 블로킹 대기가 STA에서 데드락을 부른다는 것. STA/MTA 글에서 다룬 사고방식은 WinRT API에서도 그대로 살아 있습니다.
HWND나 identity를 갖춰도 쓸 수 없는 API는 있다
여기까지의 대처는 데스크톱 사용을 위한 입구가 있는 API의 이야기입니다. CoreWindow나 ApplicationView 자체에 의존하는 API는 데스크톱 앱에서는 쓸 수 없습니다.이 경우에는 초기화를 더할 것이 아니라 대체 API를 찾습니다.57
flowchart TB
accTitle: 데스크톱에서 WinRT API를 호출할 때 막히는 지점의 분기
accDescr: 먼저 스레드가 WinRT용으로 초기화되어 있는지 확인하고(네이티브 코드에서는 RoInitialize 등으로 STA/MTA를 명시적으로 지정한다. C#에서는 보통 런타임 쪽이 처리한다), 그 위에서 호출하려는 WinRT API가 CoreWindow를 전제로 하는 UI 계열이면 IInitializeWithWindow로 HWND를 넘기거나 새 피커를 쓰고, package identity가 필수이면 MSIX 또는 외부 위치를 가리키는 패키지로 식별자를 부여하며, CoreWindow나 ApplicationView 자체에 의존하는 API는 데스크톱에서는 쓸 수 없으므로 대체 API를 찾고, 그 밖의 대부분의 API는 TFM이나 C++/WinRT 설정만으로 그대로 호출할 수 있다
pre["스레드 초기화"] --> q{"호출하려는 API의 성질은?"}
pre -.-> auto["네이티브는 명시 지정, C#은 보통 자동"]
q -->|"UI 계열"| h["HWND 또는 새 피커"]
q -->|"identity 필수"| p2["MSIX 또는 식별자 부여"]
q -->|"CoreWindow 자체에 의존"| x["대체 API를 찾는다"]
q -->|"그 외"| ok3["그대로 호출할 수 있다"]
그림 12: 스레드 초기화(네이티브 코드에서는 명시적으로, C#에서는 보통 런타임에 맡김)를 전제로, 막히는 지점은 세 계통으로 분류할 수 있고 각각 정형화된 대처가 있다.
flowchart TB
accTitle: COM과 WinRT의 스레드 초기화 대응
accDescr: 고전 COM에서는 CoInitializeEx로 STA 또는 MTA를 지정해 스레드를 초기화하는 데 비해, WinRT에서는 RoInitialize로 마찬가지로 STA 또는 MTA의 동시성 모델을 지정해 초기화한다. CoInitialize의 문서도 WinRT 사용 시에는 RoInitialize를 호출하라고 안내하고 있으며, 아파트먼트라는 사고방식은 공통이다
com3["고전 COM: CoInitializeEx"] --> apt["아파트먼트(STA / MTA)"]
wrt["WinRT: RoInitialize"] --> apt
apt -.-> rule["UI 스레드는 STA, 대기에 주의"]
그림 13: 초기화 API의 이름은 바뀌어도, 아파트먼트라는 개념은 같은 것이 계속 사용되고 있다.
7. WinUI의 발밑 - 공개 개발이 되어도 계약은 변하지 않는다
공개 개발로의 이행은, 바이너리 계약의 변경이 아니다
Microsoft는 WinUI의 본류 개발을 GitHub의 공개된 장으로 옮기는 방침을 2025년 여름에 단계적 접근으로 공식 발표했습니다. 미러의 갱신 빈도를 높이고, 로컬 빌드를 가능하게 하고, 테스트를 갖춘 뒤 커뮤니티의 기여를 받아들이고, 최종적으로 GitHub를 개발의 주 거점으로 삼는다는 네 단계입니다.22
이 글의 공개 시점(2026년 8월 말)에는 공식 문서도 「WinUI는 공개된 장에서 개발되고 있다(built in the open)」고 명시하고 있습니다. 공개 리포지토리에서 나날의 엔지니어링 진척을 따라갈 수 있게 되어 있습니다.9
WinForms에서 WPF, UWP, WinUI로 세대 교체를 쫓아온 개발자가 「또 프레임워크가 바뀌는 건가」라고 느끼는 것은 자연스럽습니다. 그러나 바뀌어 온 UI 프레임워크와, 그 아래의 기반은 나누어 볼 필요가 있습니다. Win32와 COM, 그리고 2012년 이후의 WinRT ABI는 같은 자리에 계속 있어 왔습니다.
WinUI의 창도, HWND에 뒷받침되어 있다
WinUI는 Windows App SDK의 일부로 제공되는 WinRT의 API군입니다.923 그 Microsoft.UI.Xaml.Window는 UWP 세대의 CoreWindow 기반 창 모델을 대체하는, HWND에 뒷받침된 창입니다. 공식 상호 운용 튜토리얼도 먼저 창 핸들을 얻는 데서 시작합니다.24
즉 WinUI 앱은 HWND의 창 위에서, IInspectable의 계약을 따르는 객체 트리가 동작하고 있는 Win32 앱입니다. COM에서 익힌 QueryInterface, 참조 카운트, 아파트먼트의 지식도, Win32에서 익힌 HWND와 메시지 루프의 지식도, WinUI의 문제 해결에 그대로 통용됩니다.
flowchart TB
accTitle: UI 프레임워크의 세대 교체와 변하지 않는 기반
accDescr: WinForms, WPF, UWP의 XAML, WinUI로 UI 프레임워크는 세대 교체를 거듭해 왔지만, WinForms와 WPF는 Win32와 COM의 기반에 직접 얹히고, UWP의 XAML과 WinUI는 WinRT ABI를 거쳐 같은 Win32와 COM의 기반에 얹힌다. 세대 교체를 해 온 것은 위의 계층이고, 발밑의 계약은 변하지 않았다
gen["UI 프레임워크의 세대 교체"]
gen --> f1["WinForms, WPF"]
gen --> f2["UWP의 XAML"]
gen --> f3["WinUI(현재)"]
f2 --> abi3["WinRT ABI(2012년~)"]
f3 --> abi3
f1 --> stable["변하지 않는 기반(Win32 + COM)"]
abi3 --> stable
그림 14: 부침을 거듭해 온 것은 프레임워크 계층. WinForms, WPF는 Win32+COM에 직접, UWP, WinUI는 WinRT ABI를 거쳐, 같은 기반에 얹혀 있다.
flowchart TB
accTitle: WinUI 앱을 떠받치는 계층
accDescr: WinUI의 XAML과 컨트롤은 Windows App SDK의 일부로 제공되어 WinRT의 ABI 즉 IInspectable의 계약 위에서 동작하며, 그 아래는 COM과 Win32의 HWND라는 기반이다. UI 프레임워크의 세대 교체 아래에서 이 발밑의 계약은 변하지 않았다
ui["WinUI(XAML, 컨트롤)"]
sdk["Windows App SDK"]
abi2["WinRT ABI(IInspectable)"]
base2["COM + Win32(HWND)"]
ui --> sdk
sdk --> abi2
abi2 --> base2
그림 15: WinUI 아래에는 WinRT ABI가 있고, 그 아래는 고전 COM과 Win32다. 쌓는 방식이 바뀌었을 뿐 기반은 같다.
XAML Islands에 의한 부분 혼재는, 세대별 제약을 확인한다
「기존 WPF/WinForms 화면에 WinUI 컨트롤만 섞는다」는 전략은 XAML Islands의 세대를 나누어 평가해야 합니다.
| 세대 | WPF/WinForms에서 쓸 때의 상황 |
|---|---|
| UWP 세대의 XAML Islands | Windows Community Toolkit의 래퍼 컨트롤이 있다. 다만 WPF/WinForms용은 .NET Core 3.x 세대까지이고, 현행 .NET에서는 지원되지 않는다25 |
| WinUI 3 세대 | Windows App SDK의 DesktopWindowXamlSource로 WPF, WinForms, Win32에서 호스팅할 수 있다. 다만 UWP 세대 같은 편리한 래퍼 컨트롤이 없어, 호스팅 API를 직접 다루는 구현과 검증의 부담이 있다26 |
단계적 혼재를 계획한다면, 컨트롤 단위의 혼재만을 전제로 하지 않는 편이 안전합니다. 기능 단위의 WinRT API 사용(6장)이나, 화면, 프로세스 단위의 분리를 축으로 생각하는 편이 2026년 시점에서는 견실합니다.
8. 업무 앱에 대한 함의 - 전면 이행과 부분 사용은 별개의 문제
「WinRT는 새로운 다른 세계이므로, 쓰려면 기존 자산을 버리고 다시 만드는 수밖에 없다」. 수탁 개발 현장에서 만나는 이 오해는 둘로 나누면 풀립니다.
기존 COM 자산과 WinRT는 공존할 수 있다
Excel COM 자동화를 쓰고, ActiveX 컨트롤을 얹고, 자사 COM 컴포넌트를 호출하고 있는 WPF 앱에 WinRT API를 더할 수 있습니다. 특별한 곡예가 아니라, 같은 COM 기반 위에서 기능을 조합하고 있기 때문입니다.
예를 들어 토스트 알림이라면, TFM을 설정해 API를 참조하고, 알림을 내보내기 위한 등록 조건을 충족합니다. 후자를 잊지 않도록 9장에서 경로별로 정리합니다. C++/WinRT에서는 WinRT와 고전 COM 양쪽 인터페이스를 winrt::com_ptr나 winrt::implements라는 같은 구조로 다룰 수 있습니다.21
UI의 쇄신과, 기능의 추가는 따로따로 견적을 낸다
「UI를 WinUI로 전면 이행할 것인가」와 「WinRT API를 필요한 곳에만 쓸 것인가」는 규모도 기간도 위험도 다른 판단입니다.
UI 전면 이행은 프레임워크 선정의 문제로, 화면 자산, 서드파티 컨트롤, 개발 체제에 좌우됩니다. 이 판단은 WinForms/WPF/WinUI 고르는 법에서 다루었습니다. 한편 WinRT API의 부분 사용은 기존 앱에 오늘부터 착수할 수 있는 작은 개선입니다.
토스트 알림만 원하는 안건에서 UI 전면 이행을 견적 낼 필요는 없습니다. 반대로 「WinUI로 이행하지 않으니까」라는 이유로 WinRT API의 사용까지 보류할 필요도 없습니다.
flowchart TB
accTitle: 전면 이행과 부분 사용은 별개의 판단
accDescr: WinUI로의 UI 전면 이행은 프레임워크 선정의 큰 판단으로 화면 자산이나 체제에 좌우되는 데 비해, WinRT API의 부분 사용은 TFM 설정 등으로 기존 WPF나 WinForms 앱에 오늘부터 더할 수 있는 작은 판단이며, 이 둘을 혼동하지 말고 나누어 검토한다
goal["「새로운 Windows 기능을 쓰고 싶다」"]
goal --> big["UI 전면 이행(큰 판단)"]
goal --> small["WinRT API 부분 사용(작은 판단)"]
big -.-> dep["화면 자산, 체제에 좌우"]
small -.-> today["기존 앱에 오늘부터 더할 수 있다"]
그림 16: 「새로운 기능」으로 가는 길은 두 갈래이며, 혼동하면 견적도 판단도 어긋난다.
9. 판단표 - 남기기, 감싸기, 교체하기의 WinRT판
상황별로, 필요한 변경만 고른다
ActiveX 판단표의 연장으로서, WinRT 쪽의 상황별 판단을 정리합니다.
| 상황 | 권장 | 이유 |
|---|---|---|
| WPF/WinForms에서 토스트, 공유, 블루투스 등 WinRT API를 쓰고 싶다 | TFM 설정(또는 C++/WinRT 도입)으로 부분 사용 | UI 이행 없이 오늘부터 호출할 수 있다. 다만 공유 UI는 IDataTransferManagerInterop 경유(6장), 토스트는 표 아래의 보충을 참조106 |
| 피커, 대화 상자에서 예외나 조용한 실패 | IInitializeWithWindow로 HWND를 넘긴다. 신규는 WindowId를 지원하는 새 피커(Windows App SDK 도입이 전제) |
CoreWindow를 전제로 한 설계를 HWND로 메우는 공식 작법619 |
| 알림 기록, 점프 목록 등이 동작하지 않는다 | MSIX 또는 외부 위치를 가리키는 패키지로 package identity를 부여 | identity가 필수인 API는 패키징이 전제720 |
| 기존 COM/ActiveX/OLE 자산 | 버리지 않는다. 남기기/감싸기/교체하기는 자산별로 판단 | WinRT와 배타적이지 않고 같은 기반에서 공존할 수 있다(8장) |
| 신규 데스크톱 앱의 UI | WinUI를 제1 후보로 평가(WPF도 현역) | 공개 개발로 투자 방향이 명확해졌다. 발밑은 WinRT ABI229 |
| 기존 WPF/WinForms 화면에 WinUI 컨트롤을 부분 혼재 | 래퍼 부재로 인한 구현 부담을 감안해 신중하게 평가 | UWP 세대 Islands는 .NET Core 3.x까지, WinUI 3 세대는 호스팅 API뿐2526 |
| 「WinRT는 UWP와 함께 끝났다」는 전제의 계획 | 전제를 수정한다 | WinRT는 데스크톱에서 호출할 수 있는 현행 API 기반5 |
토스트 알림은, TFM 설정만으로는 표시되지 않는다
API를 호출할 수 있게 하는 설정과, 알림을 표시하기 위한 등록은 별개입니다.「빌드는 통과하는데 알림이 뜨지 않는」 때에는 호출하는 쪽뿐 아니라, 사용하는 경로, 패키지 형태, 권한 상승 여부를 확인합니다.
기존의 ToastNotificationManager를 쓰는 경우
package identity를 갖지 않는 unpackaged 앱에서는, AppUserModelID(AUMID)를 할당한 시작 메뉴 바로 가기의 등록이 전제입니다. 이것이 없으면 토스트를 내보낼 수 없습니다.27
MSIX 등으로 패키징한 앱은 패키지의 identity가 AUMID를 주므로, 이 수동 등록은 필요 없습니다.
Windows App SDK의 AppNotificationManager를 쓰는 경우
현재 권장되는 이 경로에서는 Windows App SDK 도입과, 시작 시의 Register() 호출이 필요합니다. 그 위에서 패키지 형태에 따라 조건이 갈립니다.28
| 패키지 형태 | 추가로 확인할 것 |
|---|---|
| unpackaged | 배포 대상 각 PC에 Windows App SDK 런타임의 배치가 필요. Register()가 COM 서버 등록을 수행한다 |
| MSIX로 패키징 | Register()에 의한 자동 등록은 동작하지 않는다. Package.appxmanifest에 COM 활성자를 선언한다 |
나아가 Windows App SDK 경로에서는 관리자로 권한이 상승된 프로세스에서의 알림이 미지원입니다. Show는 예외를 던지지 않고 조용히 실패합니다. 권한 상승이 필요한 앱에서는 알림만 비상승 프로세스에 맡기는 분리를 검토해 주세요.28
등록 관련 구현의 세부 사항은 트레이 상주와 알림 구현 가이드에서 다루고 있습니다.
flowchart TB
accTitle: 토스트 알림의 두 경로와 필요한 등록
accDescr: 데스크톱 앱에서 토스트 알림을 내보내려면, 기존의 ToastNotificationManager 경로에서는 packaged인지 여부로 분기하며, unpackaged 앱은 AppUserModelID를 할당한 시작 메뉴 바로 가기의 등록을 거쳐야 하고, packaged 앱은 패키지의 identity가 AppUserModelID를 준다. Windows App SDK의 AppNotificationManager 경로에서는 SDK 도입과 시작 시의 Register 호출에 더해, unpackaged 앱은 배포 대상 각 PC로의 런타임 배치를, MSIX로 패키징한 앱은 매니페스트로의 COM 활성자 선언을 충족해야 비로소 앞으로 나아갈 수 있다. 나아가 Windows App SDK 경로에는 권한 상승 프로세스인지 여부의 분기가 있어, 관리자로 권한이 상승된 프로세스에서의 알림은 미지원이고 Show는 예외를 던지지 않고 조용히 실패하므로, 이 경로에서 알림이 표시되는 것은 비상승 프로세스인 경우로 한정되며, 권한 상승이 필요한 앱은 알림을 비상승 프로세스로 분리한다
want["토스트를 내보내고 싶다"]
want --> c1["기존: ToastNotificationManager"]
want --> c2["WASDK: AppNotificationManager"]
c1 --> q1{"packaged?"}
q1 -->|"아니오"| s1["AUMID 바로 가기 등록"]
q1 -->|"예"| s2["identity가 AUMID를 제공"]
c2 --> r2["SDK 도입 + Register()"]
r2 --> q2{"packaged?"}
q2 -->|"아니오"| s3["런타임 배치"]
q2 -->|"예"| s4["매니페스트에 COM 선언"]
s1 --> shown["알림이 표시된다"]
s2 --> shown
s3 --> elev{"권한 상승 프로세스?"}
s4 --> elev
elev -->|"아니오"| shown
elev -->|"예"| fail["미지원: Show가 조용히 실패"]
fail -.-> comp["알림은 비상승 프로세스로 분리"]
그림 17: 어느 경로든 패키징 형태에 따른 전제를 충족해야 비로소 표시에 도달할 수 있다. Windows App SDK 경로는 전제가 모두 갖춰져도 권한 상승 프로세스에서는 표시되지 않는다.
마지막으로, 남기기, 감싸기, 교체하기로 돌아간다
새로운 기능이 필요한 것인가, UI 자체를 쇄신하고 싶은 것인가. 이 물음으로 돌아가면 WinRT를 쓰는 것과 기존 자산을 교체하는 것을 떼어 놓고 판단할 수 있습니다.
flowchart TB
accTitle: 기존 자산과 WinRT를 대하는 방식의 판단 흐름
accDescr: 기존 데스크톱 앱을 기점으로, 새로운 Windows 기능이 필요 없으면 남기고, 기능이 필요하면 WinRT API의 부분 사용으로 감싸며(그때는 6장의 HWND, package identity, 스레드 초기화라는 막히는 지점에 주의), UI 자체의 쇄신이 필요한 경우에 한해 WinUI로의 교체를 검토한다는 단계적 판단의 흐름
start["기존 데스크톱 앱"] --> q2{"무엇이 필요한가?"}
q2 -->|"현재 상태로 충분"| keep2["남긴다(그대로 유지보수)"]
q2 -->|"새로운 기능"| wrap2["감싼다(WinRT API 부분 사용)"]
q2 -->|"UI의 쇄신"| rep["교체한다(WinUI를 평가)"]
wrap2 -.-> note2["HWND, identity, 초기화에 주의(6장)"]
그림 18: ActiveX 판단표와 같은 「남기기, 감싸기, 교체하기」의 구도가 WinRT 쪽에도 그대로 들어맞는다.
10. 정리
WinRT는 COM과 분리된 새로운 실행 환경이 아닙니다. COM의 바이너리 계약에, 메타데이터와 각 언어로의 프로젝션을 조합한 API 기반입니다.
| 요소 | 이 글에서 짚은 역할 |
|---|---|
IUnknown과 IInspectable |
QueryInterface, 참조 카운트를 기반으로 하고, 형식 이름 등을 얻는 세 메서드를 추가한다 |
.winmd |
형식 이름에서 정의를 끌어낼 수 있는, 모든 언어 공통의 계약서. ECMA-335 형식을 빌리지만 시스템이 제공하는 것에는 실행 코드가 들어 있지 않다 |
| C++/WinRT, C#/WinRT | 계약서에서 각 언어용 API를 생성한다. C#의 프로젝션은 .NET 5 이후 런타임에서 독립한 툴체인이 되었다 |
겉모습이 C#이나 C++의 자연스러운 API가 되어도, 그 아래에서는 vtable을 거치는 COM 호출과 HRESULT가 사용됩니다. 그래서 데스크톱에서의 HWND 전달, package identity, STA/MTA와 스레드 초기화를 이해하면 WinRT의 문제도 가려내기 쉬워집니다.
WinUI의 본류 개발을 공개된 장으로 옮기는 노력은 2025년 여름에 단계적 접근으로 발표되었습니다. 이 글의 공개 시점에는 공식 문서에도 「built in the open」이라고 되어 있지만, 그 아래의 IUnknown → IInspectable이라는 계약이 바뀐 것은 아닙니다.229
업무 앱에서의 결론도 같습니다. 기존 COM/ActiveX 자산과 WinRT는 공존할 수 있습니다. UI의 전면 이행과 WinRT API의 부분 사용을 혼동하지 않는 것이 견적과 판단의 정확도를 지켜 줍니다.
OLE 글에서 본 1990년대의 복합 문서도, 현재의 WinUI도, 같은 IUnknown 위에 있습니다. COM을 안다는 것은 단순히 「레거시에 밝다」는 뜻이 아닙니다. 지금 Windows의 발밑을 읽어 낼 수 있다는 뜻입니다.
관련 글
- COM이란 무엇인가 - Windows COM의 설계가 지금도 아름다운 이유
- COM / ActiveX / OCX란 무엇인가 - 차이와 관계를 한번에 정리
- COM STA/MTA의 기초 지식 - 스레드 모델과 행을 피하는 사고방식
- ActiveX / OCX를 지금 어떻게 다룰 것인가 - 남기기, 감싸기, 교체하기 판단표
- WinForms/WPF/WinUI 고르는 법 - 실무 판단표
- OLE 객체란 무엇인가 - 임베드, 링크의 구조와 업무 문서의 함정
- .NET 8 DLL을 VBA에서 타입 있게 쓰는 방법 - COM 공개와 dscom TLB
관련 상담 영역
합동회사 고무라소프트에서는 COM 컴포넌트 개발과, COM 자산을 포함한 Windows 업무 앱의 유지보수, 수정, 교체를 다루고 있습니다. 「WPF 그대로 토스트 알림이나 피커를 쓰고 싶다」 「WinRT API를 호출했더니 예외로 멈췄다」 「WinUI로 이행해야 할지, 기존 자산을 살려야 할지 판단하고 싶다」처럼, 이 글의 내용이 그대로 논점이 되는 단계부터 상담하실 수 있습니다.
참고 링크
-
Microsoft Learn, Consume COM components with C++/WinRT. COM에서는 객체가 아니라 인터페이스를 통해 프로그래밍한다는 점, 그것이 COM의 진화(an evolution of COM)인 WinRT API의 무대 뒤에서도 성립한다는 점, winrt::com_ptr에 의한 COM 스마트 포인터로 WinRT와 고전 COM을 같은 방식으로 다룰 수 있다는 점에 대하여. ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, Windows Metadata (WinMD) files. WinRT API가 .winmd라는 기계 판독 가능 메타데이터로 기술되어 도구와 언어 프로젝션이 이용한다는 점, Windows가 시스템 제공 전체 WinRT API의 메타데이터를 함께 담고 해석용 API를 제공한다는 점, 서드파티도 같은 형식으로 언어 프로젝션에 참여할 수 있다는 점, 물리 형식이 ECMA-335 사양(CLR 어셈블리와 동일)이며 시스템 제공 WinMD가 순수한 메타데이터라는 점에 대하여. ↩ ↩2 ↩3 ↩4 ↩5
-
Microsoft Learn, Windows Runtime (WinRT) language projections. 언어 프로젝션이 WinRT API를 각 언어의 관용에 맞게 공개한다는 점, .winmd가 WinRT API를 정의하고 프로젝션이 그것을 읽는다는 점, Microsoft가 지원하는 것은 C++/WinRT(C++17 이상)와 C#/WinRT(.NET) 두 가지라는 점에 대하여. ↩ ↩2 ↩3
-
Microsoft Learn, WinRT APIs callable from a desktop app. 대부분의 WinRT API가 .NET 및 네이티브 C++ 데스크톱 앱에서 이용할 수 있다는 점, CoreDispatcher, CoreWindow, ApplicationView 등 UWP 전용으로 설계된 클래스가 예외라는 점에 대하여. ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, Display WinRT UI objects that depend on CoreWindow. 일부 피커, 팝업, 대화 상자가 CoreWindow에 의존한다는 점, CoreWindow가 데스크톱 앱에서 지원되지 않는다는 점, IInitializeWithWindow(또는 동등한 IDataTransferManagerInterop)를 구현하는 클래스에는 표시 전에 소유자 창의 HWND를 설정할 수 있다는 점, WinUI 3, WPF, WinForms 각각에서의 절차에 대하여. ↩ ↩2 ↩3 ↩4 ↩5 ↩6
-
Microsoft Learn, WinRT APIs not supported in desktop apps. 데스크톱 앱에서 쓸 수 없는 WinRT API의 두 계통으로 UWP 전용 UI 기능에 의존하는 API와 package identity를 요구하는 API(ToastNotificationHistory, JumpList 등)가 있다는 점, 후자는 MSIX로 패키징된 앱에서만 지원된다는 점에 대하여. ↩ ↩2 ↩3 ↩4 ↩5
-
Microsoft Learn, RoInitialize function (roapi.h). RoInitialize가 현재 스레드를 지정한 동시성 모델(RO_INIT_SINGLETHREADED/RO_INIT_MULTITHREADED)로 Windows Runtime용으로 초기화한다는 점, WinRT 객체를 활성화하고 조작하는 모든 스레드가 사전 초기화를 필요로 한다는 점, MTA로 초기화된 스레드에서 모순되는 지정을 하면 RPC_E_CHANGED_MODE가 된다는 점에 대하여. ↩ ↩2
-
Microsoft Learn, WinUI 3. WinUI가 신규 Windows 데스크톱 앱을 위해 권장되는 네이티브 UI 프레임워크라는 점, Windows App SDK의 일부로 제공된다는 점, Windows 10 버전 1809 이상에서 동작한다는 점, 공개된 장에서 개발되고 있다는 점에 대하여. ↩ ↩2 ↩3 ↩4 ↩5
-
Microsoft Learn, Call Windows Runtime APIs in desktop apps. .NET 6 이상에서 Windows OS 버전이 붙은 TFM(net10.0-windows10.0.22621.0 등)을 지정하면 Windows SDK targeting package가 참조되어 WinRT API를 호출할 수 있다는 점, C++는 Microsoft.Windows.CppWinRT NuGet 패키지와 C++17 이상으로 C++/WinRT를 쓴다는 점에 대하여. ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, Introduction to C++/WinRT. C++/WinRT가 완전히 표준적인 현대 C++17에 의한 언어 프로젝션이며 헤더 파일 기반 라이브러리로 구현된다는 점, C++/CX와 WRL의 권장 후속이라는 점, WinRT가 COM API에 기반하며 언어 프로젝션을 통해 접근하도록 설계되어 있다는 점, 프로젝션이 COM의 세부 사항을 감춘다는 점, cppwinrt.exe가 .winmd에서 프로젝션 헤더를 생성한다는 점에 대하여. ↩ ↩2 ↩3
-
Microsoft Learn, The Windows Runtime (WinRT) type system. 모든 WinRT 인터페이스가 암묵적으로 IInspectable을 요구하고 IInspectable이 IUnknown을 요구한다는 점, IUnknown이 QueryInterface, AddRef, Release를 정의한다는 점, IInspectable이 추가하는 GetIids, GetRuntimeClassName, GetTrustLevel의 세 메서드, GetRuntimeClassName이 메타데이터로 해석 가능한 형식 이름을 반환함으로써 언어 프로젝션을 가능하게 한다는 점, 사용자 정의 인터페이스끼리의 상속이 WinRT의 형식 시스템에 없고 requires로 표현한다는 점에 대하여. ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, IInspectable interface (inspectable.h). IInspectable이 모든 WinRT 클래스에 필요한 기능을 제공한다는 점, IUnknown을 상속한다는 점, GetIids, GetRuntimeClassName, GetTrustLevel의 세 메서드를 가진다는 점에 대하여. ↩
-
Microsoft Learn, Introduction to Microsoft Interface Definition Language 3.0. MIDL 3.0이 WinRT 형식을 선언하기 위한 간결하고 현대적인 구문이라는 점, WinRT의 계약이 지금도 IDL로 기술되며 MIDL 컴파일러가 Windows 메타데이터(.winmd)를 생성한다는 점에 대하여. ↩
-
Microsoft Learn, C#/WinRT. C#/WinRT의 NuGet 패키지에 포함된 cswinrt.exe가 .winmd를 처리해 C# 코드를 생성하고 상호 운용 어셈블리로 컴파일한다는 점, C++/WinRT가 C++용 헤더를 생성하는 것과 같은 위치라는 점에 대하여. ↩
-
Microsoft Learn, Built-in support for WinRT is removed from .NET. .NET 5에서 Windows Runtime의 기본 제공 지원이 .NET에서 제거되고 CsWinRT 툴체인으로 이행했다는 점에 대하여. ↩ ↩2
-
Microsoft Learn, IInitializeWithWindow interface (shobjidl_core.h). 데스크톱 앱에서 사용되는 WinRT 객체에 소유자 창을 제공하기 위한 인터페이스라는 점, IUnknown을 상속하고 Initialize(HWND) 메서드를 가진다는 점에 대하여. ↩
-
Microsoft Learn, Use WinRT COM interop classes in .NET. 파일 피커나 대화 상자 등 일부 WinRT 객체가 데스크톱 앱에서 동작하기 전에 HWND를 필요로 한다는 점, WinRT.Interop.WindowNative와 WinRT.Interop.InitializeWithWindow라는 형식 안전한 C# 클래스로 손수 QueryInterface를 호출하지 않고 초기화할 수 있다는 점에 대하여. ↩ ↩2
-
Microsoft Learn, Tutorial: Open files and folders with pickers in WinUI. 기존의 Windows.Storage.Pickers를 데스크톱(WinUI 3) 앱에서 쓸 경우 표시 전에 HWND로 초기화하지 않으면 예외 또는 조용한 실패가 된다는 점, WinUI 3 데스크톱 앱에 CoreWindow가 없다는 점, 새로운 Windows App SDK의 피커(Microsoft.Windows.Storage.Pickers)는 생성자에서 WindowId를 받아 InitializeWithWindow 패턴이 필요 없다는 점에 대하여. ↩ ↩2 ↩3
-
Microsoft Learn, Features that require package identity. 일부 Windows 기능과 WinRT API가 실행 시에 package identity를 요구한다는 점, MSIX 패키지로의 배포에 더해 외부 위치를 가리키는 패키지(packaged with external location)로도 식별자를 얻을 수 있다는 점에 대하여. ↩ ↩2
-
Microsoft Learn, CoInitialize function (objbase.h). CoInitialize가 COM 라이브러리를 STA로 초기화한다는 점, 새로운 앱은 CoInitializeEx를 호출해야 한다는 점, Windows Runtime을 사용하는 경우에는 대신 RoInitialize 또는 Windows::Foundation::Initialize를 호출해야 한다는 점에 대하여. ↩
-
GitHub, WinUI: Now Developing in the Open (microsoft/microsoft-ui-xaml Discussion #10700). 2025년 7월 말의 공식 발표. WinUI의 리포지토리를 공개해 나가기 위한 단계적 접근(미러 갱신 빈도의 향상, 로컬 빌드의 실현, 테스트를 갖춘 뒤 커뮤니티 기여의 수용, 최종적으로 GitHub를 개발의 주 거점으로 삼는 것)이 제시되어 있다는 점에 대하여. ↩ ↩2 ↩3
-
Microsoft Learn, Windows App SDK. Windows App SDK가 WinUI를 포함한 현행 Windows 앱 개발 라이브러리군이라는 점에 대하여. ↩
-
Microsoft Learn, Walkthrough: WinUI 3 app with Win32 interop. WinUI의 Window 클래스가 데스크톱 창을 지원하도록 확장되었다는 점, WinUI 3의 데스크톱 앱에서는 Window가 Win32의 창 핸들(HWND)에 뒷받침되어 있어 창 핸들을 얻어 Win32 API로 조작할 수 있다는 점에 대하여. ↩
-
Microsoft Learn, Host UWP XAML controls in desktop apps (UWP XAML Islands). UWP 세대의 XAML Islands가 WPF, WinForms, C++ 데스크톱 앱에 UWP XAML 컨트롤을 얹는 구조라는 점, WPF/WinForms에서의 이용이 .NET Core 3.x를 대상으로 하는 앱에 한정되며 현행 .NET이나 .NET Framework에서는 지원되지 않는다는 점에 대하여. ↩ ↩2
-
Microsoft Learn, DesktopWindowXamlSource Class (Microsoft.UI.Xaml.Hosting). Windows App SDK의 XAML 호스팅 API의 핵심 클래스이며, HWND에 연결된 임의의 UI 요소에 WinUI 컨트롤을 호스팅할 수 있다는 점, WPF, Windows Forms, Win32(Windows API)로 만든 데스크톱 앱에서 이용할 수 있다는 점에 대하여. ↩ ↩2
-
Microsoft Learn, Quickstart: Sending a toast notification from the desktop. 데스크톱 앱에서의 토스트 전송에는 System.AppUserModel.ID를 설정한 시작 메뉴의 바로 가기가 전제라는 점, CreateToastNotifier 호출에 그 AppUserModelID를 넘겨야 하며 없으면 토스트가 표시되지 않는다는 점에 대하여. ↩
-
Microsoft Learn, Use app notifications with a .NET app. WPF/WinForms 앱에서 Windows App SDK의 AppNotificationManager를 쓰려면 NotificationInvoked 핸들러 등록 후에 Register()를 호출해야 한다는 점, unpackaged 앱에서는 Register()가 알림 클릭 시 앱을 시작하기 위한 COM 서버 등록을 자동으로 수행한다는 점, 전제로 Windows App SDK 도입과 WinRT API 호출 구성이 필요하다는 점에 대하여. ↩ ↩2
관련 기사
같은 태그를 공유하는 최신 기사입니다. 더 가까운 주제로 지식을 넓힐 수 있습니다.
Windows 앱 외주·수탁 개발을 의뢰하기 전에 정리할 점
Windows 앱 외주·수탁 개발을 의뢰하기 전에, 기존 소프트웨어 수정, 장치 연동, COM/ActiveX, 배포·업데이트, 유지보수를 정리하는 포인트를 설명합니다.
OLE 개체란 무엇인가 - 포함과 연결의 구조, 그리고 업무 문서의 함정
Word에 Excel 표를 넣는 기능의 정체가 바로 OLE 개체입니다. 포함과 연결의 차이, 복합 파일과 구조화 스토리지, In-Place Activation의 구조부터 링크 깨짐, 파일 비대화, 보안 대책까지 실무 관점에서 설명합니다.
클립보드와 드래그 앤 드롭의 구조 ── 업무 앱에서 OLE 데이터 전송을 올바르게 다루기
Excel 표를 붙여 넣으면 서식이 무너지고, 원본을 닫으면 붙여 넣을 수 없다── 원인은 같은 내용을 여러 형식으로 두는 클립보드입니다. 표준 형식·지연 렌더링·OLE 드래그 앤 드롭부터 기록과 클라우드 동기 정책까지 설명합니다.
Windows 셸 통합의 현재 ── 컨텍스트 메뉴, 파일 연결, Windows 11의 변화
Windows 11에서 컨텍스트 메뉴가 「더 많은 옵션 표시」 뒤로 숨는 이유를, 확장자→ProgID→verb라는 파일 연결의 기본, 기존형 셸 확장의 주의점, IExplorerCommand와 MSIX/sparse package의 새 방식까지 이...
빠른 시작의 정체 ── Windows의 「종료」가 다시 시작과 다른 이유
Windows의 「종료」는 기본적으로 하이브리드 종료가 되어 커널과 드라이버가 최대 절전 모드 파일에 저장되고 다음 부팅에서 복원됩니다. 다시 시작해야만 해결되는 이유, 가동 시간·업데이트·Wake on LAN에 미치는 영향, 확인 방법과 비활성...
관련 토픽
이 기사와 가까운 토픽 페이지입니다. 기사를 출발점 삼아 관련 서비스와 다른 기사로 이어집니다.
Windows 기술 토픽
Windows 개발, 장애 조사, 기존 자산 활용에 관한 KomuraSoft LLC 기사를 모은 토픽 허브입니다.
ActiveX 이관
COM / ActiveX / OCX 자산을 유지할지, 감쌀지, 교체할지의 단계적 판단을 정리한 토픽 페이지입니다.
이 주제와 연결되는 서비스
이 기사는 다음 서비스 페이지로 이어집니다. 가까운 입구부터 확인해 주세요.
Windows 앱 개발
상주 처리, 장비 연동, 운영 로그, 유지 보수 가능한 구조가 필요한 Windows 데스크톱 애플리케이션을 지원합니다.
기존 자산 활용 & 이관 지원
COM / ActiveX / OCX 자산, 네이티브 코드, 32비트 의존성을 유지하면서 단계적인 이관 계획을 지원합니다.
자주 묻는 질문
이 기사 주제에 대해 상담 시 자주 나오는 질문을 모았습니다.
- WinRT는 .NET 같은 매니지드 런타임인가요?
- 아닙니다. WinRT(Windows Runtime)는 가상 머신이나 가비지 컬렉터를 가진 실행 환경이 아니라, COM을 기반으로 한 ABI(바이너리 계약)입니다. Microsoft 자신의 문서가 「The Windows Runtime is based on COM」이라고 명시하고 있으며, 모든 WinRT 인터페이스는 IUnknown에서 유래한 IInspectable을 요구합니다. 호출의 실체는 지금도 vtable을 거치는 COM 호출이며, 참조 카운트(AddRef/Release)와 QueryInterface 위에 성립합니다. 「.winmd」라는 이름이나 .NET과의 높은 친화성 때문에 매니지드 환경으로 오해하기 쉽지만, .winmd는 ECMA-335와 같은 물리 형식을 빌린 메타데이터 파일이며, 시스템이 제공하는 .winmd에는 실행 코드가 들어 있지 않습니다. C#에서 자연스럽게 호출할 수 있는 것은 언어 프로젝션이 메타데이터로부터 C#용 프로젝션을 생성하기 때문입니다.
- UWP는 이제 주류가 아니라고 들었습니다. 지금 와서 WinRT를 배울 의미가 있나요?
- 있습니다. UWP라는 앱 모델과 WinRT라는 API 기반은 별개입니다. UWP가 축소된 뒤에도 WinRT API의 대부분은 WPF, WinForms, Win32 데스크톱 앱에서 호출할 수 있는 형태로 계속 제공되고 있으며, 토스트 알림, 공유, 블루투스, OCR 등 「지금의 Windows가 가진 기능」의 상당수가 WinRT API로 공개되어 있습니다. 게다가 Microsoft가 현재 권장하는 네이티브 UI 프레임워크인 WinUI(Windows App SDK)는 WinRT의 ABI 위에 세워져 있습니다. 즉 WinRT의 구조(IInspectable, .winmd, 언어 프로젝션)는 UWP의 유산이 아니라, 현행 Windows 앱 개발의 발밑 그 자체입니다.
- WPF나 WinForms 앱에서 WinRT API를 호출할 수 있나요?
- 호출할 수 있습니다. .NET 6 이상이라면 프로젝트 파일의 TargetFramework를 net8.0-windows10.0.19041.0 처럼 Windows OS 버전이 붙은 TFM으로 바꾸기만 하면 Windows SDK의 프로젝션 어셈블리가 참조되어, Windows.* 네임스페이스의 WinRT API를 C#에서 직접 호출할 수 있습니다. C++라면 Microsoft.Windows.CppWinRT NuGet 패키지를 도입하고 C++17 이상에서 C++/WinRT를 사용합니다. 다만 막히는 지점이 세 가지 있습니다. 첫째, 피커나 대화 상자처럼 CoreWindow를 전제로 하는 UI 계열 클래스는 표시하기 전에 IInitializeWithWindow로 소유자 창의 HWND를 넘겨야 합니다(다만 공유 UI의 DataTransferManager는 예외로, IInitializeWithWindow가 아니라 전용 IDataTransferManagerInterop을 사용해 ShowShareUIForWindow에 HWND를 넘기는 별도 경로입니다). 둘째, 알림 기록이나 점프 목록 같은 일부 API는 package identity(MSIX 패키징 또는 외부 위치를 가리키는 패키지)를 요구합니다. 셋째, WinRT 객체를 다루는 스레드에는 사전 초기화가 필요합니다. 네이티브 코드에서는 winrt::init_apartment나 RoInitialize로 STA/MTA의 동시성 모델을 지정합니다(C#의 WPF/WinForms 앱에서는 보통 런타임 쪽이 처리해 줍니다). 참고로 CoreWindow나 ApplicationView 자체에 의존하는 API는 애초에 데스크톱 앱에서는 쓸 수 없습니다.
- 데스크톱 앱에서 FolderPicker 같은 피커를 호출하면 예외가 납니다. 왜 그런가요?
- 피커나 대화 상자 중 일부 WinRT 클래스가 표시 대상으로 UWP의 CoreWindow를 전제로 설계되어 있기 때문입니다. 데스크톱 앱에는 CoreWindow가 없으므로, 표시하기 전에 소유자 창을 명시적으로 알려 주어야 합니다. 구체적으로는 먼저 소유자 창의 HWND를 얻고(WinUI의 Window라면 WinRT.Interop.WindowNative.GetWindowHandle, WPF라면 WindowInteropHelper, WinForms라면 폼의 Handle 속성), C#이라면 WinRT.Interop.InitializeWithWindow.Initialize로 피커에 넘깁니다. C++/WinRT라면 객체를 IInitializeWithWindow로 QueryInterface 해서(as<IInitializeWithWindow>()) Initialize(hwnd)를 호출합니다. 이 초기화를 하지 않으면 예외가 나거나 조용히 실패합니다. 참고로 새로운 Windows App SDK의 피커(Microsoft.Windows.Storage.Pickers)는 생성자에서 WindowId를 받는 설계로 바뀌어 이 초기화 패턴 자체가 필요 없습니다(다만 Windows App SDK의 API이므로 TFM 설정만으로는 쓸 수 없고, SDK 도입과, unpackaged 앱이라면 배포 대상에 런타임을 배치하고 초기화하는 것이 전제입니다).
- WinUI 개발이 GitHub에서 공개되었다는데, 기존 WPF/WinForms 앱은 버려야 하나요?
- 서둘러 버릴 필요는 없습니다. WinUI 본류 개발의 공개는 「Microsoft가 네이티브 프레임워크에 본격적으로 투자한다」는 방향의 표명이지, 기존 프레임워크의 중단 선언이 아닙니다. WPF와 WinForms는 지금도 .NET의 일부로 계속 지원되고 있습니다. 판단을 두 가지로 나누어 주세요. 하나는 UI 프레임워크를 이행할 것인가 하는 판단으로, 이는 화면 자산의 규모, 서드파티 컨트롤, 개발 체제에 좌우되는 큰 이야기입니다. 다른 하나는 WinRT API를 기존 앱에서 부분적으로 사용할 것인가 하는 판단으로, 이쪽은 TFM 설정으로 오늘부터 시작할 수 있습니다. 다만 TFM은 API를 호출할 수 있게 해 줄 뿐이고, 예를 들어 토스트 알림에는 호출에 더해 알림 등록이 필요합니다(기존 경로는 unpackaged 앱의 경우 AppUserModelID가 붙은 바로 가기 등록 - 패키징되어 있다면 패키지의 identity가 AppUserModelID를 주므로 불필요합니다 - 이고, Windows App SDK 경로는 SDK 도입 - unpackaged 앱이라면 배포 대상 각 PC에 Windows App SDK 런타임 도입도 - 과 AppNotificationManager의 Register() 호출이며, MSIX로 패키징한 앱에서는 Register()에 의한 자동 등록이 동작하지 않고 나아가 Package.appxmanifest에 COM 활성자 선언이 필요합니다). 또한 Windows App SDK 경로에서는 관리자로 권한이 상승된 프로세스에서의 알림이 미지원이며, Show는 예외를 던지지 않고 조용히 실패합니다 - 권한 상승이 필요한 앱은 알림만 비상승 프로세스에 맡기는 분리를 검토해 주세요. 그래도 토스트 알림만 원한다면 WinUI로의 이행은 필요 없습니다. 참고로 기존 WPF/WinForms 화면에 WinUI 컨트롤을 부분적으로 섞는 것(XAML Islands)은 세대를 구별해야 합니다. UWP 세대의 Islands는 WPF/WinForms 지원이 .NET Core 3.x까지이고, WinUI 3 세대에는 Windows App SDK의 XAML 호스팅 API(DesktopWindowXamlSource)를 WPF/WinForms에서도 쓸 수 있지만 편리한 래퍼 컨트롤이 없어 구현 부담이 크므로, 현시점에서는 신중하게 평가하는 편이 안전합니다.