Claude Code를 이용한 명세 기반 개발 (Spec-Driven Development): Supabase를 활용한 인증 및 데이터 —
요약
본 글은 명세 기반 개발(Spec-Driven Development) 방법론을 활용하여 Supabase를 백엔드로 사용하는 애플리케이션의 인증 및 데이터 레이어를 구현하는 과정을 다룹니다. 핵심 원칙은 '명세(Spec)가 먼저, 코드(Code)는 그다음'이며, 이를 통해 `specs/auth.md`와 같은 명세를 작성한 후 Claude Code에게 코드를 요청하여 `useAuth` 컴포저블을 생성합니다. 이 과정은 개발의 구조화와 예측 가능성을 높이는 효과적인 방법을 제시합니다.
핵심 포인트
- 명세 기반 개발(Spec-Driven Development) 방법론을 적용하여 프로젝트를 체계적으로 구축할 수 있습니다.
- Supabase를 활용한 인증 모듈(`useAuth`) 구현 시, 명세를 바탕으로 필요한 상태 관리 및 메서드를 정의했습니다.
- 사용자 경험 개선을 위해 Supabase 에러 메시지를 사용자 친화적인 한국어 메시지로 변환하는 로직이 포함되었습니다.
- 명세(Spec)와 코드(Code)를 분리하여 개발함으로써, AI 도구(Claude Code)의 도움을 받아 높은 수준의 컴포저블 코드를 효율적으로 생성할 수 있습니다.
시리즈: Claude Code를 이용한 명세 기반 개발 (Spec-Driven Development) — Part 1 · Part 2 · Part 3
Part 1에서는 프로젝트 구조를 설정했지만 로직은 비워두었습니다. 오늘은 와이어프레임을 실제 앱으로 바꿔주는 두 가지 레이어인 인증 (Authentication)과 데이터 (Data)를 구현합니다. 패턴은 동일합니다: 명세(Spec)가 먼저, 코드(Code)는 그다음입니다.
Step 1: Supabase 설정
코드를 건드리기 전에 백엔드를 구성합니다.
- supabase.com에 접속하여 새 프로젝트를 생성합니다.
- 프로젝트가 생성되면 Project Settings → API로 이동하여 다음 항목을 복사합니다:
- URL
- anon public key
- 이를 .env 파일에 붙여넣습니다:
SUPABASE_URL = https://yourproject.supabase.co
SUPABASE_ANON_KEY = eyJhbGciOiJI... - Supabase 대시보드에서 Authentication → Providers로 이동하여 Email이 활성화되어 있는지 확인합니다 (개발을 위해 이메일 확인(email confirmation)은 비활성화하세요).
Step 2: 인증 명세 (The Authentication Spec)
인증 모듈을 위한 전용 명세를 생성합니다.
File: specs/auth.md
명세: 인증 모듈 (Authentication Module)
의존성 (Dependencies)
- @supabase/supabase-js (client)
컴포저블 (Composable): useAuth
상태 (State)
user: Ref<User | null>— 현재 사용자loading: Ref<boolean>— 초기 로드 상태error: Ref<string | null>— 에러 메시지
메서드 (Methods)
signUp(email, password): Promise<{ user, error }>signIn(email, password): Promise<{ user, error }>signOut(): Promise<void>initAuth(): Promise<void>— 로드 시 세션 복구
동작 (Behavior)
- initAuth()는 Nuxt 플러그인을 통해 한 번 호출됩니다.
- signUp/signIn은 Supabase를 호출하기 전에 필드를 검증합니다.
- 모든 Supabase 에러는 사용자 친화적인 메시지로 변환됩니다.
- signOut은 로컬 상태와 Supabase 세션을 모두 삭제합니다.
이제 Claude Code에게 구현을 요청합니다:
@specs/auth.md와 @specs/catalogo-guayoyo.md를 읽으세요. 명세를 정확히 따라 composables/useAuth.ts에 useAuth 컴포저블을 구현하세요. 또한 plugins/supabase.client.ts에 세션을 초기화하는 Nuxt 플러그인을 생성하세요.
Claude Code가 생성한 코드:
// composables/useAuth.ts
import type { User, AuthError } from '@supabase/supabase-js'
const user = ref<User | null>(null)
const loading = ref(true)
const error = ref<string | null>(null)
export function useAuth() {
const client = useSupabaseClient()
function translateError(err: AuthError): string {
const map: Record<string, string> = {
'Invalid login credentials': '이메일 또는 비밀번호가 잘못되었습니다',
'User already registered': '이미 등록된 이메일입니다',
'Password should be at least 6 characters': '비밀번호는 최소 6자 이상이어야 합니다',
}
return map[err.message] ?? err.message
}
async function signUp(email: string, password: string) {
error.value = null
const { data, error: err } = await client.auth.signUp({
email,
password,
})
if (err) {
error.value = translateError(err)
return { user: null, error: error.value }
}
user.value = data.user
return { user: data.user, error: null }
}
async function signIn(email: string, password: string) {
error.value = null
const { data, error: err } = await client.auth.signInWithPassword({
email,
password,
})
if (err) {
error.value = translateError(err)
return { user: null, error: error.value }
}
user.value = data.user
return { user: data.user, error: null }
}
async function signOut() {
await client.auth.signOut()
user.value = null
}
async function initAuth() {
loading.value = true
const { data: { session } } = await client.auth.getSession()
user.value = session?.user ?? null
loading.value = false
}
return { user, loading, error, signUp, signIn, signOut, initAuth }
}
참고: 코드가 명세(spec)를 정확히 따릅니다. 에러 번역, 정확한 메서드 시그니처(method signatures), 올바른 타입(types)까지 모두 포함되어 있습니다.
검증된 폼(validated forms), 에러 메시지, 그리고 /products로의 리다이렉트(redirect)를 포함하는 /login 및 /register를 구현합니다. 이전 단계에서 사용한 useAuth를 사용하세요. 스타일링에는 Tailwind를 사용합니다. 템플릿에는 로직을 넣지 마세요 — 모든 것은 <script setup> 내에 작성합니다. Claude가 pages/login.vue를 생성합니다:
<script setup lang="ts"> definePageMeta ({ layout : 'auth' }) const { signIn , error , user } = useAuth () const router = useRouter () const email = ref ( '' ) const password = ref ( '' ) const submitting = ref ( false ) // 이미 인증된 경우 리다이렉트 if ( user.value ) { await router.replace ( '/products' ) } async function handleSubmit () { submitting.value = true const { error : err } = await signIn ( email.value , password.value ) if ( ! err ) { await router.push ( '/products' ) } submitting.value = false } </script>그리고 유사한 로직을 가진 pages/register.vue를 생성하며, 비밀번호가 8자 이상이고 서로 일치하는지 검증합니다.
⚠️ 핵심 SDD(명세 기반 개발) 통찰: 명세(spec)는 무엇을 검증해야 하는지, 어떻게 동작해야 하는지를 정확히 명시했습니다. Claude Code는 아무것도 "추측"할 필요가 없었습니다. 결과물은 첫 시도에 바로 작동합니다.
단계 4: 제품 데이터베이스 생성
Supabase 대시보드에서 SQL Editor로 이동하여 다음을 실행하세요:
-- products 테이블 생성
CREATE TABLE products (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY ,
name TEXT NOT NULL ,
price NUMERIC ( 10 , 2 ) NOT NULL CHECK ( price > 0 ),
category TEXT NOT NULL ,
image_url TEXT NOT NULL ,
created_at TIMESTAMPTZ DEFAULT now ()
);
-- 샘플 데이터 삽입
INSERT INTO products ( name , price , category , image_url ) VALUES (
'Premium Guayoyo Coffee' , 12.99 , 'Beverages' , 'https://images.unsplash.com/photo-1559056199-641a0ac8b33e?w=400'
), (
'Electric Arepa Maker' , 34.50 , 'Appliances' , 'https://images.unsplash.com/photo-1585937421612-70a008356fbe?w=400'
), (
'"Shipping Code" T-Shirt' , 24.99 , 'Clothing' , 'https://images.unsplash.com/photo-1521572163474-6864f9cf17ab?w=400'
), (
'RGB Mechanical Keyboard' , 89.
99 , 'Technology' , 'https://images.unsplash.com/photo-1587829741301-dc798b83add3?w=400' ), ( '"No Friday Deploys" Mug' , 15 . 99 , 'Accessories' , 'https://images.unsplash.com/photo-1514228742587-6b1558fcca3d?w=400' ), ( 'Braided USB-C Cable 2m' , 9 . 99 , 'Technology' , 'https://images.unsplash.com/photo-1610397704400-33e5573b3a2a?w=400' ); -- 행 레벨 보안 (Row Level Security) 활성화
ALTER TABLE products ENABLE ROW LEVEL SECURITY ;
-- 정책: 인증된 사용자는 제품을 읽을 수 있음
CREATE POLICY "Authenticated users can read products" ON products FOR SELECT TO authenticated USING ( true );
이 SQL을 실행하면 30초 안에 데이터베이스 준비가 완료됩니다.
단계 5: 데이터 액세스 명세 (Data Access Spec) + 구현
새로운 명세 (New spec)
specs/products.md :
명세: 제품 모듈 (Products Module)
컴포저블 (Composable): useProducts
상태 (State)
products: Ref<Product[]>— 제품 목록loading: Ref<boolean>— 로딩 상태error: Ref<string | null>— 에러 메시지
메서드 (Methods)
fetchProducts(): Promise<void>— 모든 제품을 가져옴
동작 (Behavior)
created_at기준 내림차순 정렬 (최신순)- 상태 처리: 로딩, 에러, 빈 값
- 쿼리에 Supabase 클라이언트 사용
- 제품이 이미 로드된 경우 재요청하지 않음 (암시적 캐시)
제품 타입 (Product Type)
- id: string
- name: string
- price: number
- category: string
- image_url: string
- created_at: string
Claude Code에게 요청하기:
@specs/products.md를 읽어줘. composables/useProducts.ts에 useProducts를 구현하고, 명세로부터 TypeScript 타입을 생성해줘.
결과:
// composables/useProducts.ts
export interface Product {
id : string
name : string
price : number
category : string
image_url : string
created_at : string
}
export function useProducts () {
const client = useSupabaseClient ()
const products = ref < Product [] > ([])
const loading = ref ( false )
const error = ref < string | null > ( null )
async function fetchProducts () {
// 암시적 캐시: 이미 데이터가 있으면 재요청하지 않음
if ( products . value . length > 0 ) return
loading . value = true
error .
value = null
try {
const { data, error: err } = await client.from('products').select('*').order('created_at', { ascending: false })
if (err) throw err
products.value = data as Product[]
} catch (e: unknown) {
error.value = e instanceof Error ? e.message : ' Error loading products '
} finally {
loading.value = false
}
}
return { products, loading, error, fetchProducts }
}
Step 6: Supabase로부터 TypeScript 타입 생성하기
최대한의 타입 안정성 (Type safety)을 위해, 데이터베이스에서 직접 타입을 생성합니다:
npx supabase login
npx supabase gen types typescript --project-id YOUR_PROJECT_ID > types/supabase.ts
이 명령은 실제 데이터베이스 스키마 (Schema)를 읽어 정확한 TypeScript 타입을 생성합니다. 이제 Product는 수동으로 작성한 인터페이스 (Interface)가 아니라, 테이블의 정확한 타입이 됩니다.
// types/supabase.ts (자동 생성됨)
export interface Database {
public: {
Tables: {
products: {
Row: {
id: string
name: string
price: number
category: string
image_url: string
created_at: string
}
Insert: { /* ... / }
Update: { / ... */ }
}
}
}
}
이제 우리의 컴포저블 (Composable)은 100% 타입이 지정되었습니다:
import type { Database } from '~/types/supabase'
type Product = Database['public']['Tables']['products']['Row']
// 👆 손으로 만든 것이 아니라, 실제 (REAL) 데이터베이스로부터 유도된 타입입니다.
2부에서 달성한 내용 ✅
✅ Supabase Auth 작동: 회원가입, 로그인, 로그아웃, 세션 유지 (Session persistence)
✅ 유효성 검사 (Validation), 에러 메시지 및 리다이렉트 (Redirects)가 포함된 폼 (Forms)
✅ 샘플 데이터와 행 레벨 보안 (Row Level Security)이 적용된 products 테이블
✅ 로딩/에러/빈 상태 (Loading/error/empty states)를 처리하는 useProducts 컴포저블
✅ 실제 데이터베이스에서 생성된 TypeScript 타입
✅ 느슨한 프롬프트 (Prompts)가 아닌, 명세 (Specs)로부터 구현된 모든 것
약 45분 만에, 여러분은 빈 스캐폴드 (Scaffold)에서 실제 인증과 실제 데이터가 있는 앱으로 나아갔습니다.
3부: UI, 배포 및 살아있는 명세 (UI, Deploy & The Living Spec)에서는 Tailwind를 사용하여 카탈로그 UI를 구축하고, 모든 것을 연결하며, 프로덕션 (Production)에 배포하고, 가장 중요한 SDD 실습인 '프로젝트가 진화함에 따라 명세를 살아있게 유지하는 방법'을 배웁니다.
참고 문헌: Spec-Driven Development: Structure Beats Vibes — RemyBuilds (dev.to) Spec-Driven Development: The Definitive 2026 Guide — BCMS Claude Code를 이용한 명세 기반 개발 (Spec-Driven Development) — Heeki Park (Medium) Claude Code 명세 기반 개발 (Spec-Driven Development) 구현 가이드 — GitHub Claude Code를 이용한 명세 기반 개발 (Spec-Driven Development): 올바르게 구축하기 — SolGuruz Supabase 인증 (Auth) 문서 Nuxt 3 Supabase 모듈
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기