PySide6 실전 강좌 #1 프로젝트 설계 — 습관 트래커를 만들며 배우는 앱 구조

5 분 소요

PySide6로 데스크톱 앱 만들기 입문 시리즈에서 위젯, 시그널, 모델/뷰, 패키징까지 부품을 익혔습니다. 그런데 부품을 아는 것과 앱을 완성하는 것 사이에는 골짜기가 있습니다. 파일을 어떻게 나누는지, 데이터는 어디에 두는지, “동작은 하는데 코드가 엉망"을 어떻게 피하는지는 입문 예제가 알려 주지 않기 때문입니다. 이 실전 시리즈는 그 골짜기를 건넙니다. 앱 하나를 8편에 걸쳐 설계부터 배포 가능한 상태까지 완성합니다.

무엇을 만드는가: 로컬 습관 트래커 “데일리” #

만들 앱은 습관 트래커입니다. 매일 실천할 습관(운동, 독서, 물 마시기)을 등록하고, 오늘 했는지 체크하고, 잔디밭 스타일의 히트맵으로 꾸준함을 확인하는 로컬 앱입니다. 이 소재를 고른 이유는 학습 밀도 때문입니다.

  • CRUD + 영속성: 습관과 체크 기록을 SQLite에 저장합니다 (2편).
  • 모델/뷰 실전: 커스텀 모델과 델리게이트로 목록 화면을 만듭니다 (3편).
  • 커스텀 드로잉: 히트맵은 기성 위젯이 없어서 QPainter로 직접 그립니다 (4편).
  • 데스크톱다움: 트레이 상주와 리마인더 알림 (5편), 설정과 테마 (6편).
  • 품질과 배포: pytest-qt 테스트 (7편), 아이콘·버전·CI까지 배포 마감 (8편).

기능 요구는 처음에 화면 단위로 적어 두는 것이 좋습니다. 이 앱의 화면은 셋입니다. 메인 화면(오늘의 습관 목록 + 체크), 통계 화면(습관별 히트맵과 달성률), 설정 화면(테마, 리마인더 시각). 화면 목록이 곧 작업 목록이 되고, “이번 편에서 어디까지"의 경계가 됩니다.

구조: UI, 코어, 데이터의 3층 #

실전에서 가장 먼저 무너지는 것이 구조입니다. 버튼 핸들러 안에 SQL이 들어가기 시작하면, 테스트도 수정도 불가능한 한 덩어리가 됩니다. 이 시리즈는 처음부터 3층으로 나눕니다.

프로젝트 구조
daily/
├── pyproject.toml
└── src/daily/
    ├── __main__.py        # 진입점: python -m daily
    ├── app.py             # QApplication 생성과 조립
    ├── ui/                # 화면: 위젯, 창, 모델(뷰용)
    │   └── main_window.py
    ├── core/              # 규칙: 습관·연속 기록 계산 등 Qt와 무관한 로직
    │   └── habits.py
    └── data/              # 저장: SQLite 접근 (2편에서 구현)
        └── repository.py

층의 규칙은 두 줄로 요약됩니다. 의존 방향은 ui → core → data의 한 방향이고, core에는 PySide6를 임포트하지 않습니다. core가 Qt를 모르면 좋은 일이 연쇄적으로 생깁니다. “연속 14일 달성” 같은 규칙을 GUI 없이 일반 pytest로 검증할 수 있고(7편), 나중에 CLI나 웹으로 옮길 때도 core는 그대로 갑니다. 데스크톱 앱이 망가지는 전형적 경로가 “UI에 로직이 눌어붙는 것"이므로, 이 규칙 하나가 시리즈 전체의 뼈대입니다.

프로젝트 셋업 #

셋업은 uv 기준입니다. 환경 관리가 낯설면 파이썬 패키징 시리즈를 먼저 보시기 바랍니다.

프로젝트 셋업
uv init daily --package
cd daily
uv add pyside6
uv add --dev pytest pytest-qt

