최신 Windows 앱 개발 스택을 써서 작은 유틸리티를 만들며 겪은 혼란, WinUI 3와 Windows App SDK의 한계, 그리고 왜 많은 개발자가 결국 Electron 같은 웹 기술로 향하는지에 대한 이야기.
나는 Windows 사람이다. 늘 그랬다. 내가 처음 접한 프로그래밍 책 중 하나는 Beginning Visual C++ 6였는데, 결정적으로 이 책에는 체험판 Visual C++가 함께 들어 있어서 열 살이던 나는 부모님 컴퓨터에 그것을 설치할 수 있었다. .NET 1.0이 나왔을 때 가족 여행 중이었던 기억도 난다. 나는 C# 두꺼운 책을 읽어 나가며, 내 Neopets 치트 프로그램들을 MFC에서 Windows Forms로 다시 쓸 준비를 하고 있었다. 대학교를 졸업한 뒤 처음 가진 직장조차도 .NET 회사였지만, 나는 주로 프론트엔드 작업을 했다.
Windows 개발 생태계를 곁에서 계속 지켜보긴 했지만, 내 직업적 경력에서 네이티브 Windows 앱을 직접 쓰는 일은 없었다. (Chromium은 기술적으로는 네이티브 앱이지만, 사실상 그 자체의 운영 체제에 더 가깝다.) 그리고 취미 프로젝트에는 언제나 웹이 더 나은 선택이었다. 하지만 어린 시절의 좋은 기억에 이끌려, 작고 재미있는 Windows 유틸리티 프로그램을 하나 써 보는 것이 좋은 은퇴 프로젝트가 될지도 모르겠다고 생각했다.
그런데. 여기서 보고하자면, 이 판은 완전히 엉망이다. 요즘 아무도 네이티브 Windows 애플리케이션을 쓰지 않고 대신 Electron으로 가는 이유를 나는 완전히 이해하게 되었다.
내가 만든 유틸리티 Display Blackout은 내게 꼭 필요했던 것을 해결해 주었다. 모니터 세 대를 쓰는 환경에서 게임을 할 때, 좌우 디스플레이를 까맣게 만들고 싶었다. 모니터 전원을 꺼 버리면 Windows가 몇 초 동안 난리를 치고 현재 창 배치도 전부 망가뜨린다. 하지만 OLED 모니터에서는 검은 오버레이를 띄우면 모든 픽셀이 꺼지므로, 사실상 같은 효과를 낼 수 있다.
분명히 말하지만, 이것은 독창적인 아이디어가 아니다. 나는 원래 AutoHotkey 스크립트를 사용하고 있었는데, 이 글을 쓰면서 확인해 보니 그것이 이후 완전한 Windows 애플리케이션으로 발전해 있었다. 이 아이디어의 다른 | 변형들도 Microsoft Store에서 구할 수 있다. 그래도 나는 조금 더 보기 좋고 현대적인 UI를 만들 수 있겠다고 생각했고, 어쨌든 목적은 상용 제품을 만드는 것이 아니라 배우는 것이었다.
우리 목적에서 이 앱이 흥미로운 이유는, 이런 종류의 기능이 필요하다는 점이다.
앞으로 읽어 나가면서 이것들을 기억해 두자.

