PySide6 실전 강좌 #8 배포와 마무리 — 아이콘·버전 정보, 서명, GitHub Actions

5 분 소요

기능이 끝난 앱과 배포할 수 있는 앱 사이에는 마지막 구간이 있습니다. 아이콘, 버전 정보, 서명, 그리고 “내 컴퓨터에서만 빌드되는” 상태의 탈출입니다. 입문 #7에서 PyInstaller의 기본을 다뤘으므로, 이번 편은 그 위에 얹는 마감 작업들입니다.

spec 파일: 빌드 설정도 코드입니다 #

입문에서는 명령줄 옵션으로 빌드했지만, 옵션이 늘어나면 spec 파일로 옮기는 것이 맞습니다. pyinstaller 명령이 처음 실행될 때 만들어 주는 daily.spec을 저장소에 커밋하고 직접 관리합니다.

daily.spec
# daily.spec (핵심만)
a = Analysis(
    ["src/daily/__main__.py"],
    datas=[("assets/icons", "icons")],      # 리소스 포함
)
pyz = PYZ(a.pure)
exe = EXE(
    pyz, a.scripts, a.binaries, a.datas,
    name="Daily",
    icon="assets/daily.ico",                # Windows용 아이콘
    console=False,                          # 터미널 창 없이
)
app = BUNDLE(                               # macOS .app 번들
    exe,
    name="Daily.app",
    icon="assets/daily.icns",
    bundle_identifier="com.example.daily",
)

빌드는 pyinstaller daily.spec 한 줄로 고정됩니다. 챙길 세부가 둘 있습니다. 첫째, 아이콘은 플랫폼별 형식이 다릅니다(Windows .ico, macOS .icns). 원본 PNG 하나에서 변환 도구로 두 형식을 만들어 assets에 둡니다. 둘째, spec에 넣은 datas의 리소스는 실행 시 임시 폴더에 풀리므로, 코드에서 리소스 경로를 찾을 때는 개발 실행과 번들 실행을 구분하는 헬퍼가 필요합니다.

src/daily/resources.py
# src/daily/resources.py
import sys
from pathlib import Path


def resource_path(relative: str) -> Path:
    if getattr(sys, "_MEIPASS", None):          # PyInstaller 번들 안
        return Path(sys._MEIPASS) / relative
    return Path(__file__).parent.parent.parent / "assets" / relative

버전: 한 곳에서 정의하고 모두가 읽습니다 #

버전 문자열이 여러 곳에 흩어지면 릴리스마다 하나씩 빠뜨리게 됩니다. 원천을 pyproject.toml의 version 하나로 정하고, 나머지는 전부 읽어 갑니다.

src/daily/__init__.py
# src/daily/__init__.py
from importlib.metadata import version

__version__ = version("daily")

앱의 “정보” 다이얼로그, 로그 첫 줄, 그리고 아래 CI의 릴리스 태그까지 이 값 하나를 씁니다. 릴리스 절차는 “pyproject.toml의 버전 올리기 → 태그 푸시"의 두 동작으로 압축됩니다.

서명: 배포의 마지막 관문 #

빌드한 앱을 다른 컴퓨터에 보내면 처음 만나는 것이 경고창입니다. macOS는 “확인되지 않은 개발자"라며 실행을 막고, Windows의 SmartScreen은 파란 경고를 띄웁니다. 서명되지 않은 실행 파일에 대한 운영체제의 기본 방어이고, 원리와 절차는 Wails 실전 #5 서명과 공증에서 자세히 다뤘습니다. 파이썬 앱이라고 다를 것이 없으므로 여기서는 PySide6·PyInstaller 관점의 요점만 정리합니다.

  • macOS: Apple Developer Program 가입 → codesign으로 .app 서명 → notarytool로 공증 제출 → 스테이플. PyInstaller 산출물 특유의 주의점은 번들 안의 모든 바이너리(Qt 프레임워크 포함)가 서명 대상이라는 것과, hardened runtime 옵션이 필요하다는 것입니다.
  • Windows: 코드 서명 인증서로 signtool 서명. 개인 개발자에게는 인증서 비용이 부담이라, 초기에는 서명 없이 배포하고 SmartScreen 경고를 안내 문서로 대응하는 선택도 현실적입니다.
  • 공통 원칙: 서명은 “누가 만들었는지의 증명"입니다. 개인 취미 배포 단계라면 생략할 수 있지만, 불특정 다수에게 배포하는 순간부터는 통과 의례로 받아들이는 것이 맞습니다.

