Claude Design 목업을 SwiftUI 네이티브 앱으로
메뉴바 클립보드 앱 stash 만들기· 1 / 3
- 1 — Claude Design 목업을 SwiftUI 네이티브 앱으로
- 2 — 클립 하나에 33MB — 썸네일 메모리를 99.4% 줄이기
- 3 — macOS 메뉴바 앱 첫 배포기 — 뒤엎은 출시 전략
macOS 메뉴바 클립보드 매니저 stash를 만들면서, 화면 디자인을 처음부터 손으로 그리지 않고 Claude Design(claude.ai/design)이라는 AI 디자인 툴에 맡겨보기로 했다. 이유는 이렇다.
- 혼자 진행하는 프로젝트라 디자이너 없이 UI 완성도를 끌어올려야 했다.
- 코딩 에이전트(Claude Code)에게 말로 설명하는 것보다, 실제로 클릭·키보드가 동작하는 프로토타입을 넘기는 편이 명세로서 더 정확하다고 판단했다.
stash는 이후로도 build부터 ship까지 여러 편에 걸쳐 다룰 계획이라, 이 글이 그 첫 편이다.
Claude Design은 HTML/CSS/JS로 인터랙티브 목업을 만드는 AI 디자인 툴이다. 디자인을 확정하면 README·채팅 전문·프로토타입 파일·아이콘 갤러리·스크린샷을 묶은 “핸드오프 번들”을 export해 코딩 에이전트에게 넘길 수 있다.
결과부터 말하면, 1차 구현은 시각 충실도가 전면 미달이었다. 원인을 추적해보니 문제는 디자인 코드가 아니라, 프로젝트 plan 문서에 박아둔 규칙 하나였다.
Claude Design 세션: 3안에서 B안까지
stash는 3가지 호출 진입점을 가진 앱이다 — 메뉴바 좌클릭으로 여는 NSPopover(방식 1), ⌘ 길게 눌러 여는 미니멀한 NSPanel(방식 2), ⌘ 더블탭으로 여는 NSPanel(방식 3). Claude Design 세션은 PRD를 정독한 뒤 이 구조를 두고 세 방향을 제시했다.
- A. 정통파(Native Faithful): Maccy·Pastebot 결의 익숙한 UI.
- B. Liquid Glass 미니멀: macOS Tahoe의 새 디자인 언어를 반영.
- C. 키보드 히어로: 숫자 키캡을 시각적으로 강조.
“일단 ‘B.Liquid Glass 미니멀’ 방식으로 가고 싶은데”로 B를 고른 뒤, 후속 수정 요청이 약 10회 누적됐다.
- 행 높이 컴팩트화.
- 타입별 아이콘 갤러리 추가(앱·트레이·pin·설정·삭제 각 6종).
- 검색바 위치 조정(두 차례).
- pin 아이콘 3차 재설계.
- 없던 환경설정 창 신규 요청.
- 키보드 내비게이션 모델 전면 재설계 — 검색·클립·핀·설정 4구역을 순환하고, 방향키와 숫자 1/2 키, 한글 키보드에서의 P 키 매칭까지 포함.
- 클립 8개 이상일 때 스크롤 처리.
- ESC로 검색어 리셋.
- 단축키 힌트바의 키캡 그룹핑.
최종적으로 클릭·키보드·데이터(브라우저 로컬 저장소 영속)까지 실제로 동작하는 인터랙티브 프로토타입 HTML과, 디자인 토큰·아이콘 선택·단축키 매핑표·화면별 SwiftUI/AppKit 매핑·데이터 모델·검증 체크리스트 13항목을 담은 구현 핸드오프 문서가 완성됐다.
핸드오프 셋업에서 갈린 두 항목
디자인 셋업 단계에서는 Claude Design 패키지(186KB tar.gz, 압축 해제 시 28파일)를 fetch하고 README·핸드오프 문서·채팅 전문을 정독한 뒤, **“기준 진실 = 인터랙티브 프로토타입 HTML”**로 합의했다. 이미 프로젝트엔 plan 문서 체계(UX-UI·기능·아키텍처·로드맵)가 있었고, 그 안에 <디자인 산출물이 plan과 충돌하면 plan 우선>이라는 규칙이 박혀 있었다. 이 규칙으로 프로토타입과 plan을 17개 항목에 걸쳐 대조 검증했다.
- 정합: 12건은 프로토타입과 plan이 그대로 맞았다.
- 경미한 차이: 3건(⌘⇧⌫ 별칭 단축키·메뉴바 메타포·토스트 표기 방식)은 이견이 있었지만 넘어갔다.
- 정면 충돌: 2건은 규칙대로 처리해야 했다.
- 전체 삭제 동작: 프로토타입은 즉시 실행 후 토스트로 되돌리기를 제공하는데, plan은
NSAlert확인 모달을 요구했다. - 도메인 모델: 핸드오프 문서는 클립 데이터에 7개 필드·4가지 타입을 명시했는데, plan은 11개 필드·3가지 타입을 요구했다.
- 전체 삭제 동작: 프로토타입은 즉시 실행 후 토스트로 되돌리기를 제공하는데, plan은
이 두 건은 그 시점의 규칙에 따라 그대로 **“plan 우선”**으로 결정됐다. 이어서 디자인 토큰 파일 DesignTokens.swift를 새로 만들어, Colors·Typography·Spacing·Radius·WindowSize·Shadow·Animation 7개 카테고리로 디자인 토큰을 옮겼다.
1차 구현: 테스트는 통과했는데 시각은 무너졌다
UI 구현 단계는 방금 정한 두 건의 “plan 우선” 결정을 그대로 반영하며 진행했다. 화면을 그리는 뷰와 뷰모델 등 UI 레이어 파일 20여 개와, ⌘ hold·double-tap을 200ms·250ms 임계값으로 구분해 감지하는 로직이 새로 생겼다. 1차 완료 시점 self-check는 빌드 통과, 82 tests / 13 suites로 깔끔했다.
문제는 그다음 시각 검수였다.
- Liquid Glass 효과가 전혀 적용되지 않았다.
- 메뉴바 아이콘이 원래 SVG가 아니라 다른 아이콘으로 박혀 있었다.
- 시간 표시에서 “전” 접미사가 빠졌다.
- 환경설정 행에 있으면 안 되는 휴지통 아이콘이 붙어 있었다.
- popover 안쪽 여백이 프로토타입과 달랐다.
테스트는 82개 다 통과했는데, 눈으로 보면 완전히 다른 앱이었다.
Root cause: 정책 규칙을 시각까지 끌고 갔다
원인을 추적한 결론은 이랬다. “UX-UI §12-4 박을 때 시각 영역까지 plan 우선 룰로 묶은 게 본질. 정합 검증 ❌ 2건 결정(NSAlert / 도메인) 자체가 역방향이었음.”
즉, <디자인이 plan과 충돌하면 plan 우선>이라는 규칙 하나가 두 종류의 결정에 동시에 적용되고 있었다. 도메인 모델이나 확인 모달 방식 같은 정책 판단에는 이 규칙이 맞았다. 하지만 색상·블러·레이아웃 같은 시각 판단에도 같은 규칙을 그대로 물려쓰면서, 시각 영역에서는 정반대 방향으로 작동하고 있었다. status는 testing에서 developing으로 되돌아갔다.
Fix 사이클 1: 규칙을 쪼개고 다시 그렸다
우선 sub-agent에 위임해 Claude Design 산출물(프로토타입 HTML, 컴포넌트 소스 9종, 19KB짜리 아이콘 소스, CSS 2종)에서 시각·동작 명세를 100% 다시 뽑았다. 그리고 plan 문서의 §12-4 규칙 자체를 정정했다 — **“시각 = 디자인 우선 / 정책 = plan 우선”**으로 분리하고, 디자인 우선으로 뒤집을 항목 5건을 새로 정했다.
NSAlert확인 모달 제거(즉시 실행 + 되돌리기 토스트로 전환).- 메뉴바 아이콘 표현을 적층 카드 형태로 변경.
- 빈 상태 화면에 큰 제목과 보조 문구 추가.
- 단축키 충돌 시 토스트를 병행 표시.
- ⌘⇧⌫ 별칭 단축키 허용.
아이콘 소스의 SVG를 SwiftUI Path로 옮긴 TrayIconView.swift, NSVisualEffectView 래퍼인 VisualEffectView.swift가 새로 생겼고, 클립 행·popover·단축키 힌트바 등 시각 관련 코드를 전면 재작성했다. self-check는 빌드 통과(warning 1), 86 tests / 14 suites.
Fix 사이클 2: 진범은 NSPopover였다
규칙을 고쳤는데도 재검수에서 또 문제가 나왔다.
- Liquid Glass 효과가 여전히 먹지 않았다.
- 메뉴바 아이콘이 또 달랐다.
- Pin된 클립이 있어도 해당 행이 표시되지 않았다.
- 단축키 힌트바의 키캡 표기가 세로로 쪼개져 보였다.
- popover 상단 여백이 과하게 두꺼웠다.
진짜 원인은 코드가 아니라 컴포넌트 선택이었다. NSPopover의 system chrome이 SwiftUI가 그리는 Liquid Glass 효과를 그 위에서 덮어버리고 있었다. 방식 1을 전담하는 별도의 NSPanel 기반 창으로 분리했지만, 이 패널에 준 HUD 스타일 역시 system chrome을 그리면서 Material을 다시 한번 막았다.
겉면에 시스템이 이미 자기 방식대로 코팅을 입혀놓은 유리창이면, 그 위에 다른 코팅(SwiftUI Material)을 아무리 덧발라도 빛이 이미 걸러진 뒤라 소용이 없다. 색상 토큰 값을 백날 조정해도 안 풀리던 이유가 이거였다 — 문제는 값이 아니라 그 값을 그려낼 그릇 자체였다.
본질적인 fix는 NSPanel의 콘텐츠 뷰에 NSVisualEffectView를 직접 붙이고, SwiftUI body는 100% 투명 처리하는 것이었다.
그 밖에 남은 결함도 하나씩 걷어냈다.
ImageRenderer로 트레이 아이콘을 이미지로 변환해 메뉴바에 그렸다.- Pin 행 표시 조건을 “핀 된 클립이 있는지” 하나로 단순화했다.
- 키캡이 줄 단위로 쪼개지지 않도록, SwiftUI
Layout프로토콜 기반의 자체 레이아웃을 새로 만들었다. - 포커스(키 입력)를 받을 수 있도록 override한
NSPanel서브클래스KeyablePanel을 만들어, SwiftUI 키 입력 감지와 텍스트필드 포커스가 정상 동작하게 했다.
범위를 자른 판단: “UI까지만”
두 번의 fix 사이클을 거친 뒤 명시적으로 선을 그었다. “현재 task = UI까지만 / UX 버그는 후속 task로 분리.” 이후 헬퍼 PopoverPanel.swift로 NSPanel+NSVisualEffectView 셋업 중복 코드 약 80줄을 걷어내는 리팩토링은 진행했지만, 단축키 실제 동작·호버 포커스 영역·온보딩 자동 권한 감지 같은 항목은 <검증 X, 후속 task>로 명시적으로 남겨뒀다.
최종 self-check는 빌드 통과(error 0 / warning 0), 테스트 통과 — 86 tests / 14 suites로 회귀 없이 유지됐다. 다만 메뉴바 아이콘 활성·비활성 페어·popover 화살표 꼬리·⌘ hold/double-tap 등장 애니메이션 등 총 18항목은 자동화로는 확인할 수 없어 UNCERTAIN으로 명시 이관했다. 격리된 세션에서는 GUI를 직접 눈으로 볼 수 없으니, 이 부분은 사람이 직접 검수해야 하는 영역으로 문서에 못박아뒀다.
착지: 정책은 plan, 시각은 디자인
이 사이클 전체를 관통하는 결론은 하나다. 정책은 plan이 이기고, 시각은 디자인이 이긴다. 이 둘을 <디자인이 plan과 충돌하면 plan 우선>이라는 단일 규칙으로 뭉뚱그리면, 도메인 모델이나 모달 방식 같은 정책 판단에는 맞는 규칙이 색상·블러·레이아웃 같은 시각 판단에는 거꾸로 작동한다.
AI 디자인 툴이 만든 인터랙티브 프로토타입은 “참고 이미지”가 아니라 실행 가능한 명세다. 실제로 클릭·키보드·데이터가 동작하는 HTML을 기준 진실로 규정했으면, 그 우선순위를 문서 전체 규칙에서 끝까지 일관되게 지켜야 한다는 걸 이번에 몸으로 배웠다. 규칙을 처음 쓸 때는 “plan 우선”이라는 한 줄이 안전한 기본값처럼 보였지만, 막상 시각 영역에 적용해보니 두 번의 fix 사이클과 다수의 재작업을 만들어냈다 — 규칙 자체는 틀리지 않았고, 적용 범위를 잘못 그은 게 문제였다.
그리고 완벽을 기다리지 않고 “UI까지만, UX 버그는 후속으로” 자르는 판단도 이 사이클의 일부였다. 전부 붙잡고 있다가 아무것도 못 내는 것보다, 시각 충실도라는 하나의 축을 확실히 끝내고 넘기는 편이 나았다.