Skip to content

Python 데스크톱 도구를 MSIX로 패키징하여 Microsoft Store에 게시하기

2026-04-27

Tags: 튜토리얼 · Python · Windows · MSIX · 패키징

들어가며

이전 글에서 RDP Heartbeat 개발 동기와 과정을 정리했습니다. 그 글 마지막에 이런 말을 남겼습니다:

시간이 나면 Python에서 MSIX까지의 패키징 전 과정을 자세히 공유하는 글을 쓰겠습니다.

드디어 그 글이 완성됐습니다.

솔직히 말하면, Python으로 만든 데스크톱 도구를 Microsoft Store에 올리는 건 C# / WinUI 3를 쓰는 것보다 훨씬 번거롭습니다. 후자는 Visual Studio에서 몇 번의 클릭으로 거의 끝나지만, 저처럼 Python을 사용한다면 이런 길을 걸어야 합니다:

Python 소스 → PyInstaller → EXE → Inno Setup → 설치 프로그램 → MSIX Packaging Tool → MSIX → Store

각 단계마다 함정이 있지만, 각 단계마다 해결책도 있습니다. 이 글은 그 지도와 공략법입니다.

이 글은 RDP Heartbeat 프로젝트를 예시로 사용하지만, 방법과 접근 방식은 Python + Tkinter(또는 PyQt, wxPython 등)로 작성된 모든 Windows 도구에 적용됩니다.

참고로: 이론적으로 PyInstaller가 생성한 exe를 MSIX Packaging Tool로 직접 변환하고 Inno Setup을 건너뛸 수도 있습니다. Inno Setup을 먼저 거친 이유는 주로 자체 배포를 편하게 하기 위해서입니다(예: GitHub Release에 Setup.exe를 올려 사람들이 다운로드할 수 있게 하는 등). 이론적으로 최적의 해결책은 아닐 수 있지만, 이 길을 실제로 성공시켰기 때문에 공유합니다.

참고 소스 코드: 이 글에서 다루는 전체 프로젝트 코드는 [email protected]에서 확인할 수 있습니다. build_release.py, setup.iss 및 모든 패키징 설정이 포함되어 있습니다.

전체 흐름: 4단계 파이프라인

시작하기 전에 전체 체인을 펼쳐봅시다:

  • PyInstaller: .py.exe
  • Inno Setup: .exe → 설치 프로그램
  • MSIX Packaging Tool: 설치 프로그램 → .msix
  • Microsoft Store: 게시

왜 이렇게 복잡할까요? Microsoft Store가 최종적으로 필요로 하는 것은 MSIX 형식의 패키지이고, MSIX Packaging Tool의 작동 방식은 **이미 정상적으로 설치되고 실행되는 기존 설치 프로그램을 "변환"**하는 것입니다. 설치 과정을 모니터링하고, 모든 파일 쓰기, 레지스트리 변경, 바로가기 생성을 캡처한 다음 자동으로 MSIX를 생성합니다. 따라서 먼저 깨끗하고 완전한 기존 설치 프로그램이 필요합니다.

다시 말해: PyInstaller는 "실행"을 담당하고, Inno Setup은 "설치"를 담당하며, MSIX Packaging Tool은 "Store 게시"를 담당합니다.

한 단계씩 살펴보겠습니다.

1단계: PyInstaller — Python을 EXE로 변환하기

1.1 프로젝트 준비

시작하기 전에 Python 프로젝트 구조가 명확하고 진입점이 잘 정의되어 있는지 확인하세요. RDP Heartbeat을 예로 들면:

prune-rdp-heartbeat/
├── main.py              # 진입 스크립트
├── heartbeat_window.py  # 핵심 창 로직
├── win_utils.py         # Win32 API 래퍼
├── tray_icon.py         # 시스템 트레이
├── settings_dialog.py   # 설정 패널
├── about_dialog.py      # 정보 대화상자
├── config_manager.py    # 설정 관리
├── i18n.py              # 국제화
├── startup.py           # 자동 시작
├── logger.py            # 로깅
├── version.py           # 버전 번호
├── icon.ico             # 애플리케이션 아이콘
├── icon.png             # 소스 아이콘 (MSIX 리소스 생성용)
├── requirements.txt     # 의존성
└── packaging/           # MSIX 관련 리소스 (나중에 사용)