내가 만든 이 아름다운 UI를 보라. 분명히 이 분야의 다른 모든 소프트웨어보다 낫다는 데 동의할 것이다.
태초에 Win32 API가 있었다. C로 된 API였다. 불행히도 이 API는 오늘날에도 여전히 아주 중요하며, 내 프로그램에도 그렇다.
시간이 지나며 그 위에 일련의 추상화가 생겨났다. .NET 이전 시대의 대표적인 것은 MFC C++ 라이브러리였는데, 당시로서는 현대적이던 클래스와 템플릿 같은 언어 기능을 사용해 날것의 C 함수 위에 약간의 객체 지향성을 더했다.
추상화 열차는 .NET이 등장하면서 본격적으로 달리기 시작했다. .NET은 여러 가지였지만, 우리 목적에 가장 중요한 부분은 Java와 비슷한 방식으로 새로운 가상 머신 위에서 JIT 바이트코드로 실행되는 새 프로그래밍 언어 C#의 도입이었다. 이것은 Windows 프로그래밍에 자동 메모리 관리, 즉 메모리 안전성을 가져왔고, 전반적으로 Microsoft 생태계에 더 현대적인 기반을 제공했다. 추가로, .NET 라이브러리에는 Windows와 상호 작용하기 위한 완전히 새로운 API 집합도 포함되어 있었다. 특히 UI 측면에서 .NET 1.0 (2002)은 Windows Forms로 시작했다. 이것은 MFC와 비슷하게, Win32 윈도잉 및 컨트롤 API를 대체로 감싼 래퍼였다.
.NET 3.0 (2006)에서 Microsoft는 WPF를 도입했다. 이제 모든 컨트롤을 C# 객체로 만드는 대신, XAML이라는 별도의 마크업 언어가 생겼다. HTML과 JavaScript의 관계에 더 가까웠다. 또한 이것은 운영 체제와 함께 제공되는 Win32 API 컨트롤을 감싸는 대신, GPU 위에서 컨트롤을 처음부터 다시 그린 첫 사례이기도 했다. 당시에는 이것이 신선한 출발점이자, 앞으로 한동안 Windows 앱의 미래를 떠받칠 좋은 기반처럼 느껴졌다.
그다음 큰 방향 전환은 Windows 8 (2012)과 함께 WinRT가 도입되면서 일어났다. .NET과 비슷하게, 이것은 Windows 애플리케이션을 작성하는 데 필요한 모든 기능에 대해 새로운 API를 만들려는 시도였다. 개발자들이 WinRT의 테두리 안에 머문다면, 그들의 앱은 Android와 iOS 앱처럼 샌드박스된 현대적 기준을 충족하고 Windows 데스크톱, 태블릿, 휴대폰 전반에 배포될 수 있었다. UI 측면에서는 여전히 XAML 기반이었지만, 더 제약된 다중 디바이스 타깃을 지원하기 위해 WPF와는 모든 것이 조금씩 달랐다.
이 전략은 Windows 10 (2015)에서 UWP로 다시 손질되었고, 더 강력한 데스크톱/휴대폰/Xbox/HoloLens 앱을 허용하기 위해 일부 샌드박스 제한이 완화되었지만, 여전히 WPF를 사용하는 완전한 .NET 앱만큼의 권한은 아니었다. 동시에 WinRT와 UWP에서는 푸시 알림, 라이브 타일, Microsoft Store 게시 같은 특정 새로운 OS 수준 기능과 통합이 이 프레임워크를 사용하는 앱에만 허용되었다. 그 결과 Chrome이나 Microsoft Office 같은 애플리케이션이 구식 코어 주위에 WinRT/UWP 브리지 앱을 두고, IPC 같은 방식으로 통신하는 어색한 구조가 생겨났다.
Windows 11 (2021)에서 Microsoft는 마침내 모두를 더 샌드박스화되고 더 현대적인 플랫폼으로 옮기려는 시도를 포기했다. Windows App SDK는 이제 예전의 WinRT/UWP 전용 기능을 표준 C++로 작성되었든 (더 이상 C++/CLI 불필요) .NET으로 작성되었든, 모든 Windows 앱에 노출한다. 이 SDK에는 또 하나의 XAML 기반, 처음부터 다시 그린 컨트롤 라이브러리인 WinUI 3가 포함된다.
그러니 이 모든 것을 다 따라왔는가? UI 프레임워크의 진화만 봐도 이렇다.
Win32 C APIs → MFC → WinForms → WPF → WinRT XAML → UWP XAML → WinUI 3
이것이 학습 프로젝트라는 정신에 맞게, 나는 최신의 최고급 퍼스트파티 기반을 쓰고 싶다는 것을 알고 있었다. 그것은 Windows App SDK를 사용해 WinUI 3 앱을 작성한다는 뜻이었다. 그런데 이 일을 하는 방법은 결국 세 가지가 있다.
이 선택은 고통스럽다. C++는 가벼운 앱을 만들어 주고, Windows APP SDK 라이브러리에 런타임 링크되며, 필요할지도 모르는 Win32 C API와의 하향 인터롭도 쉽다. 하지만 2026년에 C++ 같은 메모리 비안전 언어로 그린필드 애플리케이션을 작성하는 것은 범죄다.
이상적으로는 시스템의 .NET을 사용하고, 브라우저가 제공하는 동일한 웹 플랫폼을 모든 웹 앱이 공유하듯 C# 바이트코드만 배포할 수 있으면 좋겠다. 이것을 “framework-dependent deployment”라고 한다. 그런데 내가 이해할 수 없는 이유로 Microsoft는 최신 Windows 11 버전조차도 .NET 4.8.1만 기본 설치해 두기로 결정했다. (현재 .NET 버전은 10이다.) 그래서 이런 방식으로 앱을 배포하면 공유지의 비극이 발생한다. 현대적인 .NET이 필요한 첫 번째 앱이 Windows로 하여금 사용자에게 .NET 라이브러리를 다운로드하고 설치하라는 대화상자를 띄우게 만들기 때문이다. 최적의 사용자 경험이 아니다!
남는 것은 .NET AOT뿐이다. 맞다. 나는 가상 머신, 가비지 컬렉터, 표준 라이브러리 등을 포함한 전체 .NET 런타임을 내 바이너리에 컴파일하고 있다. 컴파일러가 사용하지 않는 코드를 잘라내려고 하긴 하지만, 그 결과물도 모니터 몇 대를 까맣게 만드는 앱치고는 당당한 9 MiB다.
(“Rust는요?”라고 묻는 소리가 들린다. Microsoft 주변에서 Windows App SDK용 Rust 바인딩을 유지하려는 시도가 있었지만, 포기했다.)
배포 방식에서도 비슷하게 고통스러운 선택이 있다. Windows는 손수 만든 혹은 서드파티 도구가 생성한 setup.exe 설치 프로그램도 기꺼이 지원하지만, 컨테이너화된 설치/제거를 갖춘 현대적 앱에 대해 Microsoft가 권장하는 경로는 MSIX다. 하지만 이 형식은 코드 서명 인증서에 크게 의존하는데, 미국 외 거주자에게는 연간 대략 $200–300 정도 드는 듯하다. 서명되지 않은 사이드로딩 경험은 끔찍하다. 관리자 터미널에서만 사용할 수 있는 불친절한 PowerShell 명령이 필요하다. Microsoft가 내 앱을 스토어에 받아 주기만 하면 사이드로딩을 피할 수 있었겠지만, 그들은 “지속되는 고유한 가치”를 제공하지 않는다는 이유로 거절했다.
여기서의 비극은 이 모든 것이 너무 불필요해 보인다는 점이다. .NET은 Windows Update를 통해 배포될 수 있었을 테니, 최신 버전이 항상 존재하게 되어 framework-dependent deployment가 실용적이 되었을 것이다. 아니면 최소한 .NET용 MSIX 패키지라도 있어서 다른 MSIX 패키지들이 그것에 대한 의존성을 선언할 수 있게 할 수 있었을 것이다. 서명되지 않은 MSIX 사이드로드는 EXE 설치 프로그램이 받는 것과 같은 군중 기반 평판 시스템을 사용한다. Windows 코드 서명 인증서는 Apple 생태계의 비슷한 비용처럼 연간 $100 정도면 될 수도 있었다. 하지만 현대 Windows 개발의 다른 모든 것과 마찬가지로, 이것도 그냥 … 어설프다.
몇 년마다 운영 체제와 UI API를 다시 만드는 일은 엄청난 노동이라는 것이 드러난다. 여기에 간헐적인 샌드박싱 시도와 “너무 강력한” 기능의 폐기 시도까지 겹치면, 각각의 새로운 계층에는 빈틈이 생긴다. 이전 프레임워크에서는 가능했던 어떤 일을 더 이상 할 수 없게 되는 것이다.
이것은 새로운 문제가 아니다. MFC 시절에도 자주 Win32 API로 내려가야 하는 경우를 발견하곤 했다. 그리고 .NET에는 1.0부터 P/Invoke가 있었다. 그래서 특히 지금처럼 Microsoft가 새로운 기능과 맞바꿔 최신 프레임워크만 쓰라고 강요하지 않는 상황에서는, 이전 계층으로 내려가야 한다고 해서 세상이 끝나는 것은 아니다. 하지만 짜증 나는 일이다. 코드의 절반이 옛 API에 접근하기 위한 인터롭 진흙탕이라면, Microsoft의 최신 최고 제품을 쓰는 의미가 무엇인가? C API를 한 무더기 감싸야 한다면, C#으로 프로그래밍하는 의미가 무엇인가?
내 앱이 해야 하는 일 목록을 다시 보고, Windows App SDK로 무엇을 할 수 있는지 비교해 보자.
머신의 디스플레이와 그 경계를 열거하기: foreach 루프 대신 for 루프를 사용한다면 열거할 수는 있다. 하지만 변경 사항을 감시하려면 P/Invoke가 필요하다. 현대적 API가 실제로 작동하지 않기 때문이다.
테두리 없고, 제목 표시줄 없고, 포커스를 빼앗지 않는 검은 창 배치하기: 상당 부분은 가능하지만, 포커스를 빼앗지 않는 동작은 P/Invoke가 필요하다.
전역 키보드 단축키 가로채기: 안 된다. P/Invoke가 필요하다.
선택적으로 시작 시 실행하기: 가능하다. 시스템 설정과 통합된, 기본적으로 꺼져 있는 멋진 API가 있다.
몇 가지 영속 설정 저장하기: 가능하다.
몇 개의 메뉴 항목이 있는 트레이 아이콘 표시하기: 제공되지 않는다. 트레이 아이콘 자체가 P/Invoke를 필요로 할 뿐 아니라, 트레이 아이콘용 메뉴 개념도 표준화되어 있지 않아서 어떤 래퍼 패키지를 고르느냐에 따라 여러 다른 컨텍스트 메뉴 스타일 중 하나를 얻게 된다.
하지만 이것들은 그저 대표 기능들일 뿐이다. 앱 창 크기를 내용에 맞게 자동으로 조정하는 것처럼 단순한 기능조차 WPF에서 WinUI 3로 오는 어딘가에서 사라져 버렸다.
Win32 C API를 이렇게 자주 다시 호출해야 한다는 점을 생각하면, 인터롭 기술 자체도 전환 중이라는 사실은 전혀 도움이 되지 않는다. 현대적인 방식은 CsWin32라는 무엇인가인 듯한데, P/Invoke의 고통을 조금 덜어 주기 위한 것이다. 하지만 이것은 구조체 안의 문자열조차 올바르게 래핑하지 못한다. 내가 보기에는 이것도 예산이 부족하고 영원히 1.0 이전 상태인 프로젝트 중 하나처럼 보이며, 의욕을 불러일으키지 않는 변경 로그를 달고 몇 년 뒤 버려질 궤도에 올라 있다.
그리고 CsWin32의 문제는 구현 공백만이 아니다. 그중 일부는 C# 자체의 기능 부족으로 거슬러 올라간다. 문서에는 이렇게 소름 끼치게 웃긴 구절이 있다.
win32의 일부 매개변수는
[optional, out]또는[optional, in, out]이다. C#에는 이 개념을 표현하는 관용적인 방법이 없으므로, 이러한 매개변수를 가진 메서드에 대해서 CsWin32는 두 가지 버전을 생성한다. 하나는 모든ref또는out매개변수를 포함한 버전이고, 다른 하나는 그런 매개변수를 모두 생략한 버전이다.
C# 언어에는 Win32 API의 기초적인 매개변수 타입 을 지정하는 방법이 없다고? 기존에 지원되는 두 매개변수 타입의 선형 결합에 불과한데도? Microsoft가 C#을 통제한다는 점의 장점은, 그것이 Windows API를 위한 완벽한 프로그래밍 언어가 되도록 신중하게 다듬고 함께 진화시켰으리라는 데 있을 법하다. 하지만 실제로는 전혀 그렇지 않아 보인다.
사실 C#이 목표 플랫폼의 요구를 충족하지 못하는 것은 오래된 Win32 API와의 인터롭에서만이 아니다. WPF가 양방향 데이터 바인딩을 강조하며 2006년에 처음 등장했을 때, 모두는 UI에 바인딩 가능한 클래스를 만드는 데 필요한 상용구가 지속 불가능하다는 사실을 빠르게 깨달았다. 본질적으로 모든 프로퍼티는 getter/setter 쌍이 되어야 하고, setter에는 동일 값 보호 코드와 이벤트 발생 호출이 들어가야 한다. (그리고 C#에서 이벤트를 발생시키는 일은 형식 절차가 많다.) 사람들은 베이스 클래스에서 코드 생성기까지 여러 해결책으로 이를 덮어 보려 했다. 하지만 진짜 해결책은 JavaScript가 데코레이터와 프록시로 했듯, 언어에 무언가를 넣는 것이다.
그래서 내가 앱 작업을 시작했을 때, WPF가 출시된 지 20년이 지난 뒤에도 상용구가 거의 바뀌지 않았다는 사실에 경악했다. (유일한 개선은 C#이 어떤 기능을 얻어서 이벤트를 발생시킬 때 프로퍼티 이름을 생략할 수 있게 되었다는 점이다.) C# 언어 팀은 20년 동안 무엇을 하고 있었기에, 네이티브 관찰 가능 클래스를 만드는 일이 한 번도 우선순위가 되지 않았단 말인가?
솔직히 말해, 네이티브 Windows 앱 개발이라는 전체 프로젝트는 Microsoft에게 우선순위가 아닌 것처럼 느껴진다. 관련 이슈 트래커는 고통스러운 버그와 빈틈을 마주친 개발자들로 가득하지만, Microsoft 엔지니어들에게서 돌아오는 반응은 거의 없거나 아예 없다. Windows App SDK 변경 로그는 대부분 새 머신 러닝 API를 추가했다는 이야기다. 그리고 잘 알려져 있듯이 Visual Studio Code, Outlook, 심지어 시작 메뉴 자체에 이르기까지 많은 퍼스트파티 앱이 웹 기술로 작성되어 있다.
아마도 이것이 커뮤니티의 큰 부분이 Avalonia나 Uno Platform 같은 서드파티 UI 프레임워크에 투자하며 자신들의 길을 가기로 한 이유일 것이다. 그들의 랜딩 페이지와 GitHub 저장소를 둘러본 인상으로는, 이들은 더 잘 유지보수되고 있으며 WPF를 사랑했고 WinUI가 그만큼 강력하길 바랐던 사람들이 만들고 있다. 이들은 크로스플랫폼 개발도 받아들이고 있는데, 어떤 사용 사례에는 분명 중요하다.
하지만 그 지점에 이르면, 왜 Electron이 아니어야 하는가? 진지하게. C#과 XAML이 TypeScript/React/CSS에 비해 그렇게 놀라운 것도 아니다. 위의 목록에서 봤듯이, 기본을 넘는 대부분의 일을 하려면 어차피 Win32 인터롭으로 내려가야 한다. Tauri 같은 것을 사용하면 Chromium 바이너리 전체를 번들할 필요조차 없다. 시스템 웹뷰를 사용할 수 있다. 아이러니하게도 시스템 웹뷰는 매 4주마다 업데이트를 받는다(곧 2주가 될지도?). 반면 시스템 .NET은 영원히 4.8.1 버전에 묶여 있다!
아직 Microsoft가 이것을 바로잡을 가능성은 있다. Windows App SDK 접근법은 WinRT와 UWP로 길게 샜던 우회에 비하면 분명 개선처럼 보인다. 위에서 나는 패키징과 배포 주변의 손쉬운 개선점 몇 가지를 짚었고, 그들이 그것들을 실행해 주면 좋겠다. 그리고 최근의 Windows 품질에 집중하겠다는 발표에는 OS 전반에서 WinUI 3를 더 많이 사용하겠다는 문장이 포함되어 있는데, 이론적으로는 그것이 WinUI 자체의 개선으로 다시 흘러갈 수도 있다.
나는 숨을 참고 기다리지 않을 것이다. 내가 보기에는 대부분의 개발자도 마찬가지다. Hacker News의 논객들은 네이티브 앱의 죽음을 한탄하길 좋아한다. 하지만 Windows 앱 플랫폼이 이렇게 엉망이라면, 나는 언제든 웹 스택을 고르겠다. 그리고 OS 통합을 위해 필요한 Win32 API로 내려가는 다리로 Electron이나 Tauri를 쓰면 된다.