작은 macOS 앱이 성장하는 방식 — 배포 자동화, 수동 베일, 그리고 업데이트 UX

· 4분 읽기
목차
시리즈 Veil 개발기
2 / 2
  1. 1. 영화 볼 때 옆 모니터가 거슬려서 만든 macOS 앱 — CoreGraphics 전체화면 감지부터 Homebrew 배포까지
  2. 2. 작은 macOS 앱이 성장하는 방식 — 배포 자동화, 수동 베일, 그리고 업데이트 UX 현재

도구가 가치를 증명하면, 가치를 유지하는 방식도 같이 성장해야 한다.

Veil v0.5.0 개발기 커버 이미지

이전 글에서 Veil을 처음 만들고 배포하는 과정을 다뤘다. 그때 마지막에 남겨둔 문장이 있었다: “릴리즈마다 수동으로 처리하고 있는데, GitHub Actions가 자동으로 업데이트하도록 자동화할 예정이다.”

그 예정이 현실이 됐고, 그 과정에서 수동 베일 모드와 자동 업데이트라는 두 가지 기능이 더해졌다. 이 글은 그 사이에 어떤 결정들이 쌓였는지에 대한 기록이다.


자동 감지의 한계 — 영상이 아닌 전체화면

Veil의 핵심은 “전체화면 영상이 감지되면 다른 모니터를 가린다”는 것이다. 하지만 실제로 써보니 전체화면이 영상이 아닌 경우에도 반응하는 상황이 있었다.

코드 에디터를 전체화면으로 쓰거나, 터미널을 전체화면으로 띄울 때도 오버레이가 올라온다. 의도한 동작이긴 하지만 — CGWindowList로 창 크기만 보니까 당연하다 — 항상 원하는 건 아니었다.

더 중요한 시나리오가 있었다. 영상이 아닌 상황에서도, 지금 당장 다른 모니터를 가리고 싶을 때. 예를 들어 화면 공유 중인데 다른 모니터에 알림이 뜨는 게 싫거나, 발표 전에 미리 화면을 정리하고 싶을 때.

이건 자동 감지의 영역이 아니다. 사용자가 능동적으로 “지금 가려라”라고 지시하는 방식이 필요했다.


수동 베일 — 핫키와 커서 위치 휴리스틱

수동 모드의 설계에서 핵심 질문은 하나였다: “어떤 모니터를 활성 모니터로 유지할 것인가?”

전체화면 감지에서는 정답이 명확하다. 전체화면 창이 있는 모니터가 활성 모니터다. 하지만 수동 모드에서는 사용자가 어떤 모니터를 보고 있는지를 알아내야 한다.

선택한 방식은 간단하다: 마우스 커서가 있는 모니터를 활성 모니터로 유지한다. 핫키(⌃⌥⌘ V)를 누르는 순간 NSEvent.mouseLocation으로 커서 위치를 가져오고, 그 위치가 속한 모니터를 제외하고 나머지를 모두 가린다.

이 결정의 근거는 사용 패턴에 있다. 핫키를 누르는 순간 보통 작업 중인 모니터를 보고 있다. 커서 위치는 가장 신뢰할 수 있는 “지금 어디를 보고 있는가”의 대리 지표다.

핫키를 다시 누르거나, 메뉴바의 “Veil Now” 토글을 끄면 수동 모드가 해제된다. 수동 모드는 자동 감지보다 우선순위가 높다 — 수동 모드가 켜진 상태에서는 전체화면 변화를 무시한다. 의도가 명확할 때는 자동 판단이 끼어들지 않아야 한다.


업데이트 UX — Homebrew만으로 충분한가?

Veil은 Homebrew tap으로 배포된다. brew upgrade veil이 업데이트 경로다. 개발자에게는 자연스러운 방식이지만, 모든 사용자가 터미널에서 정기적으로 brew upgrade를 실행하지는 않는다.

“새 버전이 나왔다는 걸 어떻게 알 수 있을까?”에서 시작했다. 몇 가지 선택지가 있었다.

첫 번째: 아무것도 하지 않는다. Homebrew 사용자는 알아서 업데이트한다. 가능하지만, 버그 수정이나 개선이 있어도 사용자가 알 방법이 없다.

두 번째: GitHub Releases API를 호출해 새 버전을 감지하고 알림만 띄운다. “새 버전이 있습니다. brew upgrade veil을 실행하세요.” 구현은 간단하지만, 알림을 받고 터미널로 가서 명령어를 치는 건 좋은 UX가 아니다. 사용자는 업데이트 여부를 판단해야 하고, 그 판단을 실행으로 옮기는 과정이 단절되어 있다.

