Claude Code로 Claude Code 도구 만들기
재귀적 즐거움
Claude Code를 분석하는 도구를 Claude Code로 만들면 어떤 일이 벌어질까?
"내가 쓰는 도구를 분석하는 도구를 그 도구로 만든다"는 문장은 처음엔 말장난 같지만, 실제로 해보면 묘한 피드백 루프에 빠집니다. Claude Code에게 "네가 얼마나 호출됐는지 세는 코드 만들어줘"라고 부탁하면, 그 대화 자체가 또 하나의 호출 기록이 되니까요.
ccfg는 이런 재귀적 호기심에서 시작한 프로젝트입니다. Claude Code의 설정 파일들을 시각화하고, 에이전트와 도구의 사용량을 게임화된 랭킹으로 보여주는 TUI 도구입니다. 이 글에서는 ccfg를 만들면서 경험한 기술적 선택들, Bubbletea라는 Go TUI 프레임워크, 그리고 AI와 함께 코딩한다는 것의 실체에 대해 이야기합니다.
ccfg가 뭔가요
Claude Code를 쓰다 보면 자연스러운 궁금증이 생깁니다. 내 설정이 어디에 있는지, 어떤 도구를 가장 많이 쓰는지, 내 사용 패턴은 어떤지. 문제는 Claude Code의 설정이 8개 이상의 파일에 분산되어 있다는 겁니다. 시스템 관리자 설정, 사용자 전역 설정, 프로젝트별 설정이 제각각 다른 경로에 존재하고, 우선순위도 다릅니다.
ccfg는 이 문제를 세 가지 방식으로 해결합니다:
- 설정 시각화 — 분산된 설정 파일들을 트리 뷰로 한눈에 보여줍니다. 파일이 존재하는지, 병합했을 때 최종 값이 뭔지 확인할 수 있습니다.
- 사용량 통계 — transcript JSONL 파일을 파싱해서 어떤 도구와 에이전트를 얼마나 호출했는지 집계합니다.
- 게임화 랭킹 — 단순 숫자 나열이 아니라 SSS~F 등급으로 변환합니다. 프로그레스 바와 네온 컬러 배지로 아케이드 느낌을 줍니다.
기술 스택은 Go 1.25, Bubbletea(TUI 프레임워크), Lipgloss(스타일링), Glamour(마크다운 렌더링)입니다. 현재 약 4,800줄의 Go 코드와 49개의 커밋으로 이루어져 있으며, 오픈소스 공개를 목표로 개발 중입니다.
Go + Bubbletea: Elm Architecture로 TUI 만들기
TUI를 만드는 방법은 여러 가지가 있습니다. ncurses 스타일의 절차적 접근, 또는 React처럼 선언적으로 만들거나. Bubbletea는 후자에 가깝습니다. Elm Architecture를 Go에 옮겨놓은 것 같은 구조입니다.
핵심은 세 가지 함수입니다:
- Init — 초기 상태와 첫 명령을 반환
- Update — 메시지(이벤트)를 받아 상태를 갱신
- View — 현재 상태를 문자열로 렌더링
// Model은 TUI 전체 상태를 관리한다.
type Model struct {
scan *model.ScanResult
tree TreeModel
preview PreviewModel
focus Pane
width int
height int
ready bool
searchMode bool
rankingMode bool
ranking RankingModel
}
func (m Model) Init() tea.Cmd { return nil }
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.WindowSizeMsg:
m.width = msg.Width
m.height = msg.Height
m.ready = true
m.updateLayout()
return m, nil
case tea.KeyMsg:
// 키보드 입력 처리...
}
return m, nil
}
func (m Model) View() string {
if !m.ready {
return "로딩 중..."
}
header := m.renderHeader()
treeView := m.tree.View(treeW, m.focus == PaneTree)
previewView := m.preview.View(previewW, m.focus == PanePreview)
main := lipgloss.JoinHorizontal(lipgloss.Top, treeView, previewView)
return lipgloss.JoinVertical(lipgloss.Left, header, main, footer)
}상태가 불변이고 Update에서만 변경되기 때문에, 복잡한 UI 상태를 다룰 때도 "지금 이 상태에서 이 메시지를 받으면 어떻게 되는지"만 생각하면 됩니다. React의 useReducer와 비슷한 사고 흐름입니다.
Lipgloss는 스타일링 라이브러리인데, CSS-in-JS를 Go로 가져온 느낌입니다. 선언적으로 Border, Padding, Foreground 색상을 체이닝할 수 있습니다:
// 레트로 아케이드 색상 팔레트
var (
colorYellow = lipgloss.Color("#FFD700") // 골드 옐로우
colorOrange = lipgloss.Color("#FF8C00") // 오렌지
colorGreen = lipgloss.Color("#39FF14") // 네온 그린
colorCyan = lipgloss.Color("#00FFFF") // 시안
colorMagenta = lipgloss.Color("#FF00FF") // 마젠타
// 패널 테두리 (포커스) — Double Line
panelFocusedStyle = lipgloss.NewStyle().
Border(lipgloss.DoubleBorder()).
BorderForeground(colorOrange).
Padding(0, 1)
)다만 Lipgloss와의 싸움도 있었습니다. 레이아웃 계산이 생각보다 까다로웠는데, 특히 패널 높이 고정과 MaxWidth 줄바꿈 이슈가 반복적으로 발생했습니다. 터미널 크기가 바뀔 때 패널 높이가 음수가 되거나, 긴 텍스트가 패널 밖으로 삐져나가는 버그들이었습니다. GetHorizontalFrameSize()와 GetHorizontalBorderSize()의 차이를 정확히 이해하기까지 여러 커밋이 필요했습니다.
게임화 설계: SSS 랭크의 비밀
도구 사용량을 단순히 "Read: 1,234회, Bash: 567회"로 보여줄 수도 있었습니다. 하지만 숫자만 나열하면 금방 흥미를 잃습니다. 게임처럼 등급을 부여하면 "다음 등급까지 얼마나 남았지?"라는 자연스러운 동기부여가 생깁니다.
핵심 알고리즘은 로그 스케일 기반입니다. 도구 사용량은 전형적인 멱법칙(power law) 분포를 따르기 때문입니다. Read 같은 도구는 수천 번 호출되지만, 특수 도구는 한두 번에 그칩니다. 단순 선형 비율로는 대부분이 F등급에 몰립니다.
func logScore(count, maxCount int) float64 {
if maxCount <= 0 {
return 0
}
return math.Log(float64(count)+1) / math.Log(float64(maxCount)+1)
}
func gradeFromScore(score float64) Grade {
switch {
case score >= 0.95: return GradeSSS
case score >= 0.80: return GradeSS
case score >= 0.65: return GradeS
case score >= 0.50: return GradeA
case score >= 0.35: return GradeB
case score >= 0.20: return GradeC
case score >= 0.10: return GradeD
default: return GradeF
}
}log(count+1) / log(max+1) 공식으로 0~1 사이 점수를 만들고, 이를 8단계 등급으로 매핑합니다. +1은 count가 0일 때 log(0)이 되는 걸 방지합니다. 이 방식의 장점은 사용량이 적은 도구도 의미 있는 등급을 받을 수 있다는 점입니다.
시각적으로는 레트로 아케이드 테마를 적용했습니다. SSS는 마젠타, SS는 골드, S는 오렌지. 등급별로 █ 프로그레스 바의 색상이 바뀌고, 배지도 달라집니다. 터미널에서 네온 컬러가 빛나는 걸 보면 자체적인 보상 효과가 있습니다.
Claude Code와 함께 개발하기
ccfg의 약 49개 커밋 대부분은 Claude Code와의 대화에서 나왔습니다. 완전한 자동 생성이 아니라 지속적인 대화형 페어 프로그래밍이었습니다.
잘 작동한 것들
빠른 프로토타이핑이 가장 효과적이었습니다. "트리 뷰 패널을 이중 테두리로 만들어줘", "키보드 네비게이션 추가해줘" 같은 요청에 작동하는 코드가 바로 나옵니다. Bubbletea의 Init/Update/View 패턴이 명확해서 AI가 올바른 위치에 코드를 삽입하기 좋았습니다.
보일러플레이트 제거도 큰 도움이었습니다. Go의 에러 핸들링 (if err != nil 반복), JSONL 한 줄씩 읽기, JSON 언마셜링 같은 반복 코드를 AI가 빠르게 생성했습니다. transcript에서 도구 사용량을 추출하는 extractToolsFromLine 함수 같은 경우, Claude Code 자체의 내부 데이터 포맷을 다루는 코드를 Claude Code가 작성하는 아이러니한 상황이 벌어졌습니다.
func extractToolsFromLine(line []byte, counts map[string]int) {
// Claude Code transcript의 assistant 메시지에서
// tool_use 블록을 찾아 도구별 호출 횟수를 센다
if !bytes.Contains(line, []byte(`"tool_use"`)) {
return
}
var cl claudeCodeLine
if err := json.Unmarshal(line, &cl); err != nil || cl.Type != "assistant" {
return
}
var blocks []contentBlock
if err := json.Unmarshal(cl.Message.Content, &blocks); err != nil {
return
}
for _, block := range blocks {
if block.Type == "tool_use" && block.Name != "" {
counts[block.Name]++
}
}
}리팩토링 자동화도 빈번했습니다. "이 파일에서 랭킹 관련 로직을 별도 파일로 분리해줘" 같은 요청으로 구조를 개선했습니다. 커밋 히스토리를 보면 feat → fix → refactor 사이클이 반복되는 패턴이 보입니다.
한계와 주의점
맥락 손실이 가장 큰 문제였습니다. 긴 세션에서 초기 설계 의도가 점점 희미해지면서 이전에 결정한 구조와 어긋나는 코드가 나오기 시작합니다. 이걸 해결하기 위해 CLAUDE.md에 프로젝트 컨벤션을 명시하고, progress.txt로 현재 진행 상황을 추적했습니다. "다음 컨텍스트가 이어받을 수 있게" 상태를 기록하는 습관이 중요했습니다.
과도한 의존은 미묘한 위험입니다. AI가 생성한 코드가 컴파일되고 테스트도 통과하면 "되겠지" 하고 넘어가기 쉽습니다. 하지만 미묘한 로직 오류나 비효율적인 구현이 숨어 있을 수 있습니다. 특히 Lipgloss 레이아웃 계산에서 Width와 MaxWidth의 차이, 프레임 사이즈 계산 같은 부분은 AI도 자주 틀렸습니다.
창의적 결정은 여전히 인간의 몫입니다. "사용량을 게임화하자"는 아이디어, 로그 스케일을 선택한 이유, 레트로 아케이드 테마라는 디자인 방향은 모두 직접 결정했습니다. AI는 구현을 도왔지만, "무엇을 만들지"는 제가 정해야 했습니다.
복잡한 상태 버그 디버깅도 어려웠습니다. TUI에서 여러 모드(일반/검색/병합/랭킹)가 중첩되고, 터미널 크기 변경과 결합되면 재현하기 어려운 버그가 발생합니다. 이런 버그는 "이런 에러가 났어"라고 말하기도 어렵고, AI에게 설명하기는 더 어렵습니다.
실전 워크플로우 패턴
개발 과정은 대체로 이런 사이클을 반복했습니다:
- 기능 설명 ("랭킹 뷰에 탭 네비게이션 추가해줘")
- 코드 생성 (AI가 작성)
- 실행 및 확인 (터미널에서 직접 확인)
- 피드백 ("탭 바가 너무 넓어, 간격 줄여줘")
- 수정 (2~5회 반복)
한 기능당 평균 3~4번의 피드백 루프를 거쳤습니다. 완벽한 코드가 한 번에 나오는 경우는 드물었지만, 피드백을 주면 빠르게 수렴합니다.
배운 것들
작은 커밋의 힘. 49개의 커밋은 개발 과정의 타임랩스 같습니다. 각 커밋이 하나의 논리적 변경을 담고 있으면 나중에 "왜 이렇게 했지?"를 추적하기 쉽습니다. AI와 작업할 때 특히 중요한데, 큰 변경을 한 번에 하면 문제가 생겼을 때 어느 부분이 잘못됐는지 찾기 어렵기 때문입니다.
TUI 개발만의 독특한 디버깅. 웹이나 모바일과 달리 터미널은 크기가 유동적이고, ANSI 이스케이프 시퀀스가 레이아웃 계산을 복잡하게 만듭니다. lipgloss.Width()는 시각적 너비를 계산하지만, len()은 바이트 길이를 반환합니다. 이 차이를 간과하면 한글이 포함된 문자열에서 레이아웃이 깨집니다.
transcript 데이터 마이닝의 가치. Claude Code가 남기는 JSONL transcript는 사용 패턴의 보물창고입니다. 어떤 도구를 가장 많이 쓰는지, 어떤 에이전트를 호출하는지, 세션당 평균 턴 수는 얼마인지. 이 데이터를 분석하면 자신의 AI 코딩 습관을 객관적으로 볼 수 있습니다.
"무엇을 만들지" 결정하는 능력. AI가 구현 속도를 비약적으로 올려주면서, 병목은 "코드를 얼마나 빨리 짜는가"에서 "무엇을 만들지 얼마나 잘 결정하는가"로 이동했습니다. ccfg의 게임화 아이디어, 로그 스케일 선택, 레트로 테마 같은 결정들이 프로젝트의 성격을 만들었고, 이건 AI에게 맡길 수 없는 영역이었습니다.
마무리
ccfg를 만들면서 가장 많이 한 생각은 이것이었습니다.
AI는 더 나은 연필이다. 연필이 좋아져도 무엇을 그릴지는 사람이 정한다. 다만 이전에는 스케치에 며칠 걸리던 것이 이제 몇 시간이면 됩니다. 더 많이 시도하고, 더 빨리 실패하고, 더 빨리 배울 수 있습니다.
ccfg는 곧 오픈소스로 공개할 예정입니다. Claude Code 사용자라면 자신의 사용 패턴을 시각화하는 재미를 느껴볼 수 있을 겁니다.
마지막으로 하나만 말하자면 — 자신만의 도구를 만들어보세요. 남이 만든 도구를 잘 쓰는 것도 중요하지만, 자기 워크플로우에 맞는 도구를 직접 만들면 개발 자체에 대한 이해가 깊어집니다. AI가 구현을 도와주는 시대이니, 아이디어만 있으면 충분합니다.