앱 하나와 몇 가지 스킬을 만들었다. 개발 속도는 두 배로, 토큰 소모는 절반으로 줄었다.

나는 프로덕트 시큐리티를 주력으로 하는 회사의 CTO이고, 0살과 2살 두 아들을 키우고 있다. 낮에는 회의가 잦고, 저녁은 아내가 쉴 수 있게 애들 봐야 한다. 그리고 종종 이상한 시간에 보안 이슈가 터진다. react2shell, litellm, tanstack 대상의 서플라이체인 어택 등등. 그럴 때마다 업무 시간이 아니더라도 고객사 시스템이 노출되지는 않았는지 확인하고 연락해야 한다. 삶이 미친 듯이 바쁘다.

그래서 나만의 메모 앱, 지금의 나홀로를 만들었다. 고객사나 팀에서 온 짧은 아이디어나 피드백을 떠오른 순간 바로 던져 넣을 곳이 필요했다. 사실상 나 혼자 떠드는 간단한 스레드 기반 채팅 앱이었다. 메모를 남기고, 여유가 생겼을 때 쌓인 것들을 훑어보고, 하나씩 처리하는 식이었다.

NAHOLO OPERATION LOGS/NOTES

#222 Redact ai transcripts

Junyoung
Users might be afraid to use the app because it uploads full transcripts. Need to remove all messages from both users and agents
Some tool usages are exposing file path. Should be better to redact all string other than we need for calculating stats.
Type a message…

그러다 이 앱에 대한 피드백도 이 앱에서 관리하기 시작했다. 앱이 거슬릴 때마다 메모를 남겼고, 시간이 생길 때마다 하나씩 고쳤다. 그러다 문득, MCP 서버를 붙여서 이 아이디어들을 곧바로 VSCode로 끌어올 수 있다면, 매번 컨텍스트 스위칭 없이 바로 받아와서 에이전트에게 맡겨둘 수 있을 거란 생각이 들었다. 그래서 작은 CLI 앱을 만들어 MCP 서버와 커맨드로 웹앱과 연계할 수 있게 만들었다.

여기까지는 잘 됐다. 하지만 계속해서 병목 지점이 보였다.

우선 Claude Code의 /plan을 쓸 때, 매 세션이 도박처럼 느껴졌다. 컨텍스트 크기와 정보의 밀도에 따라 세션의 퀄리티가 확연히 달라지는 게 느껴졌다. 컨텍스트가 너무 많으면 에이전트가 할루시네이션을 일으키기 시작했고, 너무 상세하면 몇 줄 고치자고 긴 글을 읽어야 했다. 이를 피하기 위해 적절한 크기와 밀도를 찾으려 했지만, 쉽지 않았다. 첫 컨텍스트를 준비하는 것 자체가 일이 된 기분이었다.

그러다 갑자기 생각났다. 거친 아이디어든 단편적인 피드백이든 장대한 계획이든, 크기와 밀도는 다르지만, 각각 유저를 고통스럽게 하는 근본적인 무언가가 있다는 것을. 그래서 에이전트에게 티켓에 숨겨진 페인포인트를 도출해내도록 유도했다. "이걸 만들고 싶다, 그런데 이건 왜 필요한 걸까?" 결과는 매우 좋았다. 에이전트가 컨텍스트 사이즈가 어떻든 요약 하나는 기가 막히게 잘하는 만큼, 이를 살짝 틀어서 페인포인트로 가공하는 건 정말 쉬운 일이었다. 그래서 이걸 모든 티켓의 베이스라인 컨텍스트로 삼기로 했다.

다음으로는 개념 작업, 즉 어떤 해법이 유효할지 콘셉트를 에이전트에게 만들게 해보았다. 하지만 많이 불안정했다. 뻔한 태스크면 큰 문제 없었지만, 조금이라도 복잡한 접근이 필요하면 추상적인 계획은 그저 공상에 불과하여, 구체 계획을 짜거나 실제 구현하다 보면 결정을 번복하는 일이 잦았다. 그래서 콘셉트에서 한발짝 더 들어간, 현실성 있는 아이디어가 될 수 있는 설계를 해보기로 했다. 그때 아키텍처 결정 기록(Architecture decision record)이란 개념을 차용하여 제한사항(Constraints)을 만들어보려 했다.

