Claude Code에 "add user authentication"이라고 입력하는 개발자는 매번 다른 결과를 받는다. JWT일 수도 있고, 세션 쿠키일 수도 있고, 리프레시 토큰과 PKCE가 포함된 완전한 OAuth2 플로우일 수도 있다. 에이전트는 당신이 뭘 원하는지 모른다. 당신이 알려주지 않았으니까. 방향을 줬을 뿐, 목적지를 주지 않았다.
내가 보기에 Claude Code에서 일관된, 출시 가능한 결과물을 얻는 개발자들은 하나의 습관을 공유한다: 에이전트에게 작업을 넘기기 전에 스펙을 쓴다. 소설이 아니다. 컨텍스트 세 줄짜리 Jira 티켓도 아니다. 누구든 코드를 한 줄이라도 쓰기 전에 "완료"가 어떤 모습인지 정의하는 구체적인 문서다.
새로운 지혜가 아니다. 스펙 기반 개발은 AI 이전 수십 년 전부터 있었다. 하지만 에이전트에서는 스펙을 건너뛰는 비용이 더 크고 스펙을 쓰는 이득이 더 크다. 인간 개발자는 구현 도중에 멈추고 "잠깐, 비밀번호 인증 말한 거야 SSO 말한 거야?"라고 물을 수 있다. 에이전트는 조용히 하나를 선택하고 계속 진행한다. 알아챘을 때는 이미 잘못된 걸 만들어놨고, 버려야 할 코드를 리뷰하는 데 20분을 쓴 뒤다.
이 글에서는 내가 Claude Code와 함께 매일 사용하는 스펙 기반 라이프사이클을 다룬다: 에이전트가 실행할 수 있는 스펙 작성법, 오해를 초기에 잡아내는 plan-before-code 체크포인트, 그리고 "컴파일 된다"보다 엄격한 검증 프로토콜.
"그냥 만들어"가 에이전트에서 실패하는 이유
실패 모드를 구체적으로 보자. Claude Code에 모호한 지시를 내리면 세 가지가 잘못된다:
무언의 가정. 에이전트는 스펙의 모든 빈틈을 자체 가정으로 채운다. 그 가정이 합리적일 때도 있고 아닐 때도 있다. 결과물을 읽기 전까지는 어느 쪽인지 모른다. 모호한 지시에서는 스펙을 쓰는 데 들었을 노력보다 더 꼼꼼하게 결과물을 읽게 된다.
재현 불가능한 결과. 같은 모호한 프롬프트를 두 번 실행하면 두 가지 다른 구현을 얻는다. 변수명이나 포매팅만 다른 게 아니다. 아키텍처 결정이 다르다. 라이브러리가 다르다. 에러 핸들링 전략이 다르다. 결과를 재현할 수 없으면 그 주위에 신뢰할 수 있는 프로세스를 구축할 수 없다.
리뷰가 병목이 된다. 에이전트가 모든 결정을 내리면, 당신이 모든 결정을 검증해야 한다. 모든 선택을 이해하는 400줄 diff는 리뷰에 5분 걸린다. 에이전트가 데이터베이스 스키마, API 형태, 에러 코드, 검증 로직을 선택한 400줄 diff는 구현에서 스펙을 역추적해야 하므로 30분 걸린다.
해결책은 더 나은 프롬프트가 아니다. 중요한 결정을 에이전트가 실행할 수 있는 문서에 미리 담는 것이다.
스펙 기반 라이프사이클
워크플로우에는 다섯 단계가 있다. 각각 명확한 진입 조건과 종료 조건이 있다.
1단계: 브레인스토밍. 문제 공간을 탐색한다. 제약은 무엇인가? 어떤 접근법이 있는가? 이전에 뭘 시도했는가? 혼자서 또는 Claude Code의 대화 모드에서 생각을 소리 내어 한다. 종료 조건: 선호하는 접근법이 있고 트레이드오프를 이해한다.
2단계: 리뷰. 접근법을 스트레스 테스트한다. 뭐가 잘못될 수 있는가? 어떤 엣지 케이스가 있는가? 코드베이스의 기존 항목과 충돌하는가? 여러 에이전트와 작업 중이면 여기서 아키텍처 에이전트나 세컨드 오피니언이 가치 있다. 종료 조건: 접근법이 견고하다고 확신한다.
3단계: 스펙. 결정한 것을 적는다. 문제 설명, 제안하는 접근법, 수정할 파일, 기계적으로 검증 가능한 수용 기준, 테스트 계획. 이것이 계약이다. 종료 조건: 누구든(인간 또는 에이전트) 이 스펙을 읽고 무엇을 만들고 어떻게 검증할지 정확히 알 수 있다.
4단계: 구현. 에이전트가 스펙에 맞춰 실행한다. 모호한 아이디어가 아니라 테스트 가능한 기준이 있는 구체적인 문서에 맞춰. 종료 조건: 에이전트가 완료를 선언하고 검증 증거를 게시했다.
5단계: 검증. 당신(또는 QA 에이전트)이 구현이 스펙과 일치하는지 확인한다. "맞아 보이는지"가 아니라 "각 수용 기준을 충족하는지"다. 종료 조건: 모든 기준이 확인되고, 실패한 것은 4단계로 돌아간다.
핵심 통찰: 1-3단계는 비용이 적다. 중간 규모 기능에 10-20분이다. 4단계는 구현에 필요한 시간만큼 걸린다. 5단계는 5-10분이다. 1-3단계를 건너뛴다고 10-20분을 아끼는 게 아니다. 잘못된 방향으로 간 작업을 리뷰, 디버그, 다시 하는 시간이 든다.
좋은 에이전트 스펙의 모습
실제 스펙 템플릿이다. 유저 스토리가 아니다. 프로덕트 요구사항 문서도 아니다. 에이전트에게 무엇을 만들지 정확히 알려주는 작업 문서다.
## Problem
The filter bar resets when switching workspaces. Users lose their
filter state and have to re-apply filters every time they switch.
## Approach
Persist filter state per-workspace in localStorage. Key the stored
state by workspace database path so filters don't bleed across
workspaces.
## Files to Modify
- lib/local-storage.ts: Add getWorkspaceFilters / setWorkspaceFilters
- components/filter-bar.tsx: Read initial state from localStorage,
write on every change
- hooks/use-workspace.ts: Trigger filter restore on workspace switch
## Acceptance Criteria
1. Select workspace A, set filters to status=open + type=bug
2. Switch to workspace B. Filters reset to defaults.
3. Switch back to workspace A. Filters restore to status=open + type=bug.
4. Close the browser tab, reopen. Filters for the active workspace
are still applied.
5. bd list --status=open --type=bug output matches the filtered table.
## Out of Scope
- Server-side filter persistence
- Filter presets / saved filter combinations
- URL-based filter state (query params)
## Test Plan
- Unit test: getWorkspaceFilters returns stored value for matching
workspace path
- Unit test: setWorkspaceFilters writes correct key format
- Manual test: steps 1-5 from acceptance criteria above
이 스펙이 무엇을 포함하고 무엇을 포함하지 않는지 주목하라. localStorage가 어떻게 작동하는지 설명하지 않는다. Claude Code는 안다. URL params 대신 localStorage를 선택한 이유를 정당화하지 않는다. 그건 브레인스토밍 단계에서 일어났다. 에이전트가 건드려야 할 모든 파일을 나열한다. 에이전트가 이 목록 밖의 파일을 수정하기 시작하면 경고 신호다. Out of scope 섹션이 있어 에이전트의 골드 플레이팅을 방지한다.
수용 기준이 가장 중요한 부분이다. 각각은 관찰 가능한 결과가 있는 구체적인 액션이다. "필터가 유지되어야 한다"가 아니다. 그건 모호하다. "워크스페이스 A로 전환하고, 필터가 status=open + type=bug인지 확인한다"는 테스트 가능하다. 에이전트가 실행할 수 있다. QA 리뷰어가 검증할 수 있다.
Plan-before-code 패턴
구현 실수의 대다수를 코드가 되기 전에 잡아내는 프로토콜이 있다: 에이전트에게 무엇이든 쓰기 전에 구현 계획을 게시하도록 요구하는 것이다.
실제로는 태스크에 대한 구조화된 코멘트 형태다. 에이전트는 파일을 열기 전에 무엇을 할 것인지 정확히 적는다.
PLAN: Persist filter state per-workspace using localStorage.
1. Add workspace-scoped storage helpers to lib/local-storage.ts:
- getWorkspaceFilters(dbPath: string): FilterState | null
- setWorkspaceFilters(dbPath: string, filters: FilterState): void
- Key format: "beadbox:filters:{dbPath}"
2. Update components/filter-bar.tsx:
- On mount, read stored filters for current workspace
- On every filter change, write to localStorage
- Accept databasePath as prop (currently not passed)
3. Update hooks/use-workspace.ts:
- On workspace switch, emit event that filter-bar listens for
- Filter-bar reads new workspace's stored filters on event
4. Threading databasePath:
- filter-bar needs databasePath from the workspace context
- Will add it to the WorkspaceProvider context value
Files: lib/local-storage.ts, components/filter-bar.tsx,
hooks/use-workspace.ts, components/workspace-provider.tsx
Test: Unit tests for storage helpers, manual test of all 5
acceptance criteria.
에이전트가 이걸 만드는 데 약 30초. 당신이 읽는 데 약 2분. 그 2분 안에 구현 후에 고치려면 20분 걸릴 문제를 잡을 수 있다:
- 에이전트가 스펙 밖의 파일을 건드리고 있는가? (workspace-provider.tsx 추가는 스펙에 없었다. 괜찮은 건가 스코프 크리프인가?)
- 접근법이 합리적인가? (워크스페이스 전환에 이벤트 이미터를 쓰는 건 과도한 엔지니어링일 수 있다. 더 간단한 prop 변경이 될 수 있다.)
- 빠진 단계가 있는가? (워크스페이스 삭제 시 오래된 localStorage 항목 정리는?)
플랜은 체크포인트다. 맞아 보이면 에이전트에게 진행하라고 한다. 틀려 보이면 플랜을 수정한다. 어느 쪽이든 20분이 아니라 2분을 투자한 것이다.
