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. 앱 → Submissions → New 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에서 교류해 주세요.