OpenSpec: Spec-Driven Development
AI 코딩 에이전트가 코드를 잘 짜는 시대가 됐지만, "뭘 만들어야 하는지"를 제대로 전달하지 못하면 결과물은 여전히 엉뚱하다. 프롬프트 하나에 의존하는 방식은 컨텍스트가 세션에 갇히고, 요구사항이 복잡해질수록 한계가 뚜렷해진다.
OpenSpec은 이 문제를 "명세를 먼저 작성하고, 그 명세를 기반으로 구현하자"는 Spec-Driven Development(SDD) 방식으로 풀려는 오픈소스 프레임워크다.
Spec-Driven Development란
SDD의 핵심 아이디어는 단순하다. 코드를 쓰기 전에 "무엇을 만들지"를 문서로 합의한다. 이건 새로운 개념은 아니다. TDD가 "테스트를 먼저 쓴다"는 제약으로 설계를 강제하는 것처럼, SDD는 "명세를 먼저 쓴다"는 제약으로 요구사항 정의를 강제한다.
AI 코딩 에이전트 맥락에서 이게 중요한 이유가 있다.
- 세션 휘발성: 대부분의 AI 에이전트는 채팅 세션이 끝나면 컨텍스트를 잃는다. 명세 파일이 리포에 남아있으면 어떤 에이전트든 이어서 작업할 수 있다
- 의도 추적: git diff만으로는 "왜 이 변경을 했는지" 알기 어렵다. 명세 변경(spec delta)이 함께 있으면 리뷰어가 변경의 의도를 바로 파악할 수 있다
- 도구 독립성: 특정 AI 도구에 종속되지 않는다. 명세는 마크다운 파일이므로 Claude Code, Cursor, Copilot 어디서든 읽을 수 있다
OpenSpec 구조
프로젝트에 openspec/ 디렉터리를 추가하면, 두 가지 하위 폴더로 상태를 관리한다.
openspec/
specs/ # 시스템의 현재 명세
auth.md
billing.md
notifications.md
changes/ # 진행 중인 변경 제안
add-dark-mode/
proposal.md
tasks.md
design.md
delta.md
config.yaml # OpenSpec 설정
| 폴더 | 역할 |
|---|---|
specs/ | 시스템이 현재 어떤 상태인지 기술하는 명세. 기능 단위로 파일을 나눈다 |
changes/ | 앞으로 어떻게 바뀌어야 하는지 기술하는 변경 제안. 각 제안이 하위 폴더를 가진다 |
specs/는 "현재", changes/는 "미래"를 나타낸다. 변경이 완료되면 changes/의 내용이 specs/에 반영되고 아카이브된다.
워크플로우
1. 초기화
npm install -g @fission-ai/openspec@latest
cd my-project
openspec init
openspec init은 프로젝트에 openspec/ 폴더와 기본 설정 파일을 생성한다. 기존 프로젝트(brownfield)에도 바로 적용할 수 있다.
2. 변경 제안 (Propose)
실제로 이 블로그에서 바로 써봤다. Posts에는 읽기 진행률 바와 예상 읽기 시간이 있지만 Notes에는 빠져 있었다. 마침 필요하던 기능이라 이걸로 propose를 실행했다.
/opsx:propose notes에 읽기 진행률 바와 읽기 시간 표시 추가
이 명령을 실행하면 OpenSpec이 다음 순서로 동작한다.
- 코드베이스 탐색: Explore 에이전트가 기존 컴포넌트와 구조를 분석한다. 이번 경우
ReadingProgressBar,calculateReadingTime등 기존 구현을 자동으로 찾아냈다. - 기존 specs 확인:
openspec/specs/에 이미 작성된 명세가 있는지 확인한다. 처음이라 비어있었기 때문에 새 스펙도 함께 생성된다. - artifact 생성: proposal → design + specs(병렬) → tasks 순서로 파일을 작성한다. design과 specs는 서로 독립적이므로 병렬로 생성한다는 점이 흥미롭다.
이 단계에서는 코드가 하나도 작성되지 않는다. 오직 "무엇을, 왜, 어떻게 바꿀지"만 정의한다.
실제로 블로그 프로젝트에서 propose를 실행한 결과, openspec/changes/notes-reading-progress/ 아래에 다음 파일들이 생성됐다.
proposal.md — 변경 제안서. 이 문서의 역할은 "코드를 쓰기 전에 왜, 무엇을, 어디까지 바꿀지"를 한 곳에 정리하는 것이다. 네 개의 섹션으로 구성된다.
## Why
Notes 페이지에 읽기 진행률 바와 예상 읽기 시간이 없어 독자가 현재 위치와
남은 분량을 파악하기 어렵다. Posts에는 이미 적용되어 있으므로 ...
## What Changes
- Notes 레이아웃에 ReadingProgressBar 컴포넌트 추가
- Notes 페이지 헤더 영역에 예상 읽기 시간 표시
- 기존 유틸과 컴포넌트를 재사용하여 코드 중복 없이 구현
## Capabilities
- New: notes-reading-indicator
## Impact
- 코드: NotesLayout.tsx, [...slug].tsx 수정
- 의존성: 추가 패키지 불필요
Why(왜 필요한가) → What Changes(뭐가 바뀌는가) → Capabilities(어떤 기능이 추가되는가) → Impact(영향 범위) 순서로 정리된다. 코드베이스를 먼저 탐색한 뒤 작성되기 때문에, 단순한 요구사항 나열이 아니라 "기존에 ReadingProgressBar가 있으니 재사용하고, 수정 대상은 2개 파일"처럼 구현 가능성까지 반영된 제안서가 나온다.
design.md — 기술적 설계 결정서. "어떻게 구현할지"의 판단 근거를 남긴다. 각 결정마다 채택 이유와 기각된 대안이 함께 기록된다.
## Context
Posts 페이지에는 ReadingProgressBar와 calculateReadingTime이 이미 적용되어 있다.
동일한 컴포넌트/유틸을 재사용하여 Notes에도 적용한다.
## Decisions
### 1. ReadingProgressBar 배치 위치: NotesLayout
NotesLayout의 최상위에 배치한다. 이미 fixed top-0 z-50으로 스타일링되어 있어
레이아웃과 무관하게 동작한다.
대안: [...slug].tsx에 직접 추가 → 레이아웃 관심사가 페이지로 누출되므로 기각.
### 2. 읽기 시간 계산 시점: getStaticProps (빌드 타임)
런타임 계산이 불필요하고, Posts와 동일한 패턴을 따른다.
대안: 클라이언트 사이드 계산 → 불필요한 런타임 비용이므로 기각.
## Risks
- [낮음] 프로그레스 바 z-index 충돌 → z-50 vs z-40이므로 충돌 없음
"NotesLayout에 넣는다"가 아니라, "왜 NotesLayout인지, [...slug].tsx에 넣으면 왜 안 되는지"까지 기록된다. 몇 달 뒤에 코드를 다시 열었을 때 "왜 이렇게 했지?"라는 질문에 이 문서가 답이 된다.
tasks.md — 구현 체크리스트. design에서 결정된 내용을 기반으로, 실제로 손대야 할 작업을 순서대로 나열한다.
## 1. 읽기 시간 데이터 전달
- [ ] getStaticProps에서 calculateReadingTime 호출
- [ ] NotePageProps에 readingTime 필드 추가
## 2. 읽기 시간 UI 표시
- [ ] 헤더 메타 영역에 "N분" 읽기 시간 추가
## 3. 읽기 진행률 바
- [ ] NotesLayout에 ReadingProgressBar 추가
## 4. 검증
- [ ] npm run build 성공 확인
이 체크리스트가 /opsx:apply 실행 시 AI 에이전트가 따라가는 구현 가이드가 된다.
기존 스펙이 없는 상태에서 시작했기 때문에, 새로운 스펙(specs/notes-reading-indicator/spec.md)도 함께 생성됐다. "스크롤에 따른 프로그레스 바 업데이트", "최소 읽기 시간 1분" 같은 시나리오가 WHEN/THEN 형식으로 정의되어 있다.
3. 구현 (Apply)
/opsx:apply
tasks.md의 체크리스트를 순서대로 따라가며 AI 에이전트가 코드를 구현한다. 실제로 실행한 결과, proposal에서 예측한 대로 NotesLayout.tsx와 [...slug].tsx 2개 파일만 수정됐고, 새 패키지 없이 기존 컴포넌트를 재사용했다. 빌드 검증(npm run build)까지 tasks에 포함되어 있어 자동으로 확인된다.
완료되면 tasks.md의 체크박스가 자동으로 업데이트된다. openspec 문서 자체는 변하지 않고, 실제 코드만 변경된다.
## 1. 읽기 시간 데이터 전달
- [x] getStaticProps에서 calculateReadingTime 호출
- [x] NotePageProps에 readingTime 필드 추가
## 2. 읽기 시간 UI 표시
- [x] 헤더 메타 영역에 "N분" 읽기 시간 표시 추가
## 3. 읽기 진행률 바
- [x] NotesLayout에 ReadingProgressBar 추가
## 4. 검증
- [x] npm run build 성공 확인
- [x] Notes 페이지에서 동작 확인
4. 아카이브 (Archive)
/opsx:archive
apply가 끝났다고 바로 아카이브할 필요는 없다. 아카이브는 변경이 확정됐을 때 하는 것이다. 코드 리뷰를 거치거나, PR이 머지되거나, 실제로 배포까지 완료된 시점이 적절하다.
아카이브를 실행하면 changes/ 폴더의 내용이 specs/에 반영되고 정리된다. 이 과정에서 spec delta(기존 명세 대비 요구사항이 어떻게 바뀌었는지)도 함께 기록되므로, 코드 diff만으로는 파악하기 어려운 변경의 의도를 추적할 수 있다. specs/는 항상 시스템의 최신 상태를 반영하게 된다. 아직 확정 전이라면 changes/에 그대로 두면 된다. 이후 수정이 필요하면 같은 change 폴더에서 이어서 작업할 수 있다.
기존 AI 코딩 도구와의 차이점
OpenSpec을 써보면서 느낀 건, AI 코딩 도구와 OpenSpec이 해결하는 문제가 다르다는 것이다.
평소에 oh-my-claudecode(OMC)라는 AI 코딩 도구를 쓰고 있다. 기능 구현, 코드 리뷰, 병렬 작업 처리까지 실행 전반을 맡긴다. OMC는 "어떻게 만들지"에 대한 도구다. 코드를 짜고, 테스트를 돌리고, 리뷰하고, 커밋한다. 반면 OpenSpec은 "뭘 만들지"에 대한 도구다. 핵심은 코드가 아니라 명세를 만드는 데 있다.
OpenSpec은 설계사고, OMC는 시공팀이다. 설계사 없이 집을 지을 수 있고, 시공팀 없이 도면만 그릴 수도 있다. 다만 설계 없이 지은 집은 방 크기가 안 맞거나 문이 엉뚱한 곳에 달릴 수 있다. 기능이 복잡해질수록 "일단 만들어봐"보다 "먼저 정리하고 만들자"가 결과물의 질을 높인다.
실제로 조합하면 이런 흐름이 된다.
- OpenSpec으로 변경 제안 → 명세, 태스크 생성 (뭘 만들지 확정)
- OMC의
/ultrawork나/autopilot으로 태스크 실행 (명세 기반이라 방향 이탈이 줄어든다) - OMC의
/code-review로 결과 검증 - OpenSpec으로 아카이브 → 스펙 업데이트 (다음 변경의 출발점이 된다)
지금까지는 "이거 만들어줘"라고 바로 에이전트에게 넘겼다면, 그 사이에 "이걸 왜, 어떤 범위로 만들지" 정리하는 단계가 하나 끼어드는 셈이다. 물론 단순한 버그 수정이나 한 줄짜리 변경에는 오버헤드가 될 수 있다. 도구는 문제의 크기에 맞게 쓰는 것이 맞다.