의존성 requirements.txt:

pystray
Pillow
pywin32
customtkinter
packaging

1.2 PyInstaller의 "숨겨진 의존성" 처리

PyInstaller는 정적 분석으로 의존성을 감지하지만, 일부 라이브러리(특히 pystray, PIL)는 감지되지 않아 수동으로 선언해야 합니다:

python
# build_release.py의 주요 설정
cmd = [
    sys.executable, "-m", "PyInstaller",
    "--noconsole",           # 콘솔 창 없음
    "--onefile",             # 단일 exe로 패키징
    "--name=RDPHeartbeat",
    "--clean",
    "--icon=icon.ico",
    "--add-data=icon.ico;.", # Windows는 세미콜론, Linux/macOS는 콜론
    "--hidden-import=PIL._tkinter_finder",
    "--hidden-import=pystray",
    "main.py"
]

또한 리소스 파일(아이콘 등)은 패키징 후 임시 디렉토리에 압축 해제되므로, 호환성을 위해 resource_path()가 필요합니다:

python
# tray_icon.py의 접근 방식
def resource_path(relative_path):
    try:
        base_path = sys._MEIPASS   # PyInstaller가 압축 해제하는 임시 디렉토리
    except Exception:
        base_path = os.path.abspath(".")  # 개발 모드: 현재 디렉토리
    return os.path.join(base_path, relative_path)

파일 시스템에서 읽어야 하는 모든 리소스(아이콘, 글꼴, 설정 파일 템플릿 등)는 이 함수를 거쳐야 합니다.

1.3 원클릭 빌드 스크립트

PyInstaller 명령을 build_release.py로 감싸세요:

python
# 먼저 version.py를 읽어 버전 번호 가져오기
from version import APP_VERSION

def run_build():
    # 0. 이전 빌드 산출물 정리
    if os.path.exists("build"): shutil.rmtree("build")
    if os.path.exists("dist"):  shutil.rmtree("dist")

    # 1. PyInstaller 실행
    subprocess.check_call([...])

    # 2. 산출물 확인
    exe_path = os.path.join("dist", "RDPHeartbeat.exe")
    assert os.path.exists(exe_path), "Build failed!"

python build_release.py를 실행하세요. 모든 것이 순조롭다면 dist/RDPHeartbeat.exe가 생성됩니다.

더블클릭하여 정상적으로 작동하는지 확인할 수 있습니다.

2단계: Inno Setup — EXE를 정식 설치 프로그램으로 만들기

MSIX Packaging Tool에는 변환 소스로 설치 프로그램이 필요합니다. Windows 생태계에서 Inno Setup은 무료이고 성숙하며 스크립트 기반의 설치 프로그램 제작 도구입니다.

2.1 Inno Setup 설치

jrsoftware.org에서 다운로드하여 설치하세요. Inno Script Studio(비주얼 에디터)도 함께 설치하는 것을 권장합니다.

2.2 설치 스크립트 작성

핵심 파일 setup.iss:

ini
#define MyAppName "RDP Heartbeat"
#define MyAppVersion "1.1.3.0"
#define MyAppPublisher "Prune Lab"
#define MyAppExeName "RDPHeartbeat.exe"

