PySide6 실전 강좌 #6 설정 화면 — QSettings, 다크 테마, 데이터 백업
지금까지 리마인더 시각은 21시로, 닫기 동작은 숨기기로 코드에 박혀 있었습니다. 좋은 도구는 이런 결정을 사용자에게 돌려줍니다. 이번 편은 설정 화면을 만들고, 테마를 입히고, 데이터를 내보내는, 앱의 완성도를 끌어올리는 회입니다.
QSettings: 설정 저장의 표준 답안 #
설정을 직접 JSON 파일로 관리할 수도 있지만, Qt에는 이미 표준 답안이 있습니다. QSettings는 키-값을 플랫폼별 표준 위치(macOS는 plist, Windows는 레지스트리, 리눅스는 ini 파일)에 저장해 줍니다. 1편에서 setOrganizationName과 setApplicationName을 설정해 둔 덕에, 인자 없이 만들어도 올바른 위치를 씁니다.
# src/daily/settings.py
from PySide6.QtCore import QSettings
class AppSettings:
"""QSettings를 감싸 키 오타와 타입 실수를 한곳에서 막는다."""
def __init__(self) -> None:
self._s = QSettings()
@property
def reminder_hour(self) -> int:
return int(self._s.value("reminder/hour", 21))
@reminder_hour.setter
def reminder_hour(self, value: int) -> None:
self._s.setValue("reminder/hour", value)
@property
def close_to_tray(self) -> bool:
return self._s.value("window/close_to_tray", True, type=bool)
@close_to_tray.setter
def close_to_tray(self, value: bool) -> None:
self._s.setValue("window/close_to_tray", value)
@property
def theme(self) -> str: # "system" | "light" | "dark"
return str(self._s.value("appearance/theme", "system"))
@theme.setter
def theme(self, value: str) -> None:
self._s.setValue("appearance/theme", value)래퍼 클래스를 한 겹 두는 이유는 QSettings의 유명한 함정 때문입니다. 저장된 값이 플랫폼에 따라 문자열로 돌아올 수 있습니다. bool을 저장했는데 "true"라는 문자열이 돌아와 if 판정이 뒤집히는 사고가 전형적입니다. type=bool 지정이나 명시적 변환을 래퍼 안에 모아 두면, 이 함정을 앱 전체에서 한 번만 처리하면 됩니다. 키 이름도 "reminder/hour"처럼 그룹/이름 형식의 상수가 되어 오타가 사라집니다.
설정 화면과 동작의 연결 #
설정 페이지 자체는 입문에서 배운 폼 조립입니다. 요점은 위젯이 아니라 연결입니다.
# src/daily/ui/settings_page.py (핵심만)
class SettingsPage(QWidget):
def __init__(self, settings: AppSettings, reminder, main_window) -> None:
super().__init__()
self._settings = settings
hour = QSpinBox()
hour.setRange(0, 23)
hour.setValue(settings.reminder_hour)
hour.valueChanged.connect(self._on_hour_changed)
close_tray = QCheckBox("닫기 버튼을 누르면 트레이로 숨기기")
close_tray.setChecked(settings.close_to_tray)
close_tray.toggled.connect(self._on_close_mode_changed)
...
def _on_hour_changed(self, value: int) -> None:
self._settings.reminder_hour = value
self._reminder.set_hour(value) # 실행 중인 리마인더에 즉시 반영원칙은 두 줄입니다. 저장과 적용을 함께 합니다. 설정 값을 QSettings에 쓰는 것으로 끝내면 “재시작해야 반영되는” 앱이 됩니다. 5편의 Reminder에 set_hour 메서드를 추가하고, closeEvent가 플래그 대신 settings.close_to_tray를 읽게 고치면, 설정이 그 자리에서 살아 움직입니다. 그리고 “확인/취소” 다이얼로그 대신 즉시 적용을 택했습니다. 로컬 도구 앱의 설정은 바꾸면 바로 반영되는 쪽이 요즘 관례에 맞습니다.
테마: 스타일시트 교체와 시스템 감지 #
Qt 위젯의 외형은 CSS와 닮은 스타일시트(QSS)로 바꿀 수 있습니다. 테마 구현의 뼈대는 “다크용 QSS 문자열과 라이트용 QSS 문자열을 준비해 두고 app.setStyleSheet으로 갈아 끼우기"입니다.
# src/daily/ui/theme.py (발췌)
DARK_QSS = """
QWidget { background: #1e1f22; color: #e6e6e6; }
QPushButton { background: #2d333b; border: 1px solid #444c56; padding: 6px 12px; border-radius: 4px; }
QPushButton:hover { background: #39404a; }
QListView { background: #16171a; border: none; }
"""
def apply_theme(app: QApplication, mode: str) -> None:
if mode == "system":
scheme = app.styleHints().colorScheme() # OS의 라이트/다크 설정
mode = "dark" if scheme == Qt.ColorScheme.Dark else "light"
app.setStyleSheet(DARK_QSS if mode == "dark" else "")챙길 포인트가 둘 있습니다. 첫째, “system” 모드는 styleHints().colorScheme()으로 OS 설정을 읽고, colorSchemeChanged 시그널에 연결해 두면 OS 테마가 바뀔 때 앱도 따라 바뀝니다. 둘째, 4편의 히트맵처럼 직접 그리는 위젯은 스타일시트의 영향을 받지 않습니다. 하드코딩했던 색을 팔레트나 테마 모듈의 상수에서 읽도록 고쳐야 테마 전환에 함께 반응합니다. 커스텀 드로잉의 숨은 비용이 이런 곳에서 나타납니다.
데이터 내보내기: 사용자의 데이터는 사용자의 것 #
로컬 앱의 마지막 예의는 데이터를 인질로 잡지 않는 것입니다. 두 가지를 제공합니다.
def export_csv(repo: HabitRepository, path: Path) -> None:
import csv
with open(path, "w", newline="", encoding="utf-8") as f:
writer = csv.writer(f)
writer.writerow(["habit", "day"])
for habit in repo.active_habits():
for day in sorted(repo.checked_days(habit.id)):
writer.writerow([habit.name, day])- CSV 내보내기: 파일 위치는
QFileDialog.getSaveFileName으로 사용자가 고릅니다. 스프레드시트에서 열리는 형식이 곧 “내 데이터를 들고 나갈 수 있다"는 신뢰가 됩니다. - DB 백업: sqlite3의
Connection.backup()을 쓰면 사용 중인 DB도 안전하게 복사됩니다. 파일을 그냥 복사하는 것보다 안전한, SQLite가 제공하는 정식 백업 경로입니다. 리포지토리에backup_to(path)메서드로 추가해 둡니다.
정리 #
- 설정은 QSettings가 표준입니다. 플랫폼별 저장 위치를 알아서 처리하고, 타입 함정(문자열로 돌아오는 bool)은 래퍼 클래스에서 한 번에 막습니다.
- 설정은 저장과 동시에 적용합니다. 재시작해야 반영되는 설정은 미완성입니다.
- 테마는 QSS 교체가 뼈대이고, system 모드는 colorScheme 감지와 변경 시그널로 만듭니다. 직접 그리는 위젯은 색을 테마 상수에서 읽게 고쳐야 합니다.
- CSV 내보내기와 backup() 기반 DB 백업으로 사용자 데이터의 이동 자유를 보장합니다.
- 기능은 이제 완성입니다. 다음 편은 이 앱이 계속 완성 상태로 남게 만드는 장치, pytest-qt 테스트입니다.