--package 옵션으로 src 레이아웃의 패키지형 프로젝트를 만들었습니다. 단일 스크립트가 아니라 패키지로 시작하는 이유는 8편에서 다루겠습니다. PyInstaller로 묶을 때도, 테스트에서 임포트할 때도 패키지 구조가 문제를 줄여 줍니다.

메인 윈도우 뼈대 #

이번 편의 코드 목표는 화면 전환 구조까지입니다. 세 화면을 QStackedWidget에 담고 사이드 버튼으로 전환하는, 앞으로 7편을 지탱할 뼈대입니다.

src/daily/ui/main_window.py
# src/daily/ui/main_window.py
from PySide6.QtWidgets import (
    QHBoxLayout, QLabel, QMainWindow, QPushButton,
    QStackedWidget, QVBoxLayout, QWidget,
)


class MainWindow(QMainWindow):
    def __init__(self) -> None:
        super().__init__()
        self.setWindowTitle("데일리")
        self.resize(480, 640)

        # 화면 자리: 지금은 자리 표시 라벨, 다음 편부터 실제 화면으로 교체
        self.pages = QStackedWidget()
        self.pages.addWidget(QLabel("오늘"))     # index 0: 3편에서 구현
        self.pages.addWidget(QLabel("통계"))     # index 1: 4편에서 구현
        self.pages.addWidget(QLabel("설정"))     # index 2: 6편에서 구현

        nav = QVBoxLayout()
        for i, name in enumerate(["오늘", "통계", "설정"]):
            btn = QPushButton(name)
            btn.clicked.connect(lambda _=False, idx=i: self.pages.setCurrentIndex(idx))
            nav.addWidget(btn)
        nav.addStretch()

        root = QWidget()
        layout = QHBoxLayout(root)
        layout.addLayout(nav)
        layout.addWidget(self.pages, stretch=1)
        self.setCentralWidget(root)
src/daily/app.py
# src/daily/app.py
import sys

from PySide6.QtWidgets import QApplication

from daily.ui.main_window import MainWindow


def main() -> int:
    app = QApplication(sys.argv)
    app.setApplicationName("daily")
    app.setOrganizationName("daily-app")   # 6편의 QSettings가 이 값을 씁니다
    window = MainWindow()
    window.show()
    return app.exec()
src/daily/__main__.py
# src/daily/__main__.py
import sys

from daily.app import main

sys.exit(main())

uv run python -m daily로 실행하면 좌측 내비게이션과 세 개의 빈 화면이 있는 창이 뜹니다. 람다에서 idx=i로 값을 고정하는 것은 입문 #3에서 다룬 클로저 함정의 실전 적용입니다. setOrganizationName처럼 사소해 보이는 줄도 뒤 편(QSettings)의 복선이므로 지금 넣어 둡니다.

이 시리즈의 지도 #

편만드는 것핵심 기술
1 (이 글)설계와 뼈대3층 구조, QStackedWidget
2데이터 층SQLite, 리포지토리, 마이그레이션
3오늘 화면커스텀 모델, 델리게이트
4통계 화면QPainter 히트맵
5트레이 상주QSystemTrayIcon, 알림, 단일 인스턴스
6설정QSettings, 테마, 백업
7테스트pytest-qt, 층별 테스트 전략
8배포 마감아이콘·버전, 서명 개요, GitHub Actions

정리 #

  • 이 시리즈는 습관 트래커 앱 하나를 8편에 걸쳐 설계부터 배포 가능 상태까지 완성합니다. 화면 목록(오늘·통계·설정)이 작업의 경계입니다.
  • 구조는 ui → core → data의 한 방향 3층이고, core는 PySide6를 임포트하지 않습니다. 이 규칙이 테스트 가능성과 유지보수성의 뿌리입니다.
  • 프로젝트는 uv의 src 레이아웃 패키지로 시작합니다. 패키징과 테스트에서 이 선택이 값을 합니다.
  • 메인 윈도우는 QStackedWidget 기반의 화면 전환 뼈대로 만들어 두었습니다. 다음 편에서 이 앱의 심장인 SQLite 데이터 층을 만듭니다.
X