[Setup]
AppId={{DDEC247A-5DC0-4393-BB13-466CCAD7F90B}   ; 애플리케이션별 고유 GUID
AppName={#MyAppName}
AppVersion={#MyAppVersion}
AppPublisher={#MyAppPublisher}
DefaultDirName={autopf}\{#MyAppName}             ; 기본값: Program Files
ArchitecturesAllowed=x64compatible                ; 64비트 시스템
ArchitecturesInstallIn64BitMode=x64compatible
DisableProgramGroupPage=yes                       ; "프로그램 그룹 선택" 페이지 건너뛰기
OutputBaseFilename=RDPHeartbeat_Setup             ; 출력 파일 이름
SolidCompression=yes
WizardStyle=modern                                ; 최신 설치 마법사 스타일
SetupIconFile=icon.ico

[Languages]
Name: "english"; MessagesFile: "compiler:Default.isl"

[Files]
Source: "dist\{#MyAppExeName}"; DestDir: "{app}"; Flags: ignoreversion

[Icons]
Name: "{autoprograms}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"
Name: "{autodesktop}\{#MyAppName}";  Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon

[Tasks]
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; Flags: unchecked

[Run]
Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,...}"; Flags: nowait postinstall skipifsilent

주의할 점:

  • AppId는 고유해야 합니다: Windows가 애플리케이션을 식별하는 기준입니다. 다른 소프트웨어와 충돌하지 않도록 하세요. Inno Setup IDE에서 클릭 한 번으로 GUID를 생성할 수 있습니다.
  • 레지스트리 쓰기 불필요: MSIX 변환 시 설치 과정의 레지스트리 쓰기가 자동으로 처리되지만, 이 앱 자체가 필요하지 않으므로 안 쓰는 게 더 깔끔합니다.

2.3 버전 번호 자동 동기화

버전 번호가 Python 코드(version.py)에 정의되어 있고, setup.iss에도 있습니다. 수동 동기화는 조만간 실수로 이어집니다. build_release.py에 자동 패치를 추가하세요:

python
def patch_setup_iss():
    with open("setup.iss", 'r', encoding='utf-8') as f:
        content = f.read()
    content = re.sub(
        r'#define MyAppVersion ".*?"',
        f'#define MyAppVersion "{APP_VERSION}"',
        content
    )
    with open("setup.iss", 'w', encoding='utf-8') as f:
        f.write(content)

이제 python build_release.py를 실행하면 최신 버전 번호가 자동으로 setup.iss에 기록되고, 그 후 PyInstaller가 실행됩니다.

2.4 설치 프로그램 컴파일

Inno Setup IDE를 열고, setup.iss를 불러온 다음 Compile을 클릭하세요. 결과물은 RDPHeartbeat_Setup.exe입니다.

확인: 깨끗한 가상 머신이나 다른 컴퓨터에서 설치 프로그램을 실행하고 다음을 확인하세요:

  • 정상적으로 설치됨
  • 프로그램이 시작됨
  • 제거 후 잔류물 없음 (Inno Setup의 제거 프로그램은 {app}\unins000.exe에 있으며, MSIX가 함께 패키징하므로 문제없습니다)

3단계: MSIX Packaging Tool — 설치 프로그램에서 Store 형식으로

Microsoft Store에 게시할 계획이라면, 먼저 Partner Center에 등록하고 앱 이름을 예약하는 것을 권장합니다(4단계 참조). MPT에서 작성하는 Package Name과 Publisher가 Store에 예약된 정보와 일치해야 하기 때문입니다. 단순히 테스트만 한다면 등록을 건너뛰고 임의의 이름을 사용해도 됩니다.

3.1 환경 준비

필요한 것:

  • MSIX Packaging Tool (공식 문서)
  • 깨끗한 환경 (가상 머신 또는 별도의 컴퓨터 사용 권장. MPT가 전체 설치 과정을 모니터링하므로 시스템의 다른 프로그램이 노이즈로 캡처될 수 있습니다)
  • 서명 인증서: 테스트 단계에서는 자체 서명 인증서를 사용할 수 있습니다. PowerShell(관리자)에서 실행:
powershell
New-SelfSignedCertificate -Type Custom -Subject "CN=YourName" -KeyUsage DigitalSignature -FriendlyName "Your Cert" -CertStoreLocation "Cert:\CurrentUser\My" -TextExtension @("2.5.29.37={text}1.3.6.1.5.5.7.3.3", "2.5.29.19={text}")

인증서 내보내기:

powershell
$password = ConvertTo-SecureString -String "YourPassword" -Force -AsPlainText
Export-PfxCertificate -Cert "Cert:\CurrentUser\My\<Thumbprint>" -FilePath "C:\cert.pfx" -Password $password

공식 게시 시 Microsoft Store가 자체 인증서로 다시 서명하므로 코드 서명 인증서를 구매할 필요가 없습니다. 하지만 Store 외부에 배포하려는 경우(예: 자체 웹사이트에서 MSIX 다운로드 제공) 적절한 코드 서명 인증서를 구매해야 합니다.

3.2 MSIX Packaging Tool 실행

  1. MPT를 열고 "Application package" → **"Create a new package"**를 선택합니다.
  2. "Select environment" 단계에서 기본값을 유지하고 Next를 클릭합니다.
  3. 핵심 단계: MPT가 설치 프로그램 경로를 선택하라고 요청합니다. 방금 생성한 RDPHeartbeat_Setup.exe를 선택합니다.
  4. MPT가 시스템 모니터링을 시작하고 설치 프로그램을 실행합니다. 설치를 정상적으로 완료하세요 — 설치 프로그램 마지막에 "Launch RDP Heartbeat"가 체크되어 있어도 괜찮습니다.
  5. 설치가 완료되면 **"Next"**를 클릭하여 모니터링을 중지합니다.
  6. MPT가 감지한 모든 프로세스를 표시합니다. 앱에 속하지 않는 것(시스템 프로세스, 백신 업데이트 프로세스 등)의 체크를 해제하세요. 보통 RDPHeartbeat.exe, unins000.exe 등만 남깁니다.
  7. 애플리케이션 정보를 입력합니다:
    • Package name: Store에 게시할 경우 Partner Center에서 앱을 예약할 때 할당된 이름(예: PruneLab.RDPHeartbeat)을 입력합니다. 로컬 테스트만 하는 경우 임의의 이름을 사용할 수 있으며, 권장 형식은 YourName.YourApp입니다.
    • Package display name: 앱의 표시 이름, 예: RDP Heartbeat.
    • Publisher display name: 게시자 이름, 예: Prune Lab.
    • Publisher: Store에 게시할 경우 Partner Center 계정에 해당하는 Publisher 인증서 Subject(예: CN=21C32622-53AF-45A6-8AB1-B35BCD475CAD)를 입력합니다. 로컬 테스트의 경우 자체 서명 인증서의 Subject(예: CN=YourName)를 입력합니다.
    • Version: 설치 프로그램에서 자동으로 추출됩니다.
  8. 출력 디렉토리를 지정하고 MSIX 패키지를 생성합니다.

3.3 MSIX 패키지 테스트

.msix 파일이 생성되면 manifest를 서둘러 편집하지 마세요. 더블클릭하여 설치하고 정상적으로 작동하는지 확인하세요. 자체 서명 인증서를 사용하는 경우, 먼저 시스템의 "신뢰할 수 있는 사용자" 저장소에 인증서를 가져와야 합니다(이전 섹션의 인증서 내보내기 명령 참조, Import-PfxCertificate 사용).

4단계: Microsoft Store에 제출하기

4.1 등록 및 이름 예약

  1. Microsoft Partner Center에 개발자 계정을 등록합니다.
  2. Partner Center에서 앱 이름을 예약합니다 (Products → New Product → 이름 입력). 예약 후 Package Name과 Publisher 정보를 받게 되며, 이는 MSIX 패키지의 AppxManifest.xml과 일치해야 합니다.

4.2 제출 프로세스

  1. 앱 → SubmissionsNew submission으로 이동합니다.
  2. 각 필드를 작성합니다:
    • Packages: .msix 파일 업로드
    • Description: 앱 설명 (다국어 지원, 최소 영어와 중국어 권장)
    • Screenshots: 최소 1366×768 앱 스크린샷 1장
    • Store listing: 아이콘, 프로모션 이미지, 기능 목록
  3. 심사에 제출합니다. 첫 심사는 보통 1-3 영업일이 소요됩니다.

마무리: 이 길을 걸을 가치가 있을까?

Python 도구로 큰 돈을 벌 계획이라면, 정답은 "아마 아닐 것"입니다. WinUI 3 / .NET으로 다시 작성하는 것이 패키징 경험이 훨씬 좋습니다.

하지만 저처럼 — 이미 Python으로 작성한 도구를 평소에 자주 사용하고 있고, 더 많은 사람들과 공유하고 싶다면 — 이 과정은 돌아가는 길이지만, 적어도 작동은 합니다.

파이프라인을 돌아보면:

단계도구산출물
.py → .exePyInstaller단일 exe
.exe → 설치 프로그램Inno SetupSetup.exe
설치 프로그램 → MSIXMSIX Packaging Tool.msix
제출Partner CenterStore 게시

이 글이 약간의 우회를 줄이는 데 도움이 되기를 바랍니다. Python 도구를 Store에 올리신 분이 있다면, RDP Heartbeat의 GitHub에서 교류해 주세요.