Claude Code를 활용한 명세 주도 개발 (Spec-Driven Development): UI, 배포 및 살아있는 명세 — Part 3
요약
본 글은 Claude Code를 활용하여 명세 주도 개발(Spec-Driven Development)의 마지막 단계인 UI 구현, 경로 보호, 배포 및 살아있는 명세 유지 방법을 다룹니다. 특히, 명세서에 기반하여 인증 미들웨어를 생성하고, 다양한 상태(로딩, 에러, 빈 상태 등)를 처리하는 제품 카탈로그 UI 컴포넌트를 구축하는 과정을 보여줍니다. 명확한 명세를 통해 범용적이고 오류 없는 코드를 얻어내며, 제품이 진화함에 따라 명세 자체를 업데이트하여 시스템의 일관성을 유지하는 것이 중요함을 강조합니다.
핵심 포인트
- 명세(Spec) 기반 개발은 인증 미들웨어와 같은 복잡한 로직도 범용적이고 정확하게 구현할 수 있게 한다.
- Claude Code에게 구체적인 명세 파일(@specs/catalogo-guayoyo.md의 US-5 등)을 제공하여 코드를 요청하는 것이 효과적이다.
- Tailwind CSS를 활용하여 로딩, 에러, 빈 상태 등을 모두 처리하는 세련되고 반응형인 UI 컴포넌트를 구축할 수 있다.
- 명세가 구식이 되면 시스템 전체에 잘못된 확신을 주기 때문에, 제품의 진화와 함께 명세를 살아있는 상태로 유지해야 한다.
시리즈: Claude Code를 활용한 명세 주도 개발 (Spec-Driven Development) — Part 1 · Part 2 · Part 3
Part 1에서는 명세 (spec)와 스캐폴딩 (scaffold)을 생성했습니다. Part 2에서는 인증 (auth)과 데이터 액세스 (data access)를 구현했습니다. 오늘은 그 루프를 완성합니다: UI, 경로 보호 (route protection), 배포 (deployment), 그리고 — 가장 중요한 — 제품이 진화함에 따라 명세를 어떻게 살아있는 상태로 유지할 것인가에 대해 다룹니다. 업데이트되지 않는 명세는 명세가 없는 것보다 더 나쁩니다. 잘못된 확신을 주기 때문입니다.
Step 1: 인증 미들웨어 (Auth Middleware)
경로를 보호하는 것은 간단해 보이지만, 명확한 명세가 없으면 복잡해집니다. 우리의 명세 (US-5)는 보호해야 할 대상을 정확히 명시합니다:
- /products는 인증 (authentication)이 필요함
- 이미 인증된 경우 /login 및 /register는 /products로 리다이렉트(redirect)함
우리는 Claude Code에게 다음과 같이 요청합니다:
@specs/catalogo-guayoyo.md 의 US-5를 읽어줘. middleware/auth.ts에 인증 미들웨어 (auth middleware)를 구현해줘. defineNuxtRouteMiddleware를 사용해. 로직을 중복하지 마 — 미들웨어는 범용적 (generic)이어야 해.
// middleware/auth.ts
export default defineNuxtRouteMiddleware (( to ) => {
const { user , loading } = useAuth ()
// 세션 초기화를 기다림
if ( loading . value ) return
const publicRoutes = [
' /login '
, ' /register '
]
if ( ! user . value && ! publicRoutes . includes ( to . path )) {
return navigateTo ( ' /login ' )
}
if ( user . value && publicRoutes . includes ( to . path )) {
return navigateTo ( ' /products ' )
}
})
깔끔하고, 범용적이며, 외부 의존성 (external dependencies)이 없습니다. 정확히 명세가 요구한 대로입니다.
pages/products.vue 에서 미들웨어를 활성화합니다:
< script setup lang= "ts" >
definePageMeta ({
middleware : ' auth '
})
</ script >
Step 2: 제품 카탈로그 UI
이제 재미있는 부분입니다. Tailwind를 사용하여 명세에 정의된 모든 상태(state), 즉 로딩 (loading), 에러 (error), 빈 상태 (empty), 성공 (success)을 처리하는 세련된 UI를 구축합니다.
@specs/catalogo-guayoyo.md 의 US-3를 읽어줘. ProductCard, ProductGrid 컴포넌트와 /products 페이지를 구현해줘. 상태: 로딩 (skeleton), 빈 상태 ("No products available"), 에러 (재시도 버튼 포함), 성공 (그리드). Tailwind 사용, 반응형 (1/2/3열). 미묘한 호버 (hover) 애니메이션 적용.
components/ProductCard.vue : < script setup lang= "ts" > import type { Product } from ' ~/composables/useProducts ' defineProps < { product : Product } > () </ script > < template > <article class= "group rounded-xl bg-white shadow-sm border border-gray-100 overflow-hidden transition-all duration-300 hover:shadow-md hover:-translate-y-1" > <div class= "aspect-square overflow-hidden bg-gray-50" > <img :src= "product.image_url" :alt= "product.name" class= "h-full w-full object-cover transition-transform duration-300 group-hover:scale-105" loading= "lazy" /> </div > <div class= "p-4" > <span class= "text-xs font-medium text-guayoyo-600 uppercase tracking-wider" > {{ product . category }} </span > <h3 class= "mt-1 text-lg font-semibold text-gray-900 line-clamp-1" > {{ product . name }} </h3 > <p class= "mt-2 text-xl font-bold text-guayoyo-700" > $ {{ product . price . toFixed ( 2 ) }} </p> </div > </article> </ template > pages/products.vue : < script setup lang= "ts" > definePageMeta ({ middleware : ' auth ' }) const { products , loading , error , fetchProducts } = useProducts () const { user , signOut } = useAuth () const router = useRouter () await fetchProducts () async function handleLogout () { await signOut () await router . push ( ' /login ' ) } </ script > < template > <div class= "min-h-screen bg-gray-50" > <!-- Header --> <header class= "bg-white shadow-sm" > <div class= "mx-auto max-w-7xl px-4 py-4 flex justify-between items-center" > <h1 class= "text-xl font-bold text-gray-900" > Catálogo Guayoyo </h1 > <div class= "flex items-center gap-4" > <span class= "text-sm text-gray-500" > {{ user ?.
email }} </span> <button @ click= "handleLogout" class= "text-sm text-red-600 hover:text-red-800 transition-colors" > 로그아웃 </button> </div> </div> </header> <!-- Content --> <main class= "mx-auto max-w-7xl px-4 py-8" > <!-- Loading State --> <div v-if= "loading" class= "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6" > <div v-for= "n in 6" :key= "n" class= "h-80 rounded-xl bg-gray-200 animate-pulse" /> </div> <!-- Error State --> <div v-else-if= "error" class= "text-center py-20" > <p class= "text-red-600 text-lg" > {{ error }} </p> <button @ click= "fetchProducts" class= "mt-4 px-6 py-2 bg-guayoyo-600 text-white rounded-lg hover:bg-guayoyo-700 transition-colors" > 재시도 </button> </div> <!-- Empty State --> <div v-else-if= "products.length === 0" class= "text-center py-20" > <p class= "text-gray-400 text-lg" > 사용 가능한 제품이 없습니다 </p> </div> <!-- Success: Product Grid --> <div v-else class= "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6" > <ProductCard v-for= "product in products" :key= "product.id" :product= "product" /> </div> </main> </div> </ template > 모든 상태(로딩, 에러, 빈 상태, 성공)가 수락 기준 (Acceptance Criteria)에 명시된 대로 정확하게 구현되었음에 주목하세요. "로딩 중에 사용자가 이상한 것을 본다"와 같은 상황은 없습니다. 명세 (Spec)는 코드를 작성하기 전에 모든 상태에 대해 생각하도록 강제합니다.
3단계: 모든 것을 하나로 연결하기
미들웨어 (Middleware), 인증 (Auth), 그리고 UI가 준비되었으므로, 애플리케이션 흐름은 다음과 같습니다:
사용자가 / 에 접속함 → 미들웨어 (Middleware)가 /login 으로 리다이렉트함
사용자가 회원가입 또는 로그인함 → /products 로 리다이렉트함
/products 가 Supabase에서 제품을 가져옴
데이터가 로드되는 동안 사용자는 로딩 스켈레톤 (Loading Skeleton)과 함께 카탈로그를 봄
사용자가 로그아웃함 → 다시 /login 으로 돌아감
이 모든 것이 작동하는 이유는 각 구성 요소가 자신의 명세 (Spec)에 따라 구현되었기 때문입니다.
3개의 명령어로 배포하기:
Vercel CLI 설치
npm i -g vercel
배포
vercel
Vercel에 환경 변수 설정
vercel env add SUPABASE_URL
vercel env add SUPABASE_ANON_KEY
또는 GitHub 리포지토리(repo)를 Vercel에 연결하여 모든 푸시(push) 시 자동으로 배포할 수 있습니다. 여러분의 앱은 catalogo-guayoyo.vercel.app 에서 라이브 상태가 됩니다.
단계 5: 살아있는 명세 (The Living Spec) — SDD의 가장 중요한 관행
여기서 튜토리얼과 실제 엔지니어링 실무를 구분 짓는 교훈이 나옵니다. 명세 (Spec)는 처음에 작성하고 버려두는 문서가 아닙니다. 그것은 제품과 함께 진화하는 살아있는 산출물 (artifact)입니다.
실제 시나리오: 상사가 "카테고리 필터링 기능을 추가하세요"라고 말합니다. 전통적인 개발 방식에서는 코드를 열고, 수정할 위치를 찾아, 구현하고, 무언가를 망가뜨린 뒤, 이를 수정하고, 커밋 (commit)합니다. SDD에서는 다음과 같습니다:
먼저 명세를 업데이트합니다:
변경 사항: 카테고리 필터 (v1.1)
US-6: 카테고리별 제품 필터링
인증된 사용자로서
나는 카테고리별로 제품을 필터링하기를 원하며
그 결과 내가 찾는 것을 더 빠르게 찾을 수 있어야 한다.
수락 기준 (Acceptance criteria):
- [AC-6.1] 기존 카테고리가 포함된 드롭다운 또는 탭
- [AC-6.2] 카테고리 선택 시 페이지 새로고침 없이 제품 필터링
- [AC-6.3] 전체 카탈로그를 보기 위한 "모두" 옵션
- [AC-6.4] 카테고리에 제품이 없는 경우 특정 빈 상태 (empty state) 표시
명세를 커밋합니다:
git add specs/catalogo-guayoyo.md
git commit -m "spec: add category filter (US-6)"
Claude Code에게 업데이트된 명세에 따라 구현하도록 요청합니다:
@specs/catalogo-guayoyo.md를 읽어줘. 명세가 변경되었어 — US-6 (카테고리 필터)가 추가되었어.
기존의 모든 것과 호환성을 유지하면서 필터를 구현해줘.
US-1부터 US-5까지의 어떤 것도 망가뜨리지 마.
이것은 "Claude, 필터를 추가해줘."라고 말하는 것과는 근본적으로 다릅니다. 차이점은 다음과 같습니다:
- Claude Code는 무엇을 건드리지 말아야 할지 알고 있습니다 (US-1부터 US-5까지 문서화되어 있음)
- 구현이 수락 기준 (Acceptance Criteria)을 준수합니다 (필터가 어떻게 보여야 하는지 추측하지 않음)
- 변경 사항을 추적할 수 있습니다 (git log를 통해 명세의 무엇이 왜 변경되었는지 확인 가능)
- 무언가 고장 나면, 오래된 프롬프트를 디버깅하는 대신 명세로 돌아가면 됩니다.
💡 프로 팁: Claude Code를 활용한 SDD에 관한 Alex Op의 기사는 프로젝트가 커질 때 사용할 수 있는 고급 패턴인, Claude Code의 서브에이전트 (Subagents)를 사용하여 명세를 병렬로 조사, 계획 및 구현하는 방법을 보여줍니다.
이 시리즈를 통해 배운 것
3개의 파트 동안 여러분은 다음을 구축했습니다:
🎯 TODO 리스트가 아닌, 인증 (Auth), 데이터베이스, UI가 포함된 실제 앱
📋 프로젝트의 단일 진실 공급원 (Source of Truth) 역할을 하는 명세 (Specification)
🤖 Claude Code가 실행하고, 여러분이 결정하는 워크플로우
🔒 Supabase를 이용한 인증, 보호된 경로 (Protected Routes), 행 레벨 보안 (Row Level Security)
🎨 모든 상태 (로딩, 빈 화면, 에러, 성공)를 포함한 반응형 UI
🚀 몇 분 만에 완료하는 프로덕션 배포
🔄 살아있는 명세: 방향을 잃지 않고 반복 (Iterate)하는 방법
이 모든 과정은 명세 (Spec) → 검토 (Review) → 구현 (Implement) → 검증 (Validate)이라는 일관된 패턴으로 이루어졌습니다.
SDD는 폭포수 (Waterfall) 모델이 아닙니다
SDD에 대한 가장 흔한 오해는 "AI를 사용하는 폭포수 모델"이라는 것입니다. Marmelab의 François Zaninotto는 가장 많이 인용되는 비판가 중 한 명이며, 그의 지적은 타당합니다. 날짜를 표시하기 위해 1,300줄짜리 명세를 만드는 것은 관료주의적인 헛소리입니다.
하지만 제대로 수행되는 SDD는 폭포수 모델이 아닙니다. 그것은 "최소한의 실행 가능한 규율 (Minimum Viable Discipline)"입니다:
- 인증 기능을 위한 경우: 40줄의 명세
- 제품 필터를 위한 경우: 15줄의 명세
- 버튼 색상을 변경하는 경우: 명세가 필요하지 않음
실질적인 규칙: 아키텍처 드리프트 (Architectural Drift)의 비용이 높은 곳(인증, 결제, 멀티 테넌트 데이터, API)에는 명세를 사용하고, 틀렸을 때의 비용이 단순히 페이지 새로고침인 곳에서는 명세를 건너뛰십시오.
다음 단계는 무엇인가요?
전체 저장소 클론하기: github.com/guayoyo-tech/catalogo-guayoyo (곧 공개 예정)
원본 명세 읽어보기: specs/catalogo-guayoyo.md
명세를 수정해보고 Claude Code에게 다시 생성하도록 요청해보기
연습 문제로 US-6 (카테고리 필터) 구현해보기
#SpecDrivenDev 해시태그와 함께 여러분만의 명세 주도 (Spec-driven) 앱을 공유해주세요
이 시리즈가 마음에 드셨나요? 명세 주도 개발 (Spec-Driven Development)로 무엇을 만들고 싶은지 알려주세요. 또는 아직도 300줄짜리 프롬프트를 디버깅하고 있는 누군가에게 이 글을 공유해 주세요.
전체 시리즈 참고 문헌
Spec-Driven Development: Structure Beats Vibes — RemyBuilds
Spec-Driven Development: The Definitive 2026 Guide — BCMS
Exploring Generative AI: SDD — Martin Fowler (Birgitta Boeckeler)
Spec-Driven Development with Claude Code in Action — Alex Op
Spec-Driven Development with Claude Code: Build It Right — SolGuruz
Using Spec-Driven Development with Claude Code — Heeki Park
GitHub Spec Kit — github.com/github/spec-kit
Claude Code SDD Implementation Guide — papaoloba
Spec-Driven Development with AI: Get Started — GitHub Blog
Vibe Coding Has a Scaling Problem — VibeReady
Stack Overflow 2025 Developer Survey
CSA: AI-Generated Code Vulnerability Surge 2026
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기