GitHub Actions: 세 운영체제를 한 번에 빌드 #

“내 맥에서 macOS용만 빌드되는” 상태를 벗어나는 표준 답은 CI의 매트릭스 빌드입니다.

.github/workflows/build.yml
# .github/workflows/build.yml
name: build
on:
  push:
    tags: ["v*"]

jobs:
  test:
    runs-on: ubuntu-latest
    env:
      QT_QPA_PLATFORM: offscreen          # 7편: 화면 없이 Qt 테스트
    steps:
      - uses: actions/checkout@v4
      - uses: astral-sh/setup-uv@v4
      - run: uv sync
      - run: uv run pytest

  build:
    needs: test
    strategy:
      matrix:
        os: [ubuntu-latest, windows-latest, macos-latest]
    runs-on: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v4
      - uses: astral-sh/setup-uv@v4
      - run: uv sync
      - run: uv run pyinstaller daily.spec
      - uses: actions/upload-artifact@v4
        with:
          name: daily-${{ matrix.os }}
          path: dist/

구조는 단순합니다. 태그가 푸시되면, 먼저 테스트 잡이 7편의 테스트를 offscreen으로 돌리고, 통과하면 세 운영체제의 러너가 각자 자기 플랫폼용 실행 파일을 빌드해 아티팩트로 올립니다. PyInstaller는 크로스 컴파일을 하지 않으므로 각 OS에서 각자 빌드하는 매트릭스가 유일한 정답입니다. 여기에 릴리스 생성 액션을 이어 붙이면 “태그 푸시 = 3개 플랫폼 릴리스"가 완성됩니다. 서명 단계를 CI에 넣을 때는 인증서를 저장소 시크릿으로 관리합니다.

시리즈를 마치며 #

8편의 여정을 한 문단으로 회고합니다. 화면 목록으로 요구를 정의하고 3층 구조를 세웠고(1편), SQL을 리포지토리에 가두고 user_version으로 마이그레이션을 준비했고(2편), 커스텀 모델로 데이터와 화면을 이었고(3편), 기성품이 없는 화면은 QPainter로 직접 그렸고(4편), 트레이 상주와 리마인더로 데스크톱다움을 입혔고(5편), QSettings와 테마로 결정권을 사용자에게 돌려줬고(6편), 층별 테스트로 완성 상태를 고정했고(7편), 마지막으로 아이콘·버전·서명·CI로 남에게 건넬 수 있는 물건으로 마감했습니다(8편).

이 순서 자체가 재사용 가능한 템플릿입니다. 다음에 만들 앱이 메모든, 시간 추적이든, 사진 정리든, 같은 뼈대(3층 구조 → 데이터 → 화면 → 상주 → 설정 → 테스트 → 배포)에 살만 갈아 끼우면 됩니다. Go 진영의 같은 여정이 궁금하다면 Wails 실전 강좌와 비교하며 읽는 것도 재미있을 겁니다.

정리 #

  • 빌드 설정은 spec 파일로 저장소에 커밋합니다. 아이콘은 플랫폼별 형식(.ico/.icns)을 준비하고, 리소스 경로는 번들 실행을 구분하는 헬퍼로 찾습니다.
  • 버전은 pyproject.toml 한 곳에서 정의하고 importlib.metadata로 읽습니다. 릴리스는 버전 올리기 + 태그 푸시로 압축됩니다.
  • 서명·공증은 불특정 배포의 통과 의례입니다. macOS는 codesign + 공증, Windows는 signtool이고, 원리는 Wails 실전 #5와 동일합니다.
  • CI는 테스트(offscreen) → 3개 OS 매트릭스 빌드 → 아티팩트의 흐름입니다. PyInstaller는 크로스 컴파일이 없으므로 매트릭스가 정답입니다.
  • 이 시리즈의 순서(구조 → 데이터 → 화면 → 상주 → 설정 → 테스트 → 배포)는 다음 앱에도 그대로 쓸 수 있는 템플릿입니다.
X