하지만 또 다른 문제가 수면위로 드러났다. 주로 작은 태스크의 경우, 에이전트가 억지로 제한사항을 쥐어짜는 경우가 많았고, 이런 세세한 제한사항들은 다음 계획과 코드 변경에 오히려 방해가 되었다. 그래서 결정들의 정보밀도에 제한을 뒀다. 엄격한 길이 제한을 두어, 단 두 문장, 하나는 그게 무엇인지, 하나는 왜 필요한지를 알려주게 했고, 그리고 파일 이름이나 실제 변경하는 코드는 그게 유일한 해결책이 아닌 이상 쓰는 걸 삼가도록 하는 등 다양한 대책을 취해보았다. 이에 에이전트가 아키텍처 계획을 쥐어짜낼 수 없게 되어버렸고, 덕분에 컨텍스트 크기가 어떻든 일정한 정보밀도를 유지하게 되었다. 물론 적절한 컨텍스트 크기와 밀도는 해당 토픽에 대한 내 이해도에 따라 요동쳤기에 이상할 때도 있었지만, 상당히 제어할 수 있게 되었다. 제한사항이 너무 강하면 여전히 걷어내면 됐고, 모호하면 다시 파고들면 되었다. 그렇게 파고들면 내가 몰랐던 개념을 배우거나, 에이전트가 실수한 걸 빨리 알아차릴 수 있었고, 구체 계획과 코드 변경은 아직이기에, 이터레이션도 가볍게 돌아갔다. 태스크 크기에 따라 다르지만 대체로 영어로 200~400 단어 이내에 얼개가 한눈에 보여서 리뷰도 스트레스 없이 아주 쉽게 할 수 있게 되었다.

이젠 얼개를 잡았으니 실제 구현 계획을 세우게 했다. 태스크를 커밋 하나 단위로 쪼개 구현할 수 있게 했다. 결과는 그런대로 괜찮았다. 대체로 4~5파일 이내의 변경으로 쪼개주니 코드리뷰가 편했다. 하지만 여전히 삐걱거리는 부분이 보였다. 앞서 말한 것과 비슷한 정보 밀도의 문제였다. 종종 에이전트는 구체 계획을 거의 코드 변경 수준으로 작성했고, 똑같은 내용을 다시 읽음으로 발생하는 부담은 점점 무시할 수 없게 되어갔다. 그래서 차라리 바로 구현시키고 코드를 쓰게 만들면 어떨까 생각도 해봤다. 하지만 계획을 건너뛰면 다시 바이브 코딩으로 돌아갈 뿐이었고, 에이전트와 계속 씨름할 수밖에 없었다.

그래서 생각했다. 어떻게 해야 좋은 정보 밀도를 추려낼 수 있을까? 내가 찾아낸 적절한 지점은 모듈 레벨의 계약이었다. 스킬로 하여금 모듈마다 외부로 노출된 함수와 인터페이스의 변경만 묘사할 수 있게, 그 변경을 문장 한줄로 강제했다. 그렇게 순수하게 모듈간의 책임 범위만을 쉽게 검토할 수 있게 되었다.

물론 어떤 변경은 한 문장으로는 부족했다. 그래서 살짝 에이전트에게 숨 쉴 수 있는 공간을 제공했다. 변경이 계약에 관한 것이면, 그 계약 변경만 부각하는 의사코드(pseudocode)를 쓴다. 내부 흐름이면 아스키 다이어그램으로 스케치한다. UI도 마찬가지로, 아스키 와이어프레임을 그린다. 아스키로 화면을 어떻게 목업하냐고 물을 수도 있지만, 태스크가 커밋 하나 크기가 되고 나면 건드릴 수 있는 표면적이 아주 작아서, 러프한 와이어프레임으로 충분하고도 남는다.

모듈 레벨 계약 변경에 대한 의사코드

// src/lib/operation-search.ts
type OperationSearchConditions = {
  // ...
  exactOperationNumber: number | null // was: operationNumber: string | null
}

function toggleSearchToken(
  search: string,
  kind: 'label' | 'assignee',
  value: string,
): string // new — append the token when absent, strip it when present

함수 흐름 프리뷰

active polar sub? ──yes──► usedSeats > seat cap ? seats-exceeded : active
        └────────no──────► usedSeats > 1        ? suspended      : free

레이아웃 변경 목업 #1 - 오퍼레이션 페이지 구조 변경(태스크 1)

before                       after
┌───────────┬───────────┐    index route             detail route
│ list panel│ detail /  │    ┌──────────┐           ┌─────────────────┐
│ (always)  │ "select…" │ →  │nav │list │ row click │nav │bread crumb │
│           │           │    │    │     │ ────────► │    │op detail   │
└───────────┴───────────┘    └──────────┘           └─────────────────┘

