macOS 앱 청사진 (oh-my-opensnap 검증 스택)
새 macOS 데스크톱/메뉴바 유틸리티를 매번 다시 고민하지 말고 이 기본형에서 출발한다.
이 스킬은 oh-my-opensnap의 실제 구성(v1.0.65까지 운영)을 일반화한 것이다. "무엇을/왜"의 근거는
이 프로젝트 루트 CLAUDE.md §2를 함께 보라.
역할 분담
- 이 스킬 = 새 앱을 개발하기 위한 스택 선택 + 프로젝트 골격 + 아키텍처 패턴.
mac-app-release(전역 스킬) = 빌드 배포 + 자동 업데이트 파이프라인. oh-my-opensnap 본가의scripts/release.sh는 Developer ID + 공증 + EdDSA appcast 경로다. 멤버십이 없을 때만 스킬 템플릿의 자체서명 경로를 쓴다.
1. 확정 스택 (이유 없이 바꾸지 말 것)
- Swift 5 언어모드 (
SWIFT_VERSION: "5.0",SWIFT_STRICT_CONCURRENCY: minimal) — AppKit/콜백 안정성. - AppKit, 스토리보드 없음 —
main.swift에서NSApplication+AppDelegate코드 부트스트랩. - XcodeGen
project.yml이 유일한 진실의 원천 —.xcodeproj/생성Info.plist는 gitignore. - SPM 의존성 최소화 — 필요한 것만(자동 업데이트 쓰면 Sparkle 2.5.0).
- Apple 1st-party API 우선 — 캡처는 ScreenCaptureKit/CG, 전역 단축키는 Carbon
RegisterEventHotKey(접근성 권한 불필요). - TCC 권한은 표준 시스템 프롬프트 + 설정 딥링크 — 숨은 앱 설정 강요 금지.
메뉴바 상주 앱이면: .accessory 활성화 정책 + Info.plist의 LSUIElement: true + applicationShouldTerminateAfterLastWindowClosed → false.
2. 새 앱 스캐폴딩 절차
디렉터리 골격
<App>/ project.yml Config/Info.plist # XcodeGen이 info.properties로 생성 (gitignore) Sources/ # 기능별 디렉터리로 분리 (App/ 부터) Resources/Assets.xcassets # AppIcon scripts/ # release.sh, make-signing-cert.sh (mac-app-release에서 복사) CHANGELOG.md # "## <버전>" 섹션 — 릴리스 노트 소스 README.md / INSTALL.md .gitignoreproject.yml— 아래 골격에서 이름/번들ID/배포 타깃만 바꾼다.name: <App> options: bundleIdPrefix: com.<org> deploymentTarget: { macOS: "26.0" } createIntermediateGroups: true packages: Sparkle: { url: https://github.com/sparkle-project/Sparkle, from: "2.5.0" } settings: base: SWIFT_VERSION: "5.0" MARKETING_VERSION: "1.0.0" CURRENT_PROJECT_VERSION: "1" # 매 릴리스 +1 (release.sh가 자동) SWIFT_STRICT_CONCURRENCY: minimal DEAD_CODE_STRIPPING: YES targets: <App>: type: application platform: macOS deploymentTarget: "26.0" sources: [ { path: Sources }, { path: Resources } ] dependencies: [ { package: Sparkle } ] info: path: Config/Info.plist properties: LSUIElement: true # 메뉴바 앱이면 CFBundleShortVersionString: "$(MARKETING_VERSION)" CFBundleVersion: "$(CURRENT_PROJECT_VERSION)" LSMinimumSystemVersion: "$(MACOSX_DEPLOYMENT_TARGET)" SUFeedURL: https://raw.githubusercontent.com/<owner>/<repo>/main/appcast.xml SUPublicEDKey: <EdDSA 공개키> # mac-app-release §A에서 생성 SUEnableAutomaticChecks: true SUScheduledCheckInterval: 86400 settings: base: PRODUCT_BUNDLE_IDENTIFIER: com.<org>.<app> ASSETCATALOG_COMPILER_APPICON_NAME: AppIcon ENABLE_APP_SANDBOX: NO # 화면 캡처 등 필요 시 MACOSX_DEPLOYMENT_TARGET: "26.0" LD_RUNPATH_SEARCH_PATHS: [ "$(inherited)", "@executable_path/../Frameworks" ]Sources/App/main.swift— 코드 부트스트랩import AppKit let app = NSApplication.shared let delegate = AppDelegate() app.delegate = delegate app.setActivationPolicy(.accessory) // 메뉴바 앱 app.run()AppDelegate— 얇게: 메인 메뉴(편집 메뉴로⌘Z/⌘C라우팅) + 전역 단축키 연결 + 메뉴바 컨트롤러 +UpdaterController.shared기동.xcodegen generate→ 빌드 확인.자동 업데이트/배포는
mac-app-release+ 이 저장소scripts/release.sh참고 — Developer ID·notarytool 프로필·EdDSA 키 1회 준비,SUPublicEDKey채우기. TCC가 필요한 앱은 ad-hoc 금지.
3. 아키텍처 패턴
- 기능별 디렉터리 분리(
App/,Permissions/,Hotkey/,Settings/, 그리고 도메인별 모듈). - 전역 상태/리소스는
.shared싱글톤 + UI는@MainActor. - 윈도우는
NSWindowController기반(스토리보드/XIB 없음). - 앱 이름·식별 문자열은
Brand.swift한 곳에 모아 새 앱에서 한 번만 교체. - 권한 게이트는 전용 enum으로(
isGranted프리플라이트 /request()프롬프트 /openSystemSettings()딥링크).
4. 함정 (반복 금지)
.xcodeproj/Config/Info.plist직접 편집 ❌ →project.yml고치고xcodegen generate.GENERATE_INFOPLIST_FILE❌ → Sparkle 커스텀 키 때문에 명시적Info.plist.- ad-hoc 서명 ❌ → TCC 권한이 업데이트마다 풀린다. Developer ID(권장) 또는 고정 자체서명으로 안정적 신원 유지.
- 빌드 번호(
CURRENT_PROJECT_VERSION) 고정 ❌ → Sparkle이 업데이트 감지 못 함. - 시크릿(
.p12/EdDSA 개인키) 커밋 ❌ (.gitignore에*.p12). - 숨은 앱 설정으로 권한 강요 ❌ → 표준 시스템 권한 UX.