세 번째: Sparkle 프레임워크를 도입해 앱 내에서 업데이트를 감지, 다운로드, 설치까지 처리한다. macOS 인디 앱의 사실상 표준이다.

처음에는 두 번째 방식을 고려했다. 구현이 간단하고 외부 의존성이 추가되지 않으니까. 하지만 사용자 경험을 생각하면 반쪽짜리 해결책이었다. 알림만 띄우고 수동 업데이트를 요구하는 건, “업데이트가 있다는 걸 알려줄게, 나머지는 알아서 해”와 같다.

세 번째 방식을 선택했다. 처음에는 Homebrew와 Sparkle이 충돌하지 않을까 걱정했는데, 실제로는 문제가 없다. Rectangle, Stats 같은 인기 앱들이 Homebrew 배포 + Sparkle을 동시에 사용 중이다. Sparkle이 먼저 업데이트하고, 나중에 brew upgrade가 같은 버전을 재설치할 뿐이다.


Developer ID 없이 자동 업데이트 — EdDSA만으로 가는 길

Sparkle을 도입하기로 했을 때 가장 큰 장벽은 코드 서명이었다.

macOS 앱을 App Store 외부에서 배포하려면 Apple Developer Program($99/년)의 Developer ID 인증서로 서명해야 Gatekeeper를 통과한다. Sparkle도 예외가 아니다 — 업데이트된 앱도 Gatekeeper 검증을 받는다.

하지만 Sparkle 2는 업데이트 무결성 검증을 위해 두 가지 서명 체계를 사용한다.

서명목적필요한 것
EdDSA 서명업데이트 파일의 무결성 검증직접 생성한 키 쌍 (무료)
Apple 코드 서명macOS Gatekeeper 통과Developer ID 인증서 (유료)

Sparkle이 업데이트를 검증하는 건 EdDSA이므로 Apple 인증서가 필수가 아니다. 문제는 그 다음이다 — Gatekeeper가 서명되지 않은 앱을 차단한다.

Veil은 Homebrew가 주요 설치 경로다. Homebrew Cask는 앱 설치 시 quarantine 플래그를 처리하므로, Homebrew 사용자에게는 Gatekeeper 이슈가 발생하지 않는다. GitHub Release에서 직접 다운로드하는 사용자는 처음 설치 시 우클릭 > 열기로 수동 허용이 필요하지만, 이건 Veil이 처음부터 가진 제약이지 Sparkle 도입으로 새로 생긴 건 아니다.

결론: EdDSA 키만으로 Homebrew 사용자에게 완전한 자동 업데이트 경험을 제공할 수 있다. Developer ID는 직접 다운로드 사용자의 UX를 개선하는 요소일 뿐, 필수가 아니다.

CI에 appcast 자동 생성 추가

릴리스 파이프라인에 Sparkle appcast 생성 단계를 추가했다. CI가 빌드 후 generate_appcast 도구로 EdDSA 서명된 appcast.xml을 생성하고, GitHub Pages를 통해 호스팅한다. 앱은 SUFeedURL에 지정된 URL에서 주기적으로 appcast를 확인해 새 버전을 감지한다.

태그 푸시 한 번으로 GitHub Release + Homebrew Cask + Sparkle appcast가 모두 자동 갱신된다.


현재까지의 결정 누적

v0.3.0에서 v0.5.0 사이에 내린 결정들을 정리하면 이렇다.

결정근거대안
수동 베일에 커서 위치 휴리스틱사용 패턴 기반 가장 단순한 추론모니터 선택 UI
수동 > 자동 우선순위명확한 의도 충돌 시 수동 의도 존중자동 감지 병행
Sparkle 도입알림-only 방식의 UX 단절GitHub API 알림
EdDSA-only (Developer ID 없이)Homebrew 경로에서 충분Apple Developer Program 가입

공통 패턴이 하나 있다. “가장 단순한 것으로 충분한가?”를 먼저 물었다. 커서 위치 휴리스틱도, Homebrew + Sparkle 병행도, EdDSA-only도 그렇다. 복잡한 해결책을 고려할 때마다 “정말 필요한가?”를 다시 확인했다.

Veil은 MIT 라이선스로 GitHub에 공개되어 있다.

brew tap neocode24/tap
brew install --cask --no-quarantine veil
시리즈 Veil 개발기
2 / 2
  1. 1. 영화 볼 때 옆 모니터가 거슬려서 만든 macOS 앱 — CoreGraphics 전체화면 감지부터 Homebrew 배포까지
  2. 2. 작은 macOS 앱이 성장하는 방식 — 배포 자동화, 수동 베일, 그리고 업데이트 UX 현재

이어서 읽기