레이아웃 변경 목업 #2 - 오퍼레이션 페이지 네비게이션 (태스크 2)

┌──────────────────┐
│ ⬡ Operations   ✎ │
│ All operations   │  → ?search cleared
│ ASSIGNEES        │
│  (RO) rokt33r(me)│  → ?search=assignee:rokt33r
│  (JC) junyoung   │
│ LABELS           │
│  «bug»           │  → ?search=label:bug
│  «infra»         │
└──────────────────┘

태스크 1에서 nav로 표현된 부분이 다음 태스크에서 세세하게 묘사된다.

레이아웃 변경 목업 #3 - 오퍼레이션 리스트 아이템 (태스크 3)

◉  #275  Renew operations page      «infra»        (RO)(JC)   11:38
▲status ▲num ▲title (truncates)     ▲label pills   ▲avatars   ▲date

태스크 1에서 list로 표현된 부분이 다음 태스크에서 세세하게 묘사된다.

그렇게 개발이 쉬워지나 싶었는데, 새로운 문제를 발견했다. 에이전트는 작은 조각부터 만들고 맨 마지막에 전부 엮는 경향이 강하다는 것을. 지금 하는 작업이 나중에 어떻게 쓰이는지 파악하려면 모든 태스크를 다 읽어야 했다. 더 나쁜 건, UI에서는 에이전트가 부속 컴포넌트부터 만드는 바람에 페이지 전체가 존재하기 전까지는 그것들을 렌더링해 볼 수조차 없었다. 그때 깨달았다. 에이전트는 사람처럼 일하지 않는다는 걸. 에이전트는 모든 걸 한 방에 끝내도록 최적화되어 있지만, 우리는 그렇지 않다. 사람이 만들 때는 큰 형태에서 시작해서 필요에 따라 디테일을 채워 간다. 그림 그리듯이 말이다. 러프 스케치를 먼저 하고, 그다음 디테일을 더하고, 또 더해, 묘사가 만족스러워질 때까지.

그래서 순서를 뒤집었다. 우선 도메인 레벨의 스키마와 인터페이스부터. 그다음 큰 덩어리들, 엔드포인트 하나나 페이지 하나를 플레이스홀더 코멘트 혹은 스켈레톤 컴포넌트로 세워 둔다. 그다음 천천히 디테일을 채운다. 그렇게 큰 덩어리의 구현을 검토하면서 동시에 다음에 뭐가 일어날지 파악할 수 있게 되었다. 그렇게 모든 태스크를 미리 다 읽을 필요가 없어졌다. 코드 변경 내용이 다음 태스크의 프리뷰가 된다. 첫 번째 태스크를 완료하면 이미 두 번째 태스크에 뭐가 일어날 건지 알게 되고, 더 빠르게 리뷰할 수 있다. 더 이상 이전 태스크로 돌아가 고칠 필요가 없어졌다. 이에 에이전트가 실수를 해도 더 빨리 눈치채고, 실제 코드를 쓰기도 전에 고칠 수 있게 되었다.

마지막으로 에이전트가 코드를 구현할 때 숨통을 하나 더 트여주었다. 태스크마다 작은 목표가 있고, 목표 달성을 위해 계획엔 미처 고려되지 못한 일을 해야 할 경우가 있었다. 그래서 에이전트에게 즉흥적으로 대처하되, 그걸 계획 진행 기록에 강조되게 만들었다. 계획과 같은 포맷으로 하지만 계획에서 벗어났다는 개념으로 Deviations(편차)라 칭했다. 반대로 에이전트뿐 아니라 내가 만든 Deviations도 기록하게 만들었다. 구현 중에 생각이 바뀌면 가볍게 계약을 변경할 수 있게. 그렇게 에이전트에겐 두 가지의 Deviations을 기록하게 했다. 에이전트가 실수했거나, 내가 실수했거나. 두 경우를 분석하며 스스로 계획을 어떻게 검토해야 하는지 더욱 갈고닦을 수 있었고, 개발 경험은 더욱 쾌적해졌다.

