블로그로 돌아가기

Claude Code를 활용한 스펙 기반 개발

Claude Code를 활용한 스펙 기반 개발

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분을 투자한 것이다.

이것이 Beadbox가 해결하는 문제입니다.

전체 에이전트 플릿이 무엇을 하고 있는지 실시간으로 파악하세요.

베타 기간 중 무료로 사용해 보세요 →

검증은 "컴파일 된다"가 아니다

Claude Code에서 가장 흔한 실패 모드는 에이전트가 나쁜 코드를 쓰는 게 아니다. 아무도 결과물을 스펙에 대해 검증하지 않은 것이다.

에이전트가 "DONE"이라고 한다. 개발자가 diff를 훑어보고 합리적으로 보여서 머지한다. 이틀 뒤 누군가 그 기능이 수용 기준의 엣지 케이스 #3을 처리하지 않는다는 걸 발견한다. 5분짜리 검증 단계에서 잡는 대신 프로덕션에서 디버깅하고 있다.

검증이란 각 수용 기준을 기계적으로 확인하는 것이다. "작동하는 것 같다"가 아니다. "테스트가 통과한다"가 아니다(테스트가 잘못되었거나 불완전할 수 있다). 스펙의 각 기준이 구체적인 확인을 받는다.

적절한 완료 보고서는 이렇게 생겼다:

DONE: Filter bar now persists selected filters across workspace
switches using per-workspace localStorage.

Changes:
- lib/local-storage.ts: Added getWorkspaceFilters/setWorkspaceFilters
  with key format "beadbox:filters:{dbPath}"
- components/filter-bar.tsx: Reads stored filters on mount, writes
  on change. Accepts databasePath prop.
- hooks/use-workspace.ts: Triggers filter restore on workspace switch
  via callback prop.

QA Verification:
1. Open http://localhost:41420, select workspace A
2. Set filters to status=open, type=bug
3. Switch to workspace B via header dropdown
4. Switch back to workspace A
5. Verify filters are still status=open, type=bug
   -> Confirmed: filters restore correctly
6. Close tab, reopen. Filters persist.
   -> Confirmed: localStorage key present, filters applied on mount
7. Run: bd list --status=open --type=bug
   -> Output matches filtered table contents (14 beads)

Acceptance criteria:
- [x] Filters persist across workspace switches (steps 2-5)
- [x] Filters survive browser restart (step 6)
- [x] Filtered view matches bd CLI output (step 7)
- [x] Filters don't bleed between workspaces (step 3: workspace B
      shows defaults)

Unit tests: 3 added (storage read/write/key format). All passing.

Commit: a1b2c3d

이것과 "DONE: Fixed the filter bar"의 차이는 5분짜리 QA 통과와 30분짜리 조사의 차이다. DONE 코멘트의 모든 주장은 특정 확인으로 뒷받침된다. 모든 수용 기준이 검증 단계에 매핑되어 있다. 리뷰어는 무엇이 만들어졌는지, 어떻게 검증되었는지, 이상한 점이 있으면 어디를 봐야 하는지 정확히 안다.

스펙 컨테이너로서의 Beads

방금 설명한 라이프사이클에는 살 곳이 필요하다. 스펙, 플랜 코멘트, 구현, 완료 보고서, 검증 결과. 모든 것이 하나의 태스크에, 한 곳에.

이 문제를 해결하는 것이 beads다. Beads는 이 워크플로우를 위해 설계된 오픈소스 로컬 퍼스트 이슈 트래커다. 각 "bead"는 설명(스펙), 코멘트 스레드(플랜과 완료 보고서), 상태(open, in_progress, ready_for_qa, closed), 그리고 우선순위, 의존성, 할당 같은 메타데이터를 가진 태스크다.

bd CLI를 사용한 실제 스펙 기반 라이프사이클을 보자:

스펙과 함께 bead 생성:

bd create --title "Persist filter state across workspace switches" \
  --description "## Problem
The filter bar resets when switching workspaces...

## Acceptance Criteria
1. Select workspace A, set filters...
2. Switch to workspace B..." \
  --type feature --priority p2

에이전트가 작업을 클레임하고 플랜을 게시:

bd update bb-a1b2 --claim --actor eng1
bd comments add bb-a1b2 --author eng1 "PLAN: Persist filter state
per-workspace using localStorage.

1. Add workspace-scoped storage helpers...
2. Update filter-bar component...
3. ..."

에이전트가 작업을 완료하고 완료 보고서를 게시:

bd comments add bb-a1b2 --author eng1 "DONE: Filter bar now persists
selected filters across workspace switches.

QA Verification:
1. Open http://localhost:41420...

Acceptance criteria:
- [x] Filters persist across workspace switches
- [x] Filters survive browser restart
...

Commit: a1b2c3d"

bd update bb-a1b2 --status ready_for_qa

QA가 이어받아 검증:

bd show bb-a1b2  # 스펙과 DONE 코멘트 읽기
# 검증 단계 실행
bd comments add bb-a1b2 --author qa1 "QA PASS: All 5 acceptance
criteria verified. Filters persist, restore, and match bd CLI output."

전체 라이프사이클이 bead 안에 있다. 스펙은 설명이다. 플랜은 코멘트다. 완료 보고서는 코멘트다. QA 결과는 코멘트다. 6개월 뒤 누군가 "필터 유지가 어떻게 작동하고 URL params 대신 왜 localStorage를 선택했는지?"라고 물으면, 답은 bead의 코멘트 스레드에 있다.

하나의 스펙만 이 파이프라인을 거칠 때는 터미널과 bd show로 충분하다. 하지만 이 워크플로우의 진가는 여러 스펙을 병렬로 실행할 때 나타난다.

스펙 기반 개발 스케일링

실제 시나리오를 상상해보라: 세 개의 Claude Code 에이전트가 각각 다른 스펙을 구현 중이다. 에이전트 A는 필터 유지 기능을 만들고 있다. 에이전트 B는 워크스페이스 통계용 새 API 엔드포인트를 추가 중이다. 에이전트 C는 WebSocket 재연결 버그를 수정 중이다. 각각 스펙 기반 라이프사이클의 어딘가에 있다.

터미널에서는 bd list를 실행해 모든 활성 bead를 보고, 각각에 bd show를 실행해 상태와 최신 코멘트를 확인해야 한다. 세 개의 병렬 워크스트림 스냅샷을 얻는 데 여섯 개의 명령어다. 이걸 다섯 개나 열 개의 에이전트로 곱하면 플랜 리뷰보다 상태 확인에 더 많은 시간을 쓰게 된다.

여기서 Beadbox가 맞아 들어간다. Beadbox는 워크스페이스의 모든 bead 상태를 실시간으로 보여주는 대시보드다. 어떤 스펙이 열려 있고 에이전트를 기다리는지. 어떤 것에 리뷰가 필요한 플랜이 게시되어 있는지. 어떤 것이 진행 중인지. 어떤 것이 QA 검증 준비가 되었는지. 에이전트가 bd CLI를 통해 코멘트를 쓰고 상태를 변경할 때마다 라이브로 업데이트된다.

스펙 기반 개발에 Beadbox가 필수는 아니다. CLI가 전체 라이프사이클을 처리한다. 하지만 여러 스펙 기반 워크플로우를 병렬로 실행할 때, 각 에이전트의 상태를 개별적으로 폴링하는 대신 파이프라인을 한 눈에 볼 수 있다는 것이 플랜 리뷰, 에이전트 언블록, 정체된 작업 감지 속도를 바꾼다.

Beadbox는 베타 기간 동안 무료이며, 그 위에서 실행되는 beads CLI는 오픈소스다.

도구와 무관하게 변하지 않는 것

beads를 쓰든, GitHub Issues를 쓰든, Linear을 쓰든, 일반 텍스트 파일을 쓰든, 스펙 기반 패턴이 작동하는 이유는 에이전트 동작 방식의 근본적인 비대칭을 다루기 때문이다: 실행은 빠르고 판단은 서투르다. 명확한 스펙을 쓰는 데 투자하는 매 분이 잘못된 결과물 리뷰, 무언의 가정 디버깅, 방향이 틀어진 작업 재수행의 여러 분을 절약한다.

원칙들:

  1. "완료"를 "시작" 전에 정의하라. 수용 기준은 선택이 아니다. 검증을 가능하게 하는 유일한 것이다.

  2. 플랜은 체크포인트지 관료주의가 아니다. 30초짜리 플랜 코멘트가 20분짜리 재작성을 절약한다. 코드가 아니라 플랜을 리뷰하라.

  3. 검증은 프로토콜이지 느낌이 아니다. "괜찮아 보인다"는 검증이 아니다. 각 수용 기준을 구체적인 확인에 매핑하는 것이 검증이다.

  4. 스펙이 유일한 진실의 원천이다. 구현과 스펙이 다를 때 구현이 틀린 것이다. 이 규칙이 존재하는 이유는 에이전트가 나쁜 플랜에 의문을 제기하지 않기 때문이다. 충실히 실행하고 충실히 잘못된 결과물을 생성한다.

  5. 스코프 경계가 드리프트를 방지한다. 수정할 파일의 명시적 목록과 out of scope 섹션이 에이전트가 요청하지 않은 것을 "개선"하는 것을 막는다.

투자는 작다: 구현에 한 시간 걸리는 기능의 스펙을 쓰는 데 10-20분. 리턴은 크다: 일관된 결과, 리뷰 가능한 결과물, 그리고 무엇이 만들어졌고 왜인지의 영구적인 기록.

이런 워크플로우를 구축하고 있다면, GitHub에서 Beadbox에 스타를 달아달라.

Like what you read?

Beadbox is a real-time dashboard for AI agent coordination. Free during the beta.

Share