태그
# CLAUDE
#CLAUDE-CODE
#도구
2026년 3월 17일 13:32

CLI는 한계가 있었다 — Token Garden, 토큰 사용량을 키우는 정원
터미널을 떠나고 싶었다
지난 글에서 cctoken을 만들었다. Claude Code가 토큰을 얼마나 쓰는지 터미널에서 확인하는 CLI 도구였다.
쓸만했다. 근데 문제가 있었다.
터미널을 열어야 한다.
코딩하다가 "오늘 얼마나 썼지?" 싶으면 새 탭 열고, cctoken today 치고, 숫자 확인하고,
다시 돌아온다. 하루에 이걸 열 번은 하니까 은근 귀찮다. 그리고 히트맵을 보려면 터미널
폭이 넓어야 하는데, 분할 화면에서는 깨진다.
그래서 생각했다. 메뉴바에 항상 떠있으면 안 되나?
클릭 한 번이면 히트맵, 프로젝트별 사용량, 활성 세션을 볼 수 있고, 아이콘 옆에 오늘 토큰 수가 계속 보이는 앱. 식물이 자라는 애니메이션으로 토큰 사용량을 시각화하면 재밌겠다 싶었다.
Token Garden은 그렇게 시작됐다.
만든 것
macOS 메뉴바에 상주하는 토큰 트래커 앱이다.
~/.claude/ 디렉토리의 JSONL 로그를 실시간으로 파싱해서, 히트맵/통계/프로젝트별
사용량/활성 세션을 팝오버로 보여준다. GitHub 잔디처럼 일별 사용량이 색깔로 표시된다.
주요 기능
- 히트맵 — 일/주/월/연 뷰 전환 가능. 7단계 색상 강도. 8가지 컬러 테마 (레인보우 포함)
- 프로젝트별 통계 — 어떤 프로젝트에서 토큰을 많이 썼는지 비율로 확인
- 활성 세션 — 지금 돌아가는 Claude Code 세션을 실시간 추적
- 메뉴바 표시 — 아이콘만 / 아이콘+토큰 수 / 아이콘+미니 그래프 중 선택
- 자동 업데이트 확인 — GitHub Releases에서 새 버전 감지
기술 스택
- Swift 6 + SwiftUI + SwiftData
- FSEventStream으로 로그 파일 실시간 감시
- NSPopover 기반 메뉴바 앱
ps+lsof로 활성 세션 감지
Claude에게 앱을 만들게 하면 생기는 일
솔직히 말하면, 이 앱은 Claude Code가 거의 다 짰다. 나는 요구사항을 말하고, 결과를 확인하고, 버그를 리포트했다.
근데 그 과정이 순탄하지 않았다.
팝오버가 춤을 춘다
"섹션 접고 펼칠 때 높이가 줄어서 목록이 안 보여."
Claude가 전체를 ScrollView로 감쌌다. 내부 스크롤이 생겼다. 싫다고 했다.
그럼 팝오버 높이를 동적으로 바꾸겠다고 sizingOptions = .preferredContentSize를 썼다.
섹션 토글할 때마다 팝오버 전체가 흔들렸다. 타이틀부터 덜덜 떨렸다.
withAnimation이 팝오버 리사이즈와 충돌한다는 걸 세 번의 수정 끝에 알아냈다. 결론:
동적 높이 + 애니메이션 없음. 각 섹션만 내부 스크롤.
클릭이 안 된다
우클릭 Quit 메뉴를 추가했더니 좌클릭이 먹통이 됐다. statusItem.menu를 설정하면
NSStatusItem이 메뉴 모드로 들어가서 popover 액션이 씹힌다.
DispatchQueue.main.asyncAfter로 0.1초 뒤에 menu를 nil로 바꾸는 코드를 넣었는데,
타이밍이 맞지 않으면 또 막혔다. 결국 좌클릭 시 무조건 statusItem.menu = nil로
클리어하는 방식으로 해결.
세션이 안 죽는다
Claude 세션을 껐는데 앱에는 계속 "Active"로 떠 있었다. 이 문제를 잡는 데 가장 오래 걸렸다.
첫 번째 원인: TokenDataStore가 ModelContext(modelContainer)로 새 컨텍스트를
만들고 있었다. SwiftUI의 @Query는 modelContainer.mainContext를 쓴다. 서로 다른
컨텍스트라 한쪽에서 isActive = false로 바꿔도 다른 쪽에서 모른다. 한 줄 수정:
self.modelContext = modelContainer.mainContext
두 번째 원인: record() 메서드가 토큰 이벤트마다 session.isActive = true를
설정하고 있었다. backfill에서 과거 이벤트를 처리하면 이미 죽은 세션이 되살아났다.
세 번째 원인: ps + lsof를 Process로 실행할 때, waitUntilExit() →
readDataToEndOfFile() 순서로 호출하면 파이프 버퍼가 가득 찼을 때 데드락이 걸린다. ps -eo pid,comm의 출력이 64KB를 넘으면 ps가 쓰기에서 블록, waitUntilExit가 종료 대기,
영원히 멈춘다. 순서를 뒤집으면 해결: readDataToEndOfFile() → waitUntilExit()
이 세 가지가 겹쳐서 "세션이 안 죽는" 현상이 됐다.
오늘이 아닌데 오늘이라고 우긴다
메뉴바에 "533.7K"가 떴다. 오늘 토큰이 그렇게 많을 리 없는데?
menuBarController.onTokenEvent()이 날짜 체크 없이 모든 이벤트를 todayTokens에
더하고 있었다. backfill에서 어제 이벤트까지 합산돼서 어제 사용량이 오늘 것처럼
표시됐다.
Calendar.current.isDateInToday(event.timestamp) 한 줄 추가로 해결.
삽질에서 배운 것
Claude가 코드를 짜주면 빠르다. 하지만 "동작한다"와 "제대로 동작한다" 사이에는 넓은 강이 있다.
- SwiftData의 ModelContext는 공유해야 한다 — @Query와 같은 mainContext를 써야 변경이 반영된다
- NSPopover + sizingOptions + withAnimation = 떨림 — 동적 높이 팝오버에서 애니메이션은 쓰면 안 된다
- Process의 Pipe는 데드락 위험 — 반드시 read 먼저, wait 나중
- 백그라운드 스레드에서 메인으로의 통신 — Swift 6 concurrency에서
@MainActor메서드를 DispatchQueue.main.async로 호출하면 예상대로 안 될 수 있다. NSLock + 폴링이 더 안정적이었다
Claude는 시키는 대로 한다 — 그게 문제다
위의 삽질들을 보면 패턴이 보인다. 내가 "고쳐"라고 하면 Claude는 즉시 고친다. 근데 뭘 고쳐야 하는지 모르면서 고친다.
"세션이 안 사라져" → DispatchQueue 체인을 바꾼다 → 안 된다 → Task.detached로 바꾼다
→ 안 된다 → nonisolated로 바꾼다 → 안 된다. 원인은 ModelContext가 달랐던 건데,
증상만 보고 코드를 계속 바꾸고 있었다.
"클릭이 안 돼" → 우클릭 메뉴 타이밍을 조정한다 → 안 된다. 원인은 ps+lsof가 메인
스레드에서 파이프 데드락을 일으킨 건데, 클릭 핸들러만 만지고 있었다.
이게 AI에게 코드를 맡길 때 빠지는 함정이다. AI는 "왜"를 모른 채 "어떻게"만 바꾼다. 내가 "안 돼"라고 하면, 가장 가까운 코드부터 바꿔본다. 근본 원인을 추적하는 게 아니라 증상에 반응한다.
하네스 엔지니어링이라는 것
Claude Code 같은 AI 코딩 에이전트를 쓸 때, 프롬프트만 잘 쓴다고 되는 게 아니다. 에이전트의 행동을 제어하고, 검증하고, 올바른 방향으로 유도하는 구조가 필요하다. 이걸 하네스 엔지니어링이라고 부른다.
Token Garden을 만들면서 체감한 하네스 오케스트레이션 원칙들이 있다:
1. "고쳐"가 아니라 "분석해"를 먼저
버그가 생기면 바로 "고쳐"라고 하고 싶다. 근데 그러면 Claude는 가장 가까운 코드를 무작위로 바꾼다. 세션 안 죽는 문제가 10번 넘는 수정 끝에야 풀렸다.
뒤늦게 "배포하지 말고 문제점 분석만 해봐"라고 했더니 그제서야 제대로 된 원인 3개를 찾아냈다. 진단이 먼저, 수정은 나중.
2. 변경을 격리해서 테스트
세션 갱신이 안 되는 문제에서, 갱신 로직 자체가 문제인지 다른 곳이 문제인지 구분이 안 됐다. "세션 갱신 코드를 주석 처리하고 나머지가 되는지 확인해봐"라고 했더니, 갱신 코드가 메인 스레드를 블록하고 있다는 걸 바로 알 수 있었다.
AI한테도 과학적 방법을 시켜야 한다. 변수 하나만 바꾸고 관찰.
3. AI가 만든 문제를 AI가 못 찾을 수 있다
ModelContext를 mainContext 대신 새로 만든 건 Claude가 처음에 짠 코드다. 그리고 그걸
고치라고 했을 때도 Claude는 그 줄을 안 건드렸다. "원래 이렇게 하는 거니까" 라고 생각한
건지, 자기가 짠 코드의 구조적 문제를 인식 못 했다.
결국 내가 DB를 직접 sqlite3로 열어서 isActive 값을 확인하고, "데이터는 바뀌는데 UI가
안 바뀐다 → ModelContext가 다른 거 아니야?"라고 짚어줘야 했다.
4. CLAUDE.md로 반복 실수를 막는다
같은 피드백을 세 번 하고 있으면 뭔가 잘못된 거다. "내부 스크롤 넣지 마", "팝오버 높이 고정하지 마", "배포는 시킬 때만 해" — 이런 걸 CLAUDE.md나 메모리에 기록해두면 다음 세션에서 반복하지 않는다.
코딩 컨벤션뿐만 아니라 행동 컨벤션도 하네스에 포함된다.
결국 사람이 운전대를 잡아야 한다
AI가 코드의 90%를 짜줬지만, 나머지 10%에서 앱의 품질이 결정됐다. 그리고 그 10%는 "어디가 진짜 문제인지 판단하는 능력"이었다.
Claude는 도구다. 좋은 도구를 잘 쓰려면 하네스가 필요하다. 고삐 없는 말은 빠르지만, 어디로 갈지 모른다.
설치
GitHub Releases에서 DMG를 다운로드하면 된다.
# 또는 직접 빌드
git clone https://github.com/HongChaeMin/token-garden-app.git
cd token-garden-app
Xcode로 열어서 Cmd+R.
macOS 14.0 이상이 필요하다.
cctoken에서 Token Garden까지
cctoken은 "지금 얼마나 썼지?"에 답하는 도구였다. Token Garden은 "언제, 어디서, 얼마나 썼는지"를 보여주는 대시보드다.
터미널에서 시작한 호기심이 메뉴바에 정착했다. 식물이 자라는 걸 보면서 코딩하면 토큰을 아껴야겠다는 생각이 든다. 아니, 사실 더 쓰게 된다. 잔디를 채우고 싶으니까.