이 전체 흐름이 자리 잡고 나니, 나는 컨텍스트 상태가 어떻든 마음 편히 개발을 할 수 있게 되었다. 이제 에이전트는 컨텍스트가 아무리 커져도 고정된 정보밀도로 일하기 때문에, 큰 태스크라도 여전히 5분짜리 리뷰 창 안에 들어온다. 내가 할 일은 그저 꾸준히 5분짜리 리뷰를 소화해 나가는 것뿐이었다. 에이전트가 내뱉는 텍스트양이 제한적이라, 사실상 번아웃이 올 수 없다. 티켓을 나눠야 하는 경우도 쉽게 파악할 수 있게 되었다. 보통 제한사항이 20개, 태스크가 10개를 넘을 때, 태스크를 나누니 정말 편하게 진행할 수 있었다. 그렇게 나도, 에이전트도 둘 다 제정신을 유지한 채 철저히 아이디어부터 계획, 코드까지 리뷰하며, 할루시네이션 없이, 번아웃 없이 개발을 이어나갈 수 있게 되었다.

NAHOLO OPERATION STATS
Approx. cost$52.98
Transcripts1
Messages708 (298 user / 410 asst)
Transcript time2h 6m
claude-opus-4-710.53M (weighted, in 614 · out 216.4k · c5m 0 · c1h 2.39M · r 46.64M)
$52.65
claude-sonnet-4-6111.9k (weighted, in 12 · out 1.7k · c5m 0 · c1h 39.9k · r 236.4k)
$0.34
Skills
splashclaude-opus-4-7 4.8M
opordclaude-opus-4-7 1.3M
exfilclaude-opus-4-7 812.1k
warnoclaude-opus-4-7 212.9k
infilclaude-sonnet-4-6 111.9k
fobclaude-opus-4-7 108.9k
reconclaude-opus-4-7 81k
(none)claude-opus-4-7 3.21M
weighted tokens attributed to each (skill, model)
Tools
258
Edit 105 · Bash 84 · Read 60 · Write 5 · Skill 2 · Agent 1 · AskUserQuestion 1

여기에 한걸음 더 나아가서 각 단계의 토큰 비용도 측정하기 시작했다. 주관적인 품질로 평가하는 게 아니라 객관적인 숫자로도 평가해보고 싶었다. 이 사이클을 만들고 수백 개의 티켓을 다루니 평균 소모량은 다음과 같았다.

  • 개념 및 제한사항 결정: 7.3%
  • 태스크 자르기: 17.1%
  • 코드 변경: 65.4%
  • 기타: 10.2%

보고나니 이 워크플로우가 왜 쾌적한지를 깨닫게 되었다. 제한사항을 결정하는 7.3%나 태스크 나누는 17.1% 구간에서 이터레이션을 돌 경우, 번복 비용이 태스크를 만들기 전이면 1/9, 코드를 짜기 전이면 1/3 수준으로 토큰 소모가 적다는 것을. 토큰 소모가 적으니 컨텍스트를 크게 더럽히지 않고, 에이전트도 현저히 할루시네이션이 생기는 빈도가 줄었다. 최종적으로 내 토큰 사용량도 크게 줄었다. 번복 비용도 싸고 빈도도 현저하게 줄어버리니, 업무 시간에는 에이전트 세 개를 동시에, 각각 다른 프로젝트와 워크트리에서 돌리는데도 여전히 내 100달러짜리 Claude x5 플랜의 30% 이상 쓰기가 어려워졌다. (작년 육아 휴직 전까진 x20로도 매번 탕진하다, 소넷까지 썼지만.)

그렇게 나는 다시 코딩을 즐기게 되었다. CTO로서 산더미 같은 회의와 고객사 보안 사고들을 처리하면서도, 나는 여전히 내 코드를 꾸준히, 전부 리뷰를 거쳐 배포한다. 그리고 이 사이드 프로젝트는 퇴근 후, 두 아이를 돌보면서 틈이 날 때마다 리뷰하고 계획과 코드를 깎아나간다. 여전히 에이전트가 실수할 때도 있지만, 모든 단계에 엄격한 제한이 걸려 있어서, 문제가 걷잡을 수 없이 커지기 전에 되돌릴 수 있었다. 게다가 워크플로우를 따르다 보면 에이전트가 그 세션 안에서 스스로 길들여져, 워크플로우에 벗어난 행동을 해도 다시 원궤도로 쉽게 돌릴 수 있었다. 물론 컨텍스트 사이즈가 너무 커져서 할루시네이션을 거듭하기 시작해도 큰 문제 없다. 애초에 티켓의 전체 컨텍스트를 계획 파일 하나로 관리하기에, /compact 없이 새로운 에이전트를 바로 시작하면 그만이다. 새로운 에이전트도 워크플로우를 이해하고 있기에, 그냥 이전 에이전트가 만든 파일을 한번 로드하는 것만으로 그대로 컨텍스트를 이어갈 수 있다. 더욱이 /compact를 쓰지 않기에, /compact를 기다리는 시간도 사라졌고, 이상하게 요약돼서(중요한 부분은 빼먹고 쓸데없는 부분만 남기는 경우) 할루시네이션이 생기는 경우도 완전히 사라졌다.

이번 달엔 드디어 팀을 설득해서 이걸 쓰게 했다. 다들 회의적이었다. 웹 앱에 스킬셋, 그리고 에이전트에게 도구와 커맨드를 제공하는 CLI까지 합쳐진 솔루션이라, 대체 이게 뭔지 한 문장으로 말하기가 어려워 이해시키는 데 어려움이 많았다. 그래도 한번 써보더니 그들도 달라짐을 즉시 체감했다. 개발 속도가 2~3배가 되었다. 에이전트의 실수가 줄어들고, 에이전트의 계획과 코드리뷰에 소모되는 시간이 현저하게 줄고, 스트레스 자체도 극적으로 감소하니, 티켓에 둔 버퍼 스케줄을 쓰는 경우가 현저히 줄었다. 5일짜리 태스크가 하루 반 만에 디플로이 되었고, 팀의 토큰 사용량은 태스크별로 30에서 60%까지 감소하였고, 특히 큰 태스크일수록 토큰 소모량 변화가 극적이었다.

팀원이 칭찬해준 또 다른 점은 어떻게 쓰는지 미리 배울 필요가 거의 없다는 것이었다. 흐름도 단순하고 에이전트가 뭘 해야 하는지 계속 안내해 준다. 이른바 "AI 히어로"들은 수십 가지 경우의 수를 위해, 수십 가지 스킬을 제공하지만, 나는 단지 다섯 개의 핵심 스킬만 준다. 그리고 그 다섯 개가 거의 모든 걸 커버한다. 러프한 아이디어나 고객의 불평 코멘트에서 시작해, 기능 구현까지 한방에 처리할 수 있게 되었다.

# 핵심 스킬
/infil   (Infiltration, 투입) 웹 앱에서 컨텍스트 내려받기
/warno   (WARNING ORDER, 준비명령) 콘셉트와 제한사항 확정
/opord   (OPERATION ORDER, 임무명령) 태스크 자르기
/splash  (포격 혹은 폭격시 쓰는 말, splash out/ splash over) 각 태스크 구현
/exfil   (Exfiltration, 철수) 웹 앱으로 컨텍스트 올려보내기

# 추가 스킬
/recon   계획 변경 초안 작성
/chop    이슈 쪼개기
/fob     (Forward Operating Base, 전초기지) 코드베이스에서 바로 티켓 생성
/frago   (FRAGMENTARY ORDER, 단편명령) 제한사항과 태스크를 변경

그런데 왜 군대 용어를 쓰는가?

  1. 그냥 갑자기 군대 생각나서. (전혀 그립지는 않음.)
  2. 콜 오브 듀티를 좋아해서. 연말에 나올 "모던워페어 4"가 너무 기대됨.
  3. 미군 military decision making process(MDMP)가 힌트를 이것저것 많이 줘서.

매핑도 이렇게 됐다.

  • 이슈 → 오퍼레이션(operation)
  • 멤버 → 오퍼레이터(operator)
  • 콘셉트 및 제한사항 → 준비명령(WARNO)
  • 태스크 → 임무명령(OPORD)
  • 에픽 → 캠페인(campaign) (만드는 중)
  • 마일스톤 → 랠리포인트(rallypoint) (만드는 중)
  • 계획 변경 → 단편명령(FRAGO)

덤으로, 팀이 이제 회의할 때 택티컬하게 말하기 시작해서 좀 웃기게 되었다. "이번 작전, 준비명령 준비했는데 리뷰 좀 해줘."

여튼 AI 코딩에 지쳤지만, 바이브코딩에 타협하지 않고, 다시 만드는 재미를 느끼고 싶다면, 한번 써 보길. 솔로 사용자에게는 무료다. 팀으로도 쓸 수 있고, 협업 기능도 더 강화될 예정이다. 그리고 전부 오픈 소스이니, GitHub 저장소에 들러 스타도 누르고 포크도 해 주면 좋겠다.

그럼 긴 글 읽어줘서 감사합니닷. 다들 좋은 하루 되시길.