수정 이력(6건, 최종 수정 2026년 09월 03일)
이 글에 적용한 변경 사항의 기록입니다. 보관해 둔 수정 전 버전은 DOI가 부여된 고정 URL에서 읽을 수 있습니다.
- permalink·저자 표기·지식 맵 래퍼·깨진 내부 링크 등 CI가 지적한 표시용 수정을 반영했습니다. 본문의 기술적인 주장은 바꾸지 않았습니다.
- 일본어 원문의 완역으로 다시 번역했습니다. 기존 한국어판은 원문의 일부만 옮긴 축약본이라 절, 표, Mermaid 그림, 그림 설명, FAQ가 빠져 있었습니다. 일본어 원문에 맞춰 이들을 모두 복원했고, 기술적인 주장은 일본어판과 같습니다. 수정 전 버전 보기 (DOI: 10.5281/zenodo.21635198)
- 1 EXE화와 OS 의존의 구분, .NET 게시 형태의 차이, Native AOT의 대가, WinUI 3와 WebView2의 배포 제약 등을 그림으로도 따라갈 수 있도록 Mermaid 그림을 16점 추가했습니다(본문 500~750자당 1그림 규약에 맞춘 것입니다). 기존 4단계 그림에도 캡션을 달았습니다. 본문 문장은 바꾸지 않았습니다.
- 글 맨 앞에 「이 글의 지식 맵」 절을 추가했습니다. 본문에서 다루는 개념과 그 관계를 요약·그림·상세 페이지 링크로 정리한 것입니다. 본문의 주장은 바꾸지 않았습니다.
- 외부 리뷰(1283건) 대응으로 본문을 갱신했습니다. 개별 변경 내용은 아래 이력을 참고합니다.
- `/MT`와 `/MD` 대비표와 `dotnet publish`의 3패턴을 추가하고, 레벨 A~D를 계층도로 만들었습니다. 용어표를 추가하고, single-file의 시작 시 추출 동작, `Assembly.Location`이 빈 문자열이 되는 등의 API 비호환, Native AOT 제약(동적 로드 불가, C++/CLI 불가, 내장 COM 불가), Evergreen과 Fixed Version 대비를 추가했습니다.
- 최초 공개
이 글을 인용하기(DOI: 10.5281/zenodo.21635197)
이 글은 Zenodo에 보관되어 있습니다. 항상 최신 버전으로 연결되는 DOI와 지금 보고 있는 버전에 고정된 DOI를 아래에 함께 제시합니다.
Go Komura (2026). 「Windows 앱의 1파일 배포 - 싱글 바이너리와 OS 의존의 한계」. 합동회사 코무라소프트. https://doi.org/10.5281/zenodo.21635197 https://comcomponent.com/ko/blog/windows-single-binary-and-os-dependencies/
- DOI(최신 버전)
- 10.5281/zenodo.21635197
- DOI(이 버전)
- 10.5281/zenodo.22217470
이 글은 「Windows에서는 어디까지를 싱글 바이너리라고 불러도 되는가」라는 잡담에서 시작했습니다. 계기가 된 게시물은 아래입니다. 여기를 건너뛰어도 이후는 단독으로 읽을 수 있게 썼습니다.
Windows에서 「가능하면 1파일로 배포하고 싶다」는 꽤 흔한 요구입니다. 사내 도구, 장치 연동 도구, 감시 단말, 오프라인 환경, 인스톨러를 최대한 피하고 싶은 현장에서는 싱글 바이너리화가 매우 매력적입니다.
다만 이 이야기는 처음에 나누어 두지 않으면 대개 도중에 엇갈립니다. Windows에서 말하는 「싱글 바이너리로 만들고 싶다」에는 실제로 서로 다른 네 가지가 섞이기 쉽기 때문입니다.
- 배포물을 1개로 만들고 싶다
- .NET이나 Visual C++ 런타임을 사전 설치하지 않아도 되게 하고 싶다
- 인스톨러나 관리자 권한 없이 두기만 하면 동작하게 하고 싶다
- 대상 Windows의 차이에 의존하고 싶지 않다
이 네 가지는 같지 않습니다. 실무에서는 이렇게 생각하는 편이 가장 덜 어긋납니다.
배포물을 1 EXE로 모으는 일은 상당히 할 수 있다. 하지만 대상 Windows에 대한 의존을 제로로 만들 수는 없다.
이 글에서는 그 경계를 Windows 앱 실무 기준으로 정리합니다.
flowchart TB
accTitle: 1 EXE로 만들어도 OS 의존은 사라지지 않는다
accDescr: 싱글 바이너리로 만들고 싶다는 요구에는 배포 이야기와 의존 이야기가 섞이기 쉽고, 배포물을 1 EXE로 모으는 일은 상당히 할 수 있어도 대상 Windows에 대한 의존을 제로로 만들 수는 없음을 보이는 그림.
a0["「싱글 바이너리로 만들고 싶다」"] --> a1["배포 이야기: 1개로 만들고 싶다·두기만 하면 되게 하고 싶다"]
a0 --> a2["의존 이야기: 런타임 불필요·OS 비의존"]
a1 --> a3["1 EXE로 모으는 일은 상당히 가능"]
a2 --> a4["OS 의존을 제로로 만들 수는 없다"]
그림 1: 「1파일로 배포하고 싶다」에는 가능한 이야기와 불가능한 이야기가 섞여 있다.
1. 먼저 결론
처음에 결론만 정리하면 이렇습니다.
- 일반적인 데스크톱 EXE라면 상당히 높은 수준까지 single binary화할 수 있습니다
- 다만 1 EXE로 만들 수 있는 것과 대상 Windows에 의존하지 않는 것은 별개입니다
- Shell 확장, Windows 서비스, 드라이버, WebView2, WinUI 3의 일부는 파일 수보다 OS에 무엇을 등록하고 무엇을 전제로 하는가가 본론이 되기 쉽습니다
- 실무에서 가장 중요한 것은 single binary화하고 싶은지, 인스톨러를 없애고 싶은지, OS 의존을 줄이고 싶은지를 나누어 정하는 것입니다
바꿔 말하면 Windows에서의 구분은 이렇습니다.
- 배포물을 1개로 모은다: 상당히 가능
- 추가 런타임을 포함한다: 상당히 가능
- xcopy 배포로 맞춘다: 앱 종류에 따라 다름
- 대상 Windows 쪽 의존을 없앤다: 불가능
flowchart TB
accTitle: Windows에서의 구분
accDescr: 배포물을 1개로 모으는 일과 추가 런타임을 포함하는 일은 상당히 가능하고, xcopy 배포로 맞출 수 있는지는 앱 종류에 따라 다르며, 대상 Windows 쪽 의존을 없애는 일은 불가능하다는 구분을 보이는 그림.
b1["배포물을 1개로 모은다"] --> b2["상당히 가능"]
b3["추가 런타임 포함"] --> b2
b4["xcopy 배포로 맞춘다"] --> b5["앱 종류에 따라 다름"]
b6["OS 쪽 의존을 없앤다"] --> b7["불가능"]
그림 2: 할 수 있는 것·종류에 따라 다른 것·할 수 없는 것의 구분.
1.1 이 글에서 쓰는 용어
이후에서 여러 번 나오는 말만 먼저 정리합니다.
| 용어 | 읽기·풀어 쓰기 | 이 글에서의 의미 |
|---|---|---|
| UCRT | Universal C Runtime | Visual Studio 2015에서 C 런타임이 분할되어 생긴 표준 C 라이브러리 부분. Windows 10 이후에는 OS 구성 요소로 포함되어 있습니다 |
| VC++ 재배포 가능 패키지 | Visual C++ Redistributable | UCRT 이외의 Visual C++ 런타임 DLL을 대상 기기에 넣기 위한 인스톨러 |
| framework-dependent | 대상 환경의 .NET에 의존하는 배포 | 대상 기기에 .NET 런타임이 들어 있다는 전제의 배포 형태 |
| self-contained | .NET 런타임 포함 | 앱 쪽이 .NET 런타임 전체를 포함해 배포하는 형태 |
| single-file | 단일 파일 게시 | 배포물을 1개 EXE로 묶는 .NET 게시 옵션 |
| Native AOT | Native Ahead-Of-Time | 게시 시점에 IL을 네이티브 코드로 미리 컴파일하는 배포 형태. 실행 시 JIT를 쓰지 않습니다 |
| app-local 배포 | 앱 인접 배포 | DLL을 EXE와 같은 폴더에 두고 그 앱에서만 쓰는 배포 방식 |
| xcopy 배포 | 복사만 하는 배포 | 인스톨러도 관리자 권한도 쓰지 않고 폴더째 두면 동작하는 배포 방식 |
| SCM | Service Control Manager, 서비스 제어 관리자 | Windows 서비스의 등록·시작·중지를 관리하는 OS 메커니즘 |
| UAC | User Account Control, 사용자 계정 컨트롤 | 관리자 권한으로의 상승을 제어하는 Windows 보안 기구 |
| Shell 확장 | 셸 확장 | Explorer 같은 프로세스에 로드되어 동작하는 COM 구성 요소 |
| Evergreen | 상시 업데이트 방식 | WebView2 Runtime을 자동 업데이트되는 공유 런타임 하나에 맡기는 배포 모드 |
| Fixed Version | 버전 고정 방식 | 특정 버전의 WebView2 Runtime을 자신의 앱에 포함하는 배포 모드 |
| arch | architecture, CPU 아키텍처 | x86 / x64 / Arm64를 가리킵니다 |
이 글의 지식 맵
Windows 앱은 배포물을 1개의 EXE로 모으는 것 자체는 상당히 가능하며, .NET의 self-contained나 single-file·Native AOT, C/C++의 정적 링크(/MT)를 쓸 수 있지만, OS 버전이나 아키텍처, 시스템 DLL, 보안 모델에 대한 의존은 남아 대상 Windows에 대한 의존을 0으로 만들 수는 없습니다. self-contained와 framework-dependent는 배타적인 선택지이며, Native AOT는 사실상 single-file로 동작하는 대신 내장 COM 등을 쓸 수 없게 됩니다. WebView2는 Evergreen인지 Fixed Version인지로 런타임 제공 방식을 선택해야 하고, WinUI 3은 packaged(MSIX)인지 unpackaged인지에 따라 PublishSingleFile의 가능 여부가 달라집니다. Shell 확장이나 드라이버는 등록·서명 설계가 본론이 되므로, 무리하게 1EXE로 압축하기보다 app-local 배포 쪽이 유지보수하기 쉬운 경우가 많습니다.
flowchart LR
accTitle: Windows 앱 싱글 바이너리화 지식 맵
accDescr: 배포물을 1개로 하는 것과 OS 의존을 없애는 것이 서로 다른 축이라는 점, .NET과 C/C++ 각각의 단일 파일화 수단, WebView2나 WinUI 3 같은 호스트 통합이 배포 방식 자체를 제약함을 보여주는 그림
single_binary_packaging["싱글 바이너리화(단일 파일 배포)"]
windows_os_dependency["대상 Windows 의존성"]
self_contained_deployment["자체 포함 배포"]
framework_dependent_deployment["프레임워크 종속 배포(.NET)"]
native_aot["Native AOT"]
builtin_com_interop["built-in COM interop"]
single_file_publish["single-file 게시(.NET)"]
static_linking_crt["CRT 정적 링크(/MT)"]
dynamic_linking_crt["CRT의 동적 링크(/MD)"]
vc_redistributable["Visual C++ 재배포 가능 패키지"]
ucrt["UCRT(Universal C Runtime)"]
msix_packaging["MSIX 패키징(packaged)"]
winui3["WinUI 3 / Windows App SDK"]
webview2["Microsoft Edge WebView2"]
webview2_runtime["WebView2 Runtime"]
webview2_evergreen["Evergreen 배포 모드(WebView2)"]
webview2_fixed_version["Fixed Version 런타임"]
shell_extension["셸 확장(탐색기 확장)"]
com["COM(컴포넌트 오브젝트 모델)"]
driver_signing["드라이버 서명"]
driver_package["드라이버 패키지"]
app_local_deployment["app-local 배포(앱 인접 배포)"]
single_binary_packaging -->|"전제로 한다"| windows_os_dependency
self_contained_deployment -->|"양립하지 않는다"| framework_dependent_deployment
native_aot -->|"양립하지 않는다"| builtin_com_interop
single_file_publish -.->|"전제로 한다"| windows_os_dependency
native_aot -.->|"전제로 한다"| windows_os_dependency
static_linking_crt -->|"양립하지 않는다"| dynamic_linking_crt
dynamic_linking_crt -->|"전제로 한다"| vc_redistributable
static_linking_crt -->|"이용한다"| ucrt
dynamic_linking_crt -->|"이용한다"| ucrt
msix_packaging -.->|"양립하지 않는다"| single_file_publish
winui3 -.->|"이용한다"| msix_packaging
webview2 -->|"전제로 한다"| webview2_runtime
webview2_runtime -->|"에서 구성할 수 있다"| webview2_evergreen
webview2_runtime -->|"에서 구성할 수 있다"| webview2_fixed_version
webview2_evergreen -->|"양립하지 않는다"| webview2_fixed_version
shell_extension -->|"이용한다"| com
driver_signing -->|"전제로 한다"| driver_package
driver_package -->|"사용은 비권장"| single_binary_packaging
app_local_deployment -->|"권장되는 대응"| single_binary_packaging
self_contained_deployment -.->|"전제로 한다"| windows_os_dependency
single_binary_packaging -.->|"이용한다"| static_linking_crt
webview2 -.->|"전제로 한다"| windows_os_dependency
그림의 실선은 항상 성립하는 관계, 점선은 조건이 붙는 관계입니다(성립 조건은 상세 페이지의 관계별 설명에 적혀 있습니다). 관계 전체 목록(총 22건, 근거와 확신도 포함)과 주요 개념의 정의는 지식 맵 상세 페이지에 정리되어 있습니다(일본어). 데이터: JSON-LD / Turtle
2. 「싱글 바이너리」는 4단계로 나누어 생각한다
단계를 먼저 그림으로 보면 이렇습니다. 아래에서 위로 갈수록 어려워지고, 맨 위는 Windows에서는 도달할 수 없습니다.
flowchart BT
A["레벨 A: 배포물이 1개<br/>상당히 가능"]
B["레벨 B: 언어 런타임의 사전 설치가 불필요<br/>상당히 가능"]
C["레벨 C: 설치나 등록이 불필요<br/>앱 종류에 따라 다름"]
D["레벨 D: 대상 Windows에 의존하지 않음<br/>Windows에서는 도달할 수 없음"]
A --> B
B --> C
C --> D
그림 3: 싱글 바이너리의 4단계. 아래에서 위로 갈수록 어려워지고, 레벨 D에는 도달할 수 없다.
「싱글 바이너리로 만들고 싶다」는 이야기는, 이 4단계 중 어디를 가리키는지에 따라 필요한 작업이 전혀 달라집니다.
2.1 레벨 A: 배포물이 1개
가장 표면적인 것은 이것입니다.
- 메일로 1개 보낼 수 있다
- USB에 1개 두면 된다
- 대상 폴더에
app.exe만 둔다
이것은 겉보기의 배포 단위 이야기입니다. 실제로는 시작할 때 임시로 풀어 두어도, OS 쪽 DLL에 의존해도, 이 조건만이면 충족할 수 있습니다.
2.2 레벨 B: 언어 런타임의 사전 설치가 불필요
다음은 대상 머신에 미리 .NET 런타임이나 VC++ 재배포 가능 패키지를 넣지 않아도 동작하는 상태입니다.
- C/C++ 정적 링크
- .NET의 self-contained
- .NET의 single-file
- .NET Native AOT
이 레벨이 되면 「단독으로 들고 갈 수 있다」는 느낌이 훨씬 강해집니다.
2.3 레벨 C: 설치나 등록이 불필요
여기서부터 갑자기 어려워집니다.
단순한 EXE라면 두기만 해도 동작하는 경우가 있습니다. 하지만 아래와 같은 것은 다릅니다.
- Shell 확장
- Windows 서비스
- 사용자 지정 URL 스킴이나 파일 연결
- 드라이버
- Explorer나 Office처럼 다른 프로세스에 로드되는 구성 요소
이 영역은 파일을 두기만 해서는 끝나지 않습니다. OS 쪽 등록이나 호스트 쪽과의 연결이 필요합니다.
flowchart TB
accTitle: 두기만 해서는 끝나지 않는 영역
accDescr: Shell 확장, Windows 서비스, 파일 연결, 드라이버, 다른 프로세스에 로드되는 구성 요소는 파일을 두기만 해서는 끝나지 않고 OS 쪽 등록이나 호스트 쪽과의 연결이 필요함을 보이는 그림.
c1["Shell 확장·서비스"] --> c4["두기만 해서는 끝나지 않는다"]
c2["연결·드라이버"] --> c4
c3["다른 프로세스에 로드되는 것"] --> c4
c4 --> c5["OS 쪽 등록·호스트와의 연결이 필요하다"]
그림 4: 레벨 C에서 갑자기 어려워지는 이유는 OS 등록이 필요한 영역이기 때문이다.
2.4 레벨 D: 대상 Windows에 의존하지 않음
이것은 Windows에서는 불가능합니다.
Windows 앱은 최종적으로 Windows API, 로더, 보안 모델, 디바이스 스택 위에서 동작하기 때문입니다. single binary화할 수 있는 것은 앱 쪽 책임 범위까지입니다. OS 자체까지 가져가는 것은 아닙니다.
3. 상당히 1 EXE로 만들기 쉬운 영역
Windows에서도 비교적 1 EXE로 모으기 쉬운 앱은 있습니다.
- 단독으로 실행되는 데스크톱 도구
- EXE 자신이 UI와 처리를 가진 업무 앱
- 통신, 파일 처리, 로그 수집, 감시, 장치 제어 같은 도구
- Explorer나 Office의 호스트 통합이 필요 없는 것
- Web 런타임을 전제로 하지 않는 UI
이 유형이라면 앱 본체에 넣기 쉬운 것이 많습니다.
- 자체 코드
- 리소스
- 매니페스트
- 기본 설정
- 템플릿 데이터
- 일부 서드파티 라이브러리
- 언어 런타임 본체
나아가 DLL을 완전히 EXE 안에 넣지 않아도, EXE 옆에 DLL을 두는 app-local 배포는 Windows에서 흔히 유력합니다. 실무에서는
app.exe1개- 또는
app.exe+ 인접 DLL 몇 개 - 다만 인스톨러 불필요, 관리자 권한 불필요, xcopy로 배포할 수 있다
이 형태가 무리하게 1 EXE로 압축하는 것보다 유지보수하기 쉬운 경우가 드물지 않습니다.
flowchart TB
accTitle: app-local 배포라는 타협점
accDescr: DLL을 완전히 EXE에 넣지 않아도 EXE 옆에 DLL을 두고 인스톨러 불필요·관리자 권한 불필요로 xcopy 배포할 수 있는 형태라면, 무리하게 1 EXE로 압축하는 것보다 유지보수하기 쉬운 경우가 드물지 않음을 보이는 그림.
d1["app.exe+인접 DLL 몇 개"] --> d2["인스톨러 불필요·관리자 권한 불필요"]
d2 --> d3["xcopy로 배포할 수 있다"]
d3 -.-> d4["무리한 1 EXE 압축보다 유지보수하기 쉬운 경우도"]
그림 5: 1 EXE에 집착하지 않아도 app-local 배포로 목적을 채울 수 있는 경우가 많다.
4. 1 EXE여도 사라지지 않는 Windows 의존
「EXE 1개면 대상 Windows에 의존하지 않는다」고 생각하면 여기서 사고가 납니다. 실제로는 1 EXE로 만들어도 남는 의존이 있습니다.
4.1 OS 버전 의존
Windows API에는 각각 최저 지원 OS가 있습니다. x64 / Arm64 차이도 있습니다. 즉 단일 EXE로 만들어도
- Windows 10까지 동작하는지
- Windows 11 전제인지
- Windows Server에서도 돌릴지
- x86 / x64 / Arm64 중 무엇을 대상으로 할지
는 처음에 고정해야 합니다.
4.2 시스템 DLL 의존
1 EXE로 만들었다고 생각해도, 실행 시에는 당연히 OS가 제공하는 구성 요소를 씁니다.
kernel32.dlluser32.dlladvapi32.dll- COM 기반
- 서비스 제어 기반
이 부근은 Windows 쪽 책임 범위입니다.
4.3 보안 모델 의존
- UAC
- 파일 ACL
- 서비스 제어 관리자
- 레지스트리
- 드라이버 서명 정책
이런 것들은 앱이 단독으로 끌어안을 수 없습니다.
flowchart TB
accTitle: 1 EXE로 만들어도 남는 의존
accDescr: 1 EXE로 만들어도 Windows API의 최저 지원 OS나 아키텍처, kernel32.dll 같은 시스템 DLL이나 COM 기반, UAC나 ACL, 드라이버 서명 정책 같은 보안 모델에 대한 의존은 남음을 보이는 그림.
e0["1 EXE 앱"] --> e1["OS 버전과 arch"]
e0 --> e2["시스템 DLL·COM 기반"]
e0 --> e3["보안 모델"]
e1 --> e4["어느 것도 앱 쪽에서는 끌어안을 수 없다"]
e2 --> e4
e3 --> e4
그림 6: 배포물을 1개로 만들어도 Windows 쪽 책임 범위에 대한 의존은 남는다.
4.4 호스트나 런타임 의존
단독 실행 EXE가 아니라 어떤 호스트에 올라가는 설계면 의존이 한 번에 늘어납니다.
- WebView2를 쓴다: WebView2 Runtime이 필요하다
- WinUI 3 / Windows App SDK를 쓴다: 배포 모드 정리가 필요하다
- Shell 확장을 만든다: Explorer 쪽 등록이 필요하다
즉 UI나 통합 선택이 그대로 배포의 어려움이 되는 경우가 많습니다.
flowchart TB
accTitle: UI와 통합 선택이 배포의 어려움이 된다
accDescr: WebView2를 쓰면 WebView2 Runtime이 필요하고, WinUI 3를 쓰면 배포 모드 정리가 필요하며, Shell 확장을 만들면 Explorer 쪽 등록이 필요하다는 식으로, 호스트나 런타임 선택이 그대로 배포의 어려움이 됨을 보이는 그림.
f1["WebView2를 쓴다"] --> f2["Runtime이 필요하다"]
f3["WinUI 3를 쓴다"] --> f4["배포 모드 정리가 필요하다"]
f5["Shell 확장을 만든다"] --> f6["Explorer 등록이 필요하다"]
그림 7: 단독 실행 EXE에서 멀어질수록 의존이 한 번에 늘어난다.
5. 기술별로 보는 현실적인 타협점
5.1 네이티브 C/C++
네이티브 C/C++는 single binary화의 자유도가 높은 쪽입니다. 정적 링크를 고를 여지가 있고, 단독 실행 EXE라면 상당히 모으기 쉽습니다.
다만 전부를 1파일에 밀어 넣는 일보다
- UCRT나 VC++ 런타임을 어떻게 할지
- 서드파티 DLL을 app-local에 둘지
- 대상 CPU / OS를 어디까지 줄일지
가 실무에서는 더 중요합니다.
시작점: /MT와 /MD의 차이
MSVC에서 「런타임을 포함할지」를 실질적으로 정하는 것은 이 컴파일러 옵션입니다.
| 옵션 | 링크되는 것 | 대상 기기에 필요한 것 |
|---|---|---|
/MD |
ucrt.lib와 vcruntime.lib라는 DLL용 가져오기 라이브러리. 실행 시에는 ucrtbase.dll과 vcruntime<버전>.dll을 쓴다 |
UCRT와 VC++ 런타임 |
/MT |
libucrt.lib, libvcruntime.lib, libcmt.lib를 정적 링크 |
추가 런타임 없음 |
최소로 시험해 보려면 Developer Command Prompt에서 이것만으로 충분합니다.
cl /std:c++17 /EHsc /O2 /MT app.cpp /Fe:app.exe
여기서 주의할 점이 세 가지 있습니다.
- UCRT는 Visual Studio 2015에서 C 런타임이 분할될 때 Windows의 구성 요소가 되었고, Windows 10 이후에는 OS의 일부로 포함되어 있습니다. 그보다 앞선 Windows를 대상으로 하면
vcredist로 재배포해야 합니다. - DLL 쪽을
/MT로 만드는 것은 권장되지 않습니다. 정적 링크한 CRT는 상태가 그 DLL 안에만 남으므로, EXE와 DLL에서 메모리 할당이나 로케일,_set_se_translator의 효과가 어긋납니다. 「EXE는/MT, 포함 DLL도/MT」로 대충 맞추면, 경계를 넘는 할당·해제에서 사고가 납니다. /clr과/MT는 함께 쓸 수 없습니다. C++/CLI가 섞이면/MD쪽입니다.
flowchart TB
accTitle: 정적 링크의 세 가지 주의
accDescr: UCRT는 Windows 10 이후 OS에 포함되지만 그보다 앞선 버전은 vcredist로 재배포해야 하고, DLL 쪽을 /MT로 만들면 CRT 상태가 DLL 안에 갇혀 경계를 넘는 할당·해제에서 사고가 나며, /clr과 /MT는 함께 쓸 수 없음을 보이는 그림.
g0["/MT로 정적 링크"] --> g1["오래된 Windows는 vcredist가 필요하다"]
g0 --> g2["DLL 쪽 /MT는 비권장"]
g0 --> g3["/clr과는 함께 쓸 수 없다"]
g2 -.-> g4["경계를 넘는 할당·해제에서 사고가 난다"]
그림 8: /MT는 강력하지만 UCRT 전제·DLL 경계·C++/CLI 세 점에 주의가 필요하다.
5.2 .NET
.NET은 single-file, self-contained, Native AOT가 있어서 겉보기의 배포 단위를 상당히 작게 만들 수 있습니다.
다만 구분은 필요합니다.
- framework-dependent: 대상 환경의 .NET에 의존
- self-contained: .NET 런타임을 포함
- single-file: 배포물을 하나로 모은다
- Native AOT: 시작 시 의존을 더 줄이지만 기능 제약도 있다
「single-file이니까 OS 의존이 줄어든다」는 뜻이 아닙니다. 줄어드는 것은 주로 앱 배포물의 묶음입니다.
flowchart TB
accTitle: single-file로 줄어드는 것은 배포물의 묶음
accDescr: framework-dependent는 대상 환경의 .NET에 의존하고, self-contained는 런타임을 포함하며, single-file은 배포물을 하나로 모을 뿐이라서 single-file이라고 OS 의존이 줄어드는 것은 아님을 보이는 그림.
h1["framework-dependent"] --> h2["대상 환경의 .NET에 의존"]
h3["self-contained"] --> h4[".NET 런타임을 포함"]
h5["single-file"] --> h6["배포물을 하나로 모은다"]
h6 -.-> h7["OS 의존이 줄어드는 것은 아니다"]
그림 9: .NET 게시 형태마다 줄어드는 것과 남는 것이 다르다.
시작점: dotnet publish의 3패턴
먼저 돌려 보고 차이를 보려면 이 세 가지면 충분합니다.
rem 1. framework-dependent + single-file: 대상 기기에 .NET 런타임이 필요하다
dotnet publish -c Release -r win-x64 --self-contained false -p:PublishSingleFile=true
rem 2. self-contained + single-file: .NET 런타임을 포함
dotnet publish -c Release -r win-x64 --self-contained true -p:PublishSingleFile=true
rem 3. Native AOT: csproj에 PublishAot를 쓴 다음 게시한다
dotnet publish -c Release -r win-x64
PublishSingleFile과 PublishAot는 명령줄이 아니라 csproj에 쓰는 쪽이 공식 권장입니다. 빌드 중 호환성 분석이 켜져서, 게시한 뒤에야 알아채는 사고가 줄어들기 때문입니다.
<PropertyGroup>
<PublishSingleFile>true</PublishSingleFile>
<PublishAot>true</PublishAot>
</PropertyGroup>
참고로 RuntimeIdentifier를 지정하면 SelfContained는 기본값이 true가 됩니다. framework-dependent로 하려면 --self-contained false를 명시해야 합니다. Native AOT를 Windows에서 게시하려면 Visual Studio의 C++를 사용한 데스크톱 개발 워크로드가 필요합니다.
트레이드오프: 「1개로 만드는」 일은 공짜가 아니다
여기는 「할 수 있다」만 적으면 사고가 나므로, 대가도 함께 적습니다.
single-file의 시작 시 추출
- 기본값에서는 번들되는 것은 managed DLL뿐입니다. 시작 시 메모리에 로드되고, 폴더에는 풀리지 않습니다. 런타임 본체의 네이티브 바이너리는 별도 파일로 남습니다.
- 네이티브 바이너리까지 1개에 넣으려면
IncludeNativeLibrariesForSelfExtract, 전부를 실행 전에 풀려면IncludeAllContentForSelfExtract를 씁니다. 둘 다 「푼 뒤에 시작하는」 동작이 되며, Windows에서는%TEMP%\.net아래로 풀립니다.DOTNET_BUNDLE_EXTRACT_BASE_DIR로 위치를 바꿀 수 있지만, 권한이 다른 사용자나 서비스가 쓸 수 있는 곳으로 두면 안 됩니다. EnableCompressionInSingleFile을 켜면 EXE는 상당히 작아지지만, 시작 시 메모리에서 압축을 푸는 만큼 시작이 느려집니다. 공식도 「쓰기 전에 크기와 시작 비용 둘 다 측정하라」는 식의 표현을 합니다. 영향은 앱에 따라 크게 달라집니다.
single-file의 API 비호환
배포물을 1개로 만들면, 파일 경로를 전제로 한 코드가 조용히 깨집니다.
| API | single-file에서의 동작 |
|---|---|
Assembly.Location |
빈 문자열을 반환 |
Assembly.CodeBase |
PlatformNotSupportedException |
Assembly.GetFile |
IOException |
Module.Name |
<Unknown>이라는 문자열을 반환 |
EXE 옆 파일을 다룰 때는 AppContext.BaseDirectory, 실행 파일 경로가 필요하면 Environment.ProcessPath로 맞춥니다. 서드파티 라이브러리가 내부에서 이들을 쓰는 경우도 있으므로, single-file화했다면 한 번은 실제 기기에서 실행 확인이 필요합니다.
flowchart TB
accTitle: 경로 전제 코드가 조용히 깨진다
accDescr: 배포물을 1개로 만들면 Assembly.Location이 빈 문자열을 반환하는 등 파일 경로 전제 API의 동작이 바뀌므로, EXE 옆은 AppContext.BaseDirectory, 실행 파일 경로는 Environment.ProcessPath로 맞추고 한 번은 실제 기기에서 실행 확인함을 보이는 그림.
i1["single-file화한다"] --> i2["경로 전제 API의 동작이 바뀐다"]
i2 --> i3["옆 파일은 AppContext.BaseDirectory"]
i2 --> i4["실행 파일은 Environment.ProcessPath"]
i3 --> i5["한 번은 실제 기기에서 실행 확인한다"]
i4 --> i5
그림 10: single-file화했다면 경로 얻는 방법을 바꾸고 실제 기기에서 확인한다.
Native AOT의 기능 제약
「시작이 빠르고 런타임이 필요 없다」는 대신, 다음을 쓸 수 없게 됩니다.
Assembly.LoadFile같은 동적 로드System.Reflection.Emit같은 실행 시 코드 생성- C++/CLI
- Windows에서는 내장 COM
- 트리밍이 필수이므로 트리밍 제약도 그대로 따라옵니다
- 실질적으로 single-file이므로 위의 API 비호환도 그대로 따라옵니다
System.Linq.Expressions는 항상 인터프리터 동작이 되어, 실행 시 생성 코드보다 느려집니다
대상 플랫폼도 고정입니다. .NET 8에서 Windows는 x64와 Arm64이고, .NET 9 이후에서 x86이 추가되었습니다. 「AnyCPU로 1개」라는 발상은 처음부터 성립하지 않습니다.
flowchart TB
accTitle: Native AOT의 대가
accDescr: Native AOT는 시작 시 의존을 줄일 수 있는 대신 동적 로드나 실행 시 코드 생성, C++/CLI, Windows 내장 COM을 쓸 수 없고, 트리밍과 single-file 제약도 따라오며, 대상 플랫폼도 고정됨을 보이는 그림.
j0["Native AOT"] --> j1["시작 시 의존을 줄일 수 있다"]
j0 --> j2["동적 로드·실행 시 생성이 불가"]
j0 --> j3["C++/CLI·내장 COM이 불가"]
j2 -.-> j4["트리밍과 single-file 제약도 따라온다"]
j3 -.-> j5["대상 OS와 arch는 고정"]
그림 11: Native AOT의 이점은 쓸 수 없게 되는 기능과 세트로 본다.
5.3 WebView2
WebView2를 채택하면 single binary의 어려움이 완전히 달라집니다. 여기서의 본론은 EXE 개수가 아니라 WebView2 Runtime을 어떻게 다룰지입니다.
「1 EXE로 만들 수 있는가」보다 먼저 생각할 질문이 있습니다.
- Runtime을 기존 환경 전제로 할지
- Evergreen을 쓸지
- Fixed Version을 포함할지
- 오프라인 배포에서 어디까지 책임을 질지
Evergreen과 Fixed Version의 차이는 대략 이렇습니다.
| Evergreen | Fixed Version | |
|---|---|---|
| 업데이트하는 주체 | Microsoft. 자동 업데이트된다 | 자신. 앱 업데이트에 맞춰 교체한다 |
| 단말 위 실체 | 모든 앱이 1개를 공유 | 앱마다 포함 |
| 배포 크기 | 거의 늘지 않는다 | 250 MB를 넘는다 |
| 넣는 방법 | Bootstrapper는 약 2 MB이며 필요한 만큼을 다운로드한다. 오프라인이면 Standalone Installer를 포함한다 | 풀어 둔 바이너리 일체를 앱과 함께 배포한다 |
| 기존 제약 | 단말에 들어 있는지 시작 전에 확인할 필요가 있다 | 네트워크 경로나 UNC 경로에서는 실행할 수 없다 |
Evergreen Runtime은 Windows 11에는 OS의 일부로 미리 설치되어 있습니다. 다만 Windows 10 쪽에는 들어 있지 않은 단말도 남으므로, Microsoft 자신도 「Evergreen을 골라도 Runtime은 배포하는 편이 좋다」는 식의 표현을 합니다. 즉 앱 쪽에서 유무를 확인하고 부족하면 넣는 절차는 어느 쪽이든 필요합니다. Fixed Version은 「업데이트 시점을 스스로 쥘 수 있다」는 대신 배포물이 수백 MB 늘어납니다. 1 EXE 이야기와는 별개로, 여기를 먼저 정해야 합니다.
flowchart TB
accTitle: WebView2는 배포 방식을 먼저 정한다
accDescr: Evergreen은 Microsoft가 자동 업데이트하는 공유 1개이지만 단말에 없는 경우도 있고, Fixed Version은 업데이트를 스스로 쥘 수 있는 대신 배포물이 수백 MB 늘어나므로, 어느 쪽을 골라도 앱 쪽에서 유무를 확인하고 부족하면 넣는 절차가 필요함을 보이는 그림.
k0["WebView2를 채택"] --> k1["Evergreen: 공유 1개를 자동 업데이트"]
k0 --> k2["Fixed Version: 스스로 포함·업데이트"]
k1 --> k3["유무를 확인하고 부족하면 넣는다"]
k2 -.-> k4["배포물이 수백 MB 늘어난다"]
그림 12: WebView2의 본론은 EXE 개수가 아니라 Runtime 처리에 있다.
5.4 WinUI 3 / Windows App SDK
WinUI 3도 채택한 시점에 배포 요건이 바뀝니다. UI 기술 선택이 그대로 배포 방식 선택이 됩니다.
구체적으로 정할 것은 두 축입니다.
- packaging: packaged(MSIX)인지, 외부 위치를 가리키는 packaged인지, unpackaged인지
- runtime: framework-dependent인지 self-contained인지
그리고 single binary 관점에서 효력이 있는 것이 이 조합 제약입니다.
PublishSingleFile을 쓸 수 있는 것은 unpackaged이면서 self-contained인 WinUI 3 앱뿐이며, Windows App SDK 1.5 이후가 필요합니다.- packaged 앱, 그리고 외부 위치를 가리키는 packaged 앱에서는
PublishSingleFile을 쓸 수 없습니다. - unpackaged로 하면 package identity를 잃으므로, 알림, 백그라운드 작업, 파일 연결, 컨텍스트 메뉴 확장처럼 package identity를 전제로 한 Windows 기능을 쓸 수 없게 됩니다.
즉 WinUI 3에서는 「1 EXE로 만든다」와 「Windows 확장 기능을 쓴다」가 정면에서 충돌합니다. single binary를 최우선으로 한다면 먼저 UI 기술 전제를 다시 보는 편이 빠른 경우가 많습니다.
flowchart TB
accTitle: 1 EXE와 package identity의 충돌
accDescr: WinUI 3에서 PublishSingleFile을 쓸 수 있는 것은 unpackaged이면서 self-contained인 구성뿐이고, unpackaged로 하면 package identity를 잃어 알림이나 파일 연결 같은 기능을 쓸 수 없게 되므로, 1 EXE와 Windows 확장 기능이 정면에서 충돌함을 보이는 그림.
m1["PublishSingleFile을 쓰고 싶다"] --> m2["unpackaged+self-contained만"]
m2 --> m3["package identity를 잃는다"]
m3 --> m4["알림·연결 등을 쓸 수 없다"]
m4 -.-> m5["1 EXE와 확장 기능이 정면에서 충돌한다"]
그림 13: WinUI 3에서는 1 EXE를 고르는 일이 package identity를 버리는 일이 된다.
6. 본질적으로 「등록·의존」이 필요한 영역
6.1 Shell 확장
Explorer에 로드되는 Shell 확장은 단순한 「두기만 하는 EXE」와는 다른 물건입니다. 여기서는 파일 수보다 Explorer에 어떻게 등록하는가가 본론입니다.
6.2 Windows 서비스
서비스 본체의 exe 자체는 1파일로 만들 수 있어도, 배포는 별개 문제입니다.
- SCM 등록
- 권한
- 시작 계정
- 복구 설정
을 생각해야 합니다. 즉 서비스는 「1 EXE로 만든다」보다 「어떻게 설치하는가」를 파고드는 영역입니다.
6.3 드라이버
드라이버는 더 분명합니다. INF, 서명, 설치 절차까지 포함해야 성립하므로, 처음부터 single binary 기준으로 보기 어렵습니다.
flowchart TB
accTitle: 등록과 서명이 본론인 영역
accDescr: Shell 확장은 Explorer 등록, Windows 서비스는 SCM 등록이나 권한·시작 계정, 드라이버는 INF와 서명까지 포함해야 성립하므로, 파일 수보다 등록·의존 설계가 본론이 됨을 보이는 그림.
n1["Shell 확장"] --> n2["Explorer 등록"]
n3["서비스"] --> n4["SCM 등록·권한·계정"]
n5["드라이버"] --> n6["INF와 서명"]
n4 -.-> n7["파일 수보다 등록 설계가 본론"]
그림 14: 이 세 영역에서는 「1 EXE로 만든다」보다 「어떻게 등록하는가」를 파고든다.
7. 실무에서의 판단표
대략 판단하려면 이 표가 쓰기 쉽습니다.
| 만들고 싶은 것 | 1 EXE 현실도 | 먼저 생각할 것 |
|---|---|---|
| 단독 실행 Win32 / C++ 도구 | 높음 | 정적 링크, 대상 OS / arch |
| 단독 실행 WinForms / WPF 도구 | 높음 | self-contained, single-file, Native AOT 적성 |
| WinUI 3 / Windows App SDK 앱 | 중간 | 배포 모드, 추가 의존 |
| WebView2 기반 데스크톱 UI | 낮음에서 중간 | Runtime 배포 방식 |
| Explorer 오른쪽 클릭 확장이나 미리 보기 | 낮음 | COM / 레지스트리 등록 |
| Windows 서비스 | 중간 | SCM 등록, 권한, 업데이트 절차 |
| 드라이버 포함 앱 | 낮음 | INF, 서명, 설치 |
이 표에서 가장 중요한 것은 「바이너리 개수」와 「배포 책임 범위」는 별개라는 점을 아는 것입니다.
8. 배포 설계에서 먼저 정해야 할 것
single binary화를 성공시키고 싶다면, 구현보다 먼저 정해 두고 싶은 것이 있습니다.
8.1 무엇을 1개로 만들고 싶은지를 정한다
- 배포물을 1개로 만들고 싶은지
- 런타임 사전 설치를 없애고 싶은지
- 인스톨러를 없애고 싶은지
- 오프라인 업데이트를 쉽게 하고 싶은지
이 답에 따라 고르는 기술이 달라집니다.
8.2 최저 지원 Windows와 arch를 처음에 고정한다
single-file도 Native AOT도 기본적으로 OS / architecture specific입니다. 여기를 모호하게 둔 채 「어쨌든 1파일로」 진행하면, 마지막에 API 부족이나 runtime 불일치로 막힙니다.
8.3 「포함할 것」과 「Windows에 맡길 것」을 명문화한다
실무에서는 이 표를 적어 두는 것만으로 사고가 상당히 줄어듭니다.
- 앱에 포함할 것
- 본체 exe
- 자체 DLL
- 설정 템플릿
- self-contained runtime
- Windows에 맡길 것
- 시스템 DLL
- OS API
- SCM / 레지스트리 / Explorer
- 드라이버 기반
- 별도로 전제할 것
- WebView2 Runtime
- VC++ Redistributable
- Office / Excel
- 전용 드라이버
flowchart TB
accTitle: 책임 범위의 3분류를 명문화한다
accDescr: 앱에 포함할 것, Windows에 맡길 것, 별도로 전제할 것의 3분류를 적어 두는 것만으로 배포 사고가 상당히 줄어듦을 보이는 그림.
p0["배포물의 책임 범위"] --> p1["앱에 포함할 것"]
p0 --> p2["Windows에 맡길 것"]
p0 --> p3["별도로 전제할 것"]
p3 -.-> p4["적어 두는 것만으로 사고가 줄어든다"]
그림 15: 포함·OS 위임·별도 전제의 3분류를 먼저 명문화해 둔다.
8.4 single binary를 우선한다면 호스트 통합을 줄인다
이것은 상당히 효과가 있습니다.
- Shell 확장을 그만두고 일반 EXE로 만든다
- 서비스화하지 않고 작업 스케줄러나 명시적 실행으로 끝낸다
- WebView2가 아니라 네이티브 UI를 쓴다
- COM은 자기 프로세스 안에서 닫는다
요컨대 OS에 「로드시킨다」「등록한다」는 설계를 줄일수록 single binary에 가까워집니다.
flowchart TB
accTitle: 호스트 통합을 줄일수록 가까워진다
accDescr: Shell 확장을 그만두고 일반 EXE로 만들고, 서비스화하지 않고 작업 스케줄러나 명시적 실행으로 끝내며, WebView2가 아니라 네이티브 UI를 쓰는 등, OS에 로드시키고 등록하는 설계를 줄일수록 single binary에 가까워짐을 보이는 그림.
q1["Shell 확장을 그만두고 일반 EXE로"] --> q4["OS에 등록하는 설계를 줄인다"]
q2["서비스화하지 않고 명시적 실행으로"] --> q4
q3["WebView2가 아니라 네이티브 UI로"] --> q4
q4 --> q5["single binary에 가까워진다"]
그림 16: single binary를 우선한다면 호스트 통합 자체를 줄인다.
9. 정리
Windows에서의 single binary화는 상당한 수준까지 가능합니다. 다만 도착점은 이 한 문장입니다.
앱을 1 EXE로 만드는 일은 할 수 있다. 하지만 그 앱이 의존하는 Windows까지 1 EXE로 만드는 일은 할 수 없다.
특히 기억해 두고 싶은 점을 다섯 가지 듭니다.
- 단독으로 실행되는 일반 EXE라면 상당한 수준까지 1파일 배포로 모을 수 있다
- C/C++ 정적 링크, .NET single-file, Native AOT는 유력하다
- 다만 OS 버전, arch, 시스템 DLL, 보안 모델에 대한 의존은 사라지지 않는다
- Shell 확장, 서비스, 드라이버, WebView2, WinUI 3의 일부는 OS 등록이나 추가 런타임 이야기가 본체가 된다
- single binary의 성패는 「무엇을 1개로 만들고 싶은가」를 처음에 나누는 일로 결정된다
single binary를 강하게 우선한다면, 기술 선정 시점에 OS와의 결합도를 낮추는 방향으로 설계하는 편이 훨씬 성공하기 쉽습니다.
flowchart TB
accTitle: 성공의 열쇠는 OS와의 결합도
accDescr: 앱을 1 EXE로 만드는 일은 할 수 있어도 그 앱이 의존하는 Windows까지 1 EXE로 만드는 일은 할 수 없으므로, single binary를 강하게 우선한다면 기술 선정 시점에 OS와의 결합도를 낮추는 방향으로 설계하는 편이 성공하기 쉬움을 보이는 그림.
r1["앱을 1 EXE로 만든다"] --> r2["할 수 있다"]
r3["의존하는 Windows까지 1 EXE로"] --> r4["할 수 없다"]
r4 -.-> r5["그래서 기술 선정에서 OS와의 결합도를 낮춘다"]
그림 17: 1 EXE화의 성패는 처음의 OS와의 결합도 설계로 결정된다.
10. 참고 자료
- Microsoft Learn, Create a single file for application deployment
- Microsoft Learn, Native AOT deployment overview
- Microsoft Learn, C runtime (CRT) and C++ standard library (STL) lib files
- Microsoft Learn, Dynamic-link library search order
- Microsoft Learn, Targeting your application for Windows
- Microsoft Learn, Creating Registration-Free COM Objects
- Microsoft Learn, Registering Shell Extension Handlers
- Microsoft Learn, CreateServiceW function
- Microsoft Learn, Overview of INF Files
- Microsoft Learn, Windows driver signing tutorial
- Microsoft Learn, Distribute your app and the WebView2 Runtime
- Microsoft Learn, Windows App SDK deployment guide for self-contained apps
- Microsoft Learn, Package and deploy Windows apps overview
- Microsoft Learn, Packaging overview - Windows apps
- Microsoft Learn, Evergreen vs. fixed version of the WebView2 Runtime
관련 기사
같은 태그를 공유하는 최신 기사입니다. 더 가까운 주제로 지식을 넓힐 수 있습니다.
Windows 앱 외주·수탁 개발을 의뢰하기 전에 정리할 점
Windows 앱 외주·수탁 개발을 의뢰하기 전에, 기존 소프트웨어 수정, 장치 연동, COM/ActiveX, 배포·업데이트, 유지보수를 정리하는 포인트를 설명합니다.
IE모드의 다음은 WebView2로 충분한가 ── ActiveX가 동작하지 않는 제약과 현실적인 전환 설계
WebView2의 기본 구조, Evergreen과 Fixed Version의 배포 전략, 사용자 데이터 폴더의 함정, 네이티브와 Web의 연동 방법, 그리고 「ActiveX는 동작하지 않는다」는 제약을 전제로 한 IE모드 의존 시스템에서의 현실적...
ClickOnce란 무엇인가 - 동작 방식, 업데이트, 적합한 경우와 그렇지 않은 경우를 실무 관점에서 정리
.NET의 Windows 데스크톱 앱 배포에 쓰이는 ClickOnce에 대해, 매니페스트, 업데이트, 캐시, 서명, 적합한 경우와 그렇지 않은 경우를 Mermaid 다이어그램과 함께 정리합니다.
Windows 앱에서 자식 프로세스를 안전하게 다루는 체크리스트
Windows 앱에서 자식 프로세스를 안전하게 다루려면, 시작 API보다 프로세스 트리의 소유권과 종료 절차 설계가 중요합니다. Job Object, 종료 전파, 표준 입출력, watchdog을 정리합니다.
Win32 스레드 풀 API ── CreateThreadpoolWork로 「스레드를 만들지 않는」 병렬 처리
네이티브 코드에서 CreateThread를 마구 늘리고 있지는 않은가요. Vista에서 개편된 Win32 스레드 풀 API의 work·timer·wait·io 네 객체, 클린업 그룹, 콜백에서 해서는 안 되는 일까지 1차 정보를 바탕으로 설명합니다.
관련 토픽
이 기사와 가까운 토픽 페이지입니다. 기사를 출발점 삼아 관련 서비스와 다른 기사로 이어집니다.
Windows 기술 토픽
Windows 개발, 장애 조사, 기존 자산 활용에 관한 KomuraSoft LLC 기사를 모은 토픽 허브입니다.
이 주제와 연결되는 서비스
이 기사는 다음 서비스 페이지로 이어집니다. 가까운 입구부터 확인해 주세요.
Windows 앱 개발
Windows 앱 배포에서는 single-file화, 런타임 포함, WebView2나 WinUI 채택 판단, 서비스화 여부까지 포함해 설계하는 편이 재작업을 줄입니다.
기술 상담 & 설계 리뷰
『1 EXE로 만들고 싶다』는 요구는 배포 단위, OS 의존, 등록 필요 여부, 업데이트 책임을 나눠 정리하면 판단하기 쉬워집니다.
자주 묻는 질문
이 기사 주제에 대해 상담 시 자주 나오는 질문을 모았습니다.
- Windows 앱을 1개의 EXE 파일만으로 배포할 수 있나요?
- 단독으로 실행되는 데스크톱 도구라면 상당히 높은 수준까지 가능합니다. C/C++ 정적 링크, .NET의 self-contained나 single-file, Native AOT를 쓰면 배포물을 1개 EXE로 모을 수 있습니다. 다만 1 EXE로 만들 수 있는 것과 대상 Windows에 의존하지 않는 것은 별개이며, OS 버전·아키텍처·시스템 DLL·보안 모델에 대한 의존은 사라지지 않습니다.
- .NET의 single-file로 하면 OS 의존이 없어지나요?
- 그렇지 않습니다. single-file로 줄어드는 것은 주로 앱 배포물의 묶음이지, OS 의존이 줄어드는 것은 아닙니다. framework-dependent는 대상 환경의 .NET에 의존하고, self-contained는 .NET 런타임을 포함하며, Native AOT는 시작 시 의존을 더 줄일 수 있지만 기능 제약도 있습니다. single-file도 Native AOT도 기본적으로 OS·아키텍처 고유이므로, 최저 지원 Windows와 arch는 처음에 고정해야 합니다.
- 1 EXE로 만들기 어려운 앱은 어떤 것인가요?
- Shell 확장, Windows 서비스, 드라이버, WebView2 기반 UI, WinUI 3의 일부입니다. 이들은 파일 수보다 OS 등록이나 추가 런타임 처리가 본론이 됩니다. 예를 들어 Shell 확장은 Explorer 등록이 필요하고, 서비스는 SCM 등록·권한·시작 계정 설계가 필요하며, 드라이버는 INF와 서명까지 포함해야 성립하므로 두기만 하는 배포는 성립하지 않습니다. WebView2는 WebView2 Runtime 배포 방식을 먼저 정해야 합니다.
- 무리하게 1 EXE에 넣는 것보다 나은 배포 방법이 있나요?
- EXE 옆에 DLL을 두는 app-local 배포가 유력합니다. app.exe에 인접 DLL 몇 개라는 형태라도, 인스톨러 불필요·관리자 권한 불필요이며 xcopy 배포가 가능하다면, 무리하게 1 EXE로 압축하는 것보다 유지보수가 쉬운 경우가 드물지 않습니다. 중요한 것은 배포물을 1개로 만들고 싶은지, 런타임 사전 설치를 없애고 싶은지, 인스톨러를 없애고 싶은지를 처음에 나누는 것입니다.