오늘도 공부
궁극의 바이브 코딩 가이드: AI 코딩 실전 경험 본문
Cursor와 클로드 코드를 사용하며 2500개 이상의 프롬프트를 작성했고, 개인 프로젝트부터 프로덕션 레벨 프로젝트까지 다양한 경험을 쌓았습니다. 이 과정에서 배운 모든 노하우를 한 곳에 모아 여러분과 공유하고자 합니다.
1. 명확한 비전 정의하기
구체적이고 상세한 비전으로 시작하세요. 입력이 모호하면 출력도 모호합니다. "쓰레기가 들어가면 쓰레기가 나온다"는 원칙을 항상 기억하세요.
실전 예제
❌ 나쁜 예:
Todo 앱을 만들어줘
✅ 좋은 예:
Next.js 14 (App Router)를 사용한 Todo 앱을 만들어줘. 요구사항은 다음과 같아:
기능:
- 할 일 추가/수정/삭제
- 완료 체크박스
- 우선순위 설정 (높음/중간/낮음)
- 카테고리별 필터링
- 로컬스토리지에 데이터 저장
UI/UX:
- Tailwind CSS 사용
- 모바일 반응형 디자인
- 다크모드 지원
- 드래그 앤 드롭으로 순서 변경
기술 스택:
- TypeScript
- shadcn/ui 컴포넌트
- zustand로 상태 관리
💡 팁: Google AI Studio의 Gemini 2.5 Pro를 활용해 아이디어를 구조화하고 구체화하세요.
2. UI/UX 먼저 계획하기
코드를 작성하기 전에 UI를 신중하게 계획하세요.
추천 도구
- v0.dev: 레이아웃 시각화 및 실험
- 21st.dev: AI 프롬프트가 포함된 다양한 컴포넌트 라이브러리
실전 예제: 재사용 가능한 버튼 컴포넌트
// components/ui/Button.tsx
import { ButtonHTMLAttributes, forwardRef } from 'react';
import { cn } from '@/lib/utils';
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'primary' | 'secondary' | 'outline' | 'ghost';
size?: 'sm' | 'md' | 'lg';
isLoading?: boolean;
}
const Button = forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant = 'primary', size = 'md', isLoading, children, ...props }, ref) => {
return (
<button
ref={ref}
className={cn(
'rounded-lg font-medium transition-all',
{
'bg-blue-600 text-white hover:bg-blue-700': variant === 'primary',
'bg-gray-200 text-gray-900 hover:bg-gray-300': variant === 'secondary',
'border-2 border-gray-300 hover:border-gray-400': variant === 'outline',
'hover:bg-gray-100': variant === 'ghost',
},
{
'px-3 py-1.5 text-sm': size === 'sm',
'px-4 py-2 text-base': size === 'md',
'px-6 py-3 text-lg': size === 'lg',
},
className
)}
disabled={isLoading}
{...props}
>
{isLoading ? '로딩 중...' : children}
</button>
);
}
);
Button.displayName = 'Button';
export default Button;
3. Git & GitHub 마스터하기
Git은 여러분의 최고의 친구입니다. AI가 코드를 망쳤을 때 쉽게 이전 버전으로 돌아갈 수 있습니다.
필수 Git 워크플로우
# 새 기능 시작
git checkout -b feature/user-authentication
# 작업 중 자주 커밋
git add .
git commit -m "feat: 로그인 UI 구현"
# 큰 기능 완성 후
git add .
git commit -m "feat: 사용자 인증 시스템 완성
- JWT 토큰 기반 인증
- 이메일/비밀번호 로그인
- 비밀번호 재설정 기능
- 보호된 라우트 구현"
# 메인 브랜치에 병합
git checkout main
git merge feature/user-authentication
실전 팁
# AI가 이상한 코드를 생성했을 때
git status # 변경사항 확인
git diff # 정확히 무엇이 바뀌었는지 확인
git checkout -- <파일명> # 특정 파일 복원
git reset --hard HEAD # 모든 변경사항 되돌리기 (주의!)
4. 인기 있는 기술 스택 선택하기
널리 사용되고 문서화가 잘 된 기술을 선택하세요. AI 모델은 공개 데이터로 훈련되므로, 인기 있는 스택일수록 더 나은 코드를 생성합니다.
추천 스택
// 프로젝트 구조 예시
my-app/
├── app/ # Next.js 14 App Router
│ ├── (auth)/
│ │ ├── login/
│ │ └── register/
│ ├── (dashboard)/
│ │ ├── layout.tsx
│ │ └── page.tsx
│ ├── api/ # API Routes
│ │ └── users/
│ ├── layout.tsx
│ └── page.tsx
├── components/
│ ├── ui/ # shadcn/ui 컴포넌트
│ └── features/
├── lib/
│ ├── supabase.ts # Supabase 클라이언트
│ └── utils.ts
├── styles/
│ └── globals.css # Tailwind CSS
└── types/
└── database.types.ts # Supabase 타입
추천 조합
- Frontend & API: Next.js 14 (App Router)
- Database & Auth: Supabase
- Styling: Tailwind CSS + shadcn/ui
- Hosting: Vercel
5. Cursor Rules 활용하기
Cursor Rules는 여러분의 친구입니다. 모든 기술 스택, AI 모델 지침, 모범 사례, 패턴을 포함한 강력한 규칙을 작성하세요.
실전 예제: .cursorrules 파일
# 프로젝트: 워킹앱 (Walking App) - Move 피트니스 앱
## 기술 스택
- Flutter 3.x (Clean Architecture)
- Riverpod (상태 관리)
- Freezed (불변 모델)
- Go Router (라우팅)
- Dio (HTTP 클라이언트)
## 코드 스타일 및 규칙
### 1. 아키텍처 패턴
- Clean Architecture 엄격히 준수
- 레이어 구분: presentation / domain / data
- 의존성 방향: presentation → domain ← data
### 2. 상태 관리
```dart
// ✅ 좋은 예: Riverpod Provider 사용
@riverpod
class WalkingSession extends _$WalkingSession {
@override
Future<WalkingSessionState> build() async {
return const WalkingSessionState.initial();
}
Future<void> startWalking() async {
state = const AsyncValue.loading();
// 로직 구현
}
}
// ❌ 나쁜 예: StatefulWidget으로 복잡한 상태 관리
3. 명명 규칙
- 파일명: snake_case (예: walking_session_provider.dart)
- 클래스명: PascalCase (예: WalkingSessionProvider)
- 변수/함수: camelCase (예: startWalking)
- 상수: SCREAMING_SNAKE_CASE (예: MAX_WALKING_DURATION)
4. 에러 처리
// ✅ 항상 Result 패턴 사용
Future<Result<WalkingData, AppError>> getWalkingData() async {
try {
final data = await repository.fetchWalkingData();
return Result.success(data);
} catch (e) {
return Result.failure(AppError.network(e.toString()));
}
}
5. 금지 사항
- StatefulWidget에서 직접 API 호출 금지
- Provider 없이 전역 변수 사용 금지
- try-catch 없이 async 함수 호출 금지
- BuildContext를 async 함수에 전달 금지
6. UI 규칙
- 모든 간격은 8의 배수 사용 (8, 16, 24, 32...)
- 색상은 theme에서만 가져오기
- 하드코딩된 문자열 금지 (l10n 사용)
AI 지침
- 변경하지 않은 코드는 절대 수정하지 마세요
- 각 변경사항에 대한 명확한 주석 작성
- 테스트 가능한 코드 작성
- 성능에 영향을 줄 수 있는 변경사항은 사전에 알려주세요
**템플릿 찾기**: [cursor.directory](https://cursor.directory/)
## 6. Instructions 폴더 유지하기
### 폴더 구조 예시
instructions/ ├── architecture/ │ ├── clean-architecture.md │ └── folder-structure.md ├── components/ │ ├── button-examples.md │ ├── form-patterns.md │ └── modal-patterns.md ├── api/ │ ├── supabase-queries.md │ └── error-handling.md └── common-mistakes.md
### 실전 예제: button-examples.md
```markdown
# 버튼 컴포넌트 패턴
## 기본 사용법
```tsx
<Button variant="primary" size="md">
클릭하세요
</Button>
로딩 상태
<Button variant="primary" isLoading={isSubmitting}>
제출하기
</Button>
아이콘과 함께
<Button variant="outline" icon={<PlusIcon />}>
새로 만들기
</Button>
비활성화
<Button variant="secondary" disabled={!isValid}>
저장
</Button>
주의사항
- onClick 핸들러는 항상 useCallback으로 래핑
- 비동기 작업 시 반드시 isLoading 상태 관리
- disabled일 때 툴팁으로 이유 설명
## 7. 상세한 프롬프트 작성하기
**쓰레기가 들어가면 쓰레기가 나옵니다.** AI가 추측할 여지를 남기지 마세요.
### 실전 예제
❌ **나쁜 프롬프트:**
사용자 프로필 페이지 만들어줘
✅ **좋은 프롬프트:**
사용자 프로필 페이지를 만들어줘. 다음 요구사항을 정확히 따라주세요:
레이아웃
- 상단: 프로필 이미지 (원형, 120x120px) + 이름 + 이메일
- 중단: 탭 메뉴 (프로필 정보 / 활동 내역 / 설정)
- 하단: 탭에 따른 컨텐츠 영역
프로필 정보 탭
- 편집 가능한 필드: 이름, 자기소개, 웹사이트, 위치
- 저장 버튼 (변경사항 있을 때만 활성화)
- Supabase의 profiles 테이블 업데이트
활동 내역 탭
- 최근 활동 10개 표시
- 무한 스크롤 구현
- 각 활동: 아이콘 + 설명 + 시간 (상대 시간)
설정 탭
- 알림 설정 (토글 스위치)
- 비밀번호 변경 버튼
- 계정 삭제 버튼 (확인 모달)
기술 요구사항
- TypeScript 사용
- Supabase Auth로 현재 사용자 정보 가져오기
- React Hook Form + Zod로 폼 검증
- useSWR로 데이터 캐싱
- 로딩 상태: 스켈레톤 UI
- 에러 상태: Toast 알림
파일 위치
- app/(dashboard)/profile/page.tsx (메인 페이지)
- components/profile/ProfileHeader.tsx
- components/profile/ProfileInfo.tsx
- components/profile/ActivityList.tsx
- components/profile/ProfileSettings.tsx
참고
- 기존 Button, Input 컴포넌트 사용 (components/ui/)
- 디자인은 dashboard의 다른 페이지와 일관성 유지
- 모바일 반응형 (640px 이하에서 탭을 드롭다운으로)
## 8. 복잡한 기능 분해하기
**거대한 프롬프트를 주지 마세요.** AI가 환각을 일으키고 엉망인 코드를 생성합니다.
### 실전 예제: 결제 시스템 구현
#### ❌ 나쁜 방법 (한 번에 모든 것)
결제 시스템을 만들어줘. Stripe 연동하고, 결제 페이지, 결제 성공/실패 처리, 웹훅, 구독 관리, 영수증 발송, 환불 기능 다 만들어줘.
#### ✅ 좋은 방법 (단계별 분해)
**1단계: Stripe 설정**
Stripe를 Next.js 프로젝트에 연동해줘:
- .env.local에 필요한 환경 변수 추가
- lib/stripe.ts에 Stripe 클라이언트 초기화
- types/stripe.types.ts에 타입 정의
- Stripe 테스트 키 사용
참고:
- Next.js 14 App Router 사용 중
- 서버 컴포넌트와 클라이언트 컴포넌트 구분 필요
**2단계: 결제 UI**
결제 페이지 UI를 만들어줘:
위치: app/checkout/page.tsx
포함 요소:
- 상품 정보 카드 (이미지, 이름, 가격)
- Stripe Elements로 카드 입력 폼
- 총액 표시
- "결제하기" 버튼
사용할 컴포넌트:
- components/checkout/ProductCard.tsx (새로 생성)
- components/checkout/PaymentForm.tsx (새로 생성)
- 기존 Button 컴포넌트 (components/ui/Button.tsx)
스타일:
- 중앙 정렬, 최대 너비 600px
- 카드 형태의 깔끔한 레이아웃
- 모바일 반응형
**3단계: Payment Intent API**
결제 Intent를 생성하는 API를 만들어줘:
위치: app/api/create-payment-intent/route.ts
입력:
- amount (number): 결제 금액 (센트 단위)
- productId (string): 상품 ID
출력:
- clientSecret (string): Stripe의 client secret
로직:
- 입력 검증 (Zod 사용)
- Supabase Auth로 현재 사용자 확인
- 상품 정보 검증 (products 테이블 확인)
- Stripe Payment Intent 생성
- payments 테이블에 레코드 생성 (status: 'pending')
에러 처리:
- 인증 실패: 401
- 상품 없음: 404
- Stripe 에러: 500
**4단계: 결제 처리**
PaymentForm 컴포넌트에서 실제 결제를 처리하는 로직을 추가해줘:
파일: components/checkout/PaymentForm.tsx
구현:
- useStripe, useElements 훅 사용
- handleSubmit 함수:
- stripe.confirmCardPayment() 호출
- 성공 시: /checkout/success?payment_intent=xxx로 리다이렉트
- 실패 시: 에러 메시지 toast 표시
- 로딩 상태 관리 (버튼 비활성화, 로딩 스피너)
- 폼 검증 (카드 입력 완료 여부)
주의사항:
- PaymentForm은 클라이언트 컴포넌트여야 함
- 민감한 정보는 절대 로그에 출력하지 말 것
**5단계: 결제 완료 페이지**
결제 완료 페이지를 만들어줘:
위치: app/checkout/success/page.tsx
기능:
- URL에서 payment_intent 파라미터 가져오기
- Stripe에서 PaymentIntent 상태 확인
- payments 테이블 업데이트 (status: 'succeeded')
- 사용자에게 결제 완료 메시지 표시
UI:
- 체크마크 아이콘 (성공)
- "결제가 완료되었습니다" 메시지
- 주문 번호 표시
- "주문 내역 보기" 버튼 (dashboard로 이동)
에러 처리:
- payment_intent 없음: 404
- 결제 실패/취소: 안내 메시지 + "다시 시도" 버튼
## 9. 채팅 컨텍스트 현명하게 관리하기
**채팅이 너무 길어지면 새로 시작하세요.** AI의 컨텍스트 윈도우는 제한적이며, 채팅이 길어지면 이전 내용을 잊어버립니다.
### 실전 팁
새 채팅을 시작할 때 첫 메시지:
이전 채팅에서 사용자 인증 시스템을 작업했어요. 현재 완성된 파일들:
- app/(auth)/login/page.tsx
- app/(auth)/register/page.tsx
- app/api/auth/[...auth]/route.ts
- lib/auth.ts
이제 비밀번호 재설정 기능을 추가하려고 해요.
## 10. 프롬프트 재시작/개선 주저하지 않기
AI가 잘못된 방향으로 가거나 원하지 않는 것을 추가할 때, **되돌아가서 프롬프트를 수정하고 다시 보내는 것이 훨씬 낫습니다**.
### 실전 예제
[AI가 불필요한 애니메이션을 추가함]
❌ "그 애니메이션 빼줘" (계속 문제 발생 가능)
✅ Ctrl+Z로 되돌린 후: "로그인 폼을 만들어줘. 이메일, 비밀번호 입력 필드와 로그인 버튼만 있으면 돼요. 애니메이션이나 추가 효과는 필요 없어요. 깔끔하고 단순한 디자인으로 해주세요."
## 11. 정확한 컨텍스트 제공하기
**올바른 컨텍스트 제공이 가장 중요합니다.** 특히 코드베이스가 커질수록 더욱 중요합니다.
### 실전 예제
@ 기호로 파일 멘션:
"@app/api/users/route.ts 와 @lib/supabase.ts 를 참고해서 @app/api/posts/route.ts 에 게시글 CRUD API를 만들어줘.
기존 users API와 동일한 패턴을 따라주세요:
- 에러 처리 방식
- 응답 형식
- 인증 체크
- Supabase 클라이언트 사용법"
## 12. 일관성을 위해 기존 컴포넌트 활용하기
**새 컴포넌트를 만들 때 기존 컴포넌트를 언급하세요.** AI가 패턴을 빠르게 파악합니다.
### 실전 예제
"@components/ui/Button.tsx 와 @components/ui/Input.tsx 의 스타일과 패턴을 따라서 Select 컴포넌트를 만들어줘.
동일하게 적용해야 할 것들:
- variant props (primary, secondary, outline, ghost)
- size props (sm, md, lg)
- forwardRef 사용
- TypeScript 타입 정의 방식
- Tailwind CSS 클래스 구조
- cn() 유틸리티 사용
추가 기능:
- options prop으로 옵션 리스트 받기
- placeholder prop
- onChange 핸들러"
## 13. AI로 코드 반복 검토하기
각 기능 완성 후, **Gemini 2.5 Pro로 코드를 검토**하세요.
### 실전 워크플로우
**1단계: 보안 검토**
[Gemini 2.5 Pro에게]
당신은 보안 전문가입니다. 다음 코드에서 보안 취약점을 찾아주세요:
[코드 붙여넣기]
특히 다음 항목을 확인해주세요:
- SQL Injection 가능성
- XSS 공격 가능성
- 인증/인가 누락
- 민감 정보 노출
- CSRF 취약점
**2단계: 성능 검토**
[Gemini 2.5 Pro에게 - 새 채팅]
당신은 Next.js 성능 최적화 전문가입니다. 다음 코드에서 성능 문제나 개선점을 찾아주세요:
[코드 붙여넣기]
특히 다음 항목을 확인해주세요:
- 불필요한 리렌더링
- 비효율적인 데이터 페칭
- 번들 크기 최적화
- 메모이제이션 누락
- 이미지/폰트 최적화
**3단계: 개선사항 적용**
[Cursor의 Claude에게]
Gemini의 검토 결과예요: [Gemini의 피드백 붙여넣기]
이 문제들을 수정해주세요.
## 14. 보안 모범 사례 우선하기
### 핵심 보안 체크리스트
#### 1. 클라이언트 데이터 신뢰하지 않기
```typescript
// ❌ 나쁜 예: 클라이언트 입력 직접 사용
export async function POST(request: Request) {
const { userId, amount } = await request.json();
await db.payments.create({ userId, amount }); // 위험!
}
// ✅ 좋은 예: 서버에서 검증 및 정제
import { z } from 'zod';
const paymentSchema = z.object({
amount: z.number().positive().max(1000000),
});
export async function POST(request: Request) {
// 1. 세션에서 userId 가져오기 (클라이언트 입력 무시)
const session = await getServerSession();
if (!session) return new Response('Unauthorized', { status: 401 });
// 2. 입력 검증
const body = await request.json();
const validation = paymentSchema.safeParse(body);
if (!validation.success) {
return Response.json({ error: validation.error }, { status: 400 });
}
// 3. 안전하게 사용
await db.payments.create({
userId: session.user.id, // 세션에서 가져온 ID
amount: validation.data.amount,
});
}
2. 프론트엔드에 비밀 정보 두지 않기
// ❌ 나쁜 예
// app/config.ts
export const STRIPE_SECRET_KEY = 'sk_live_xxxxx'; // 절대 안됨!
// ✅ 좋은 예
// .env.local (git에 커밋하지 않음!)
STRIPE_SECRET_KEY=sk_live_xxxxx
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_live_xxxxx
// lib/stripe.ts (서버 전용)
import Stripe from 'stripe';
if (!process.env.STRIPE_SECRET_KEY) {
throw new Error('STRIPE_SECRET_KEY is missing');
}
export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, {
apiVersion: '2023-10-16',
});
// app/checkout/page.tsx (클라이언트)
const publishableKey = process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY;
// NEXT_PUBLIC_ 접두사가 있는 것만 클라이언트에서 접근 가능
3. 약한 권한 체크
// ❌ 나쁜 예: 로그인만 확인
export async function DELETE(
request: Request,
{ params }: { params: { postId: string } }
) {
const session = await getServerSession();
if (!session) return new Response('Unauthorized', { status: 401 });
// 누구든 로그인했으면 삭제 가능! 위험!
await db.posts.delete({ where: { id: params.postId } });
}
// ✅ 좋은 예: 소유권 확인
export async function DELETE(
request: Request,
{ params }: { params: { postId: string } }
) {
const session = await getServerSession();
if (!session) return new Response('Unauthorized', { status: 401 });
// 게시글 소유자 확인
const post = await db.posts.findUnique({
where: { id: params.postId },
select: { authorId: true },
});
if (!post) return new Response('Not Found', { status: 404 });
if (post.authorId !== session.user.id) {
return new Response('Forbidden', { status: 403 });
}
await db.posts.delete({ where: { id: params.postId } });
}
4. 에러 정보 유출
// ❌ 나쁜 예
try {
await db.query(sql);
} catch (error) {
return Response.json({
error: error.message // DB 스키마 정보 노출!
}, { status: 500 });
}
// ✅ 좋은 예
import { logger } from '@/lib/logger';
try {
await db.query(sql);
} catch (error) {
// 서버 로그에만 자세한 정보 기록
logger.error('Database query failed', { error, sql });
// 사용자에게는 일반적인 메시지만
return Response.json({
error: '요청을 처리할 수 없습니다. 잠시 후 다시 시도해주세요.'
}, { status: 500 });
}
5. IDOR (Insecure Direct Object Reference)
// ❌ 나쁜 예: ID만으로 접근
// URL: /api/invoices/123
export async function GET(
request: Request,
{ params }: { params: { id: string } }
) {
const invoice = await db.invoices.findUnique({
where: { id: params.id }
});
return Response.json(invoice); // 누구나 아무 청구서나 볼 수 있음!
}
// ✅ 좋은 예: 소유권 확인
export async function GET(
request: Request,
{ params }: { params: { id: string } }
) {
const session = await getServerSession();
if (!session) return new Response('Unauthorized', { status: 401 });
const invoice = await db.invoices.findFirst({
where: {
id: params.id,
userId: session.user.id // 본인 것만 조회
}
});
if (!invoice) return new Response('Not Found', { status: 404 });
return Response.json(invoice);
}
6. DB 레벨 보안 무시 (Supabase RLS 예제)
-- Supabase에서 Row Level Security (RLS) 설정
-- 1. RLS 활성화
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
-- 2. 정책 생성: 자신의 게시글만 조회
CREATE POLICY "Users can view own posts"
ON posts FOR SELECT
USING (auth.uid() = user_id);
-- 3. 정책 생성: 자신의 게시글만 수정
CREATE POLICY "Users can update own posts"
ON posts FOR UPDATE
USING (auth.uid() = user_id);
-- 4. 정책 생성: 자신의 게시글만 삭제
CREATE POLICY "Users can delete own posts"
ON posts FOR DELETE
USING (auth.uid() = user_id);
-- 5. 정책 생성: 모두 게시글 작성 가능
CREATE POLICY "Anyone can create posts"
ON posts FOR INSERT
WITH CHECK (auth.uid() = user_id);
7. 보호되지 않은 API
// ❌ 나쁜 예: Rate Limiting 없음
export async function POST(request: Request) {
const { email } = await request.json();
await sendPasswordResetEmail(email);
return Response.json({ success: true });
// 공격자가 무한 이메일 전송 가능!
}
// ✅ 좋은 예: Rate Limiting 적용
import { Ratelimit } from '@upstash/ratelimit';
import { Redis } from '@upstash/redis';
const ratelimit = new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.slidingWindow(5, '1 h'), // 1시간에 5번
});
export async function POST(request: Request) {
const ip = request.headers.get('x-forwarded-for') ?? 'unknown';
const { success } = await ratelimit.limit(ip);
if (!success) {
return Response.json(
{ error: '너무 많은 요청입니다. 나중에 다시 시도해주세요.' },
{ status: 429 }
);
}
const { email } = await request.json();
await sendPasswordResetEmail(email);
return Response.json({ success: true });
}
15. 에러 효과적으로 처리하기
방법 1: 되돌아가서 다시 시도
[콘솔 에러 발생]
"Ctrl+Z로 되돌리고, 다시 만들어줘.
이번엔 TypeScript 타입을 더 엄격하게 지정해주세요."
방법 2: 에러 복사해서 전달
다음 에러가 발생했어요. 해결해주세요:
[에러 메시지 붙여넣기]
참고:
- @lib/api.ts 에서 발생
- fetchUsers 함수 호출 중
💡 규칙: 3번 시도 후에도 해결 안 되면 → 되돌아가서 프롬프트 개선
16. 완고한 에러 체계적으로 디버깅하기
AI가 3번 이상 시도해도 에러를 해결하지 못할 때:
실전 예제
에러 해결을 위해 다음 단계를 따라주세요:
1. @app/checkout/page.tsx 와 @components/checkout/PaymentForm.tsx
그리고 @app/api/create-payment-intent/route.ts 를 분석해서
에러 원인의 상위 3가지 가능성을 알려주세요.
2. 각 파일에 디버그 로그를 추가해주세요:
- 함수 시작/종료
- API 요청/응답
- 상태 변경
3. 로그 추가 후, 테스트하고 결과를 알려주세요.
결과 제공 후:
콘솔 로그 결과:
[로그 출력 붙여넣기]
이제 원인을 파악해서 수정해주세요.
17. 명시적으로: 원치 않는 AI 변경 방지
Claude는 요청하지 않은 것을 추가/삭제/수정하는 경향이 있습니다.
마법의 문장
모든 프롬프트 끝에 추가:
---
⚠️ 중요: 내가 명시적으로 요청한 것만 변경하세요.
다른 코드, 스타일, 구조는 절대 건드리지 마세요.
---
실전 예제
UserProfile 컴포넌트에서 이메일 표시 형식을 변경해주세요.
user@example.com → u***@example.com
⚠️ 중요:
- 오직 이메일 마스킹 로직만 추가
- 기존 컴포넌트 구조, 스타일, props는 절대 변경하지 마세요
- 다른 필드(이름, 프로필 이미지 등)는 건드리지 마세요
18. "AI의 흔한 실수" 파일 유지하기
실전 예제: common-mistakes.md
# Claude가 자주 하는 실수들
## 1. 타입 관련
❌ `any` 타입 남용
✅ 명시적 타입 지정
❌ 옵셔널 체크 없이 접근
```typescript
user.profile.email // user나 profile이 undefined일 수 있음
✅ 안전한 접근
user?.profile?.email ?? '이메일 없음'
2. 비동기 처리
❌ await 없이 Promise 반환 함수 사용 ❌ try-catch 없는 async 함수
✅ 항상 다음 패턴 사용:
try {
const result = await someAsyncFunction();
// 성공 처리
} catch (error) {
console.error('Error:', error);
// 에러 처리
}
3. React Hooks
❌ 조건문 안에서 Hook 호출 ❌ useEffect 의존성 배열 누락 ❌ 불필요한 useEffect (계산된 값은 useMemo)
4. 성능
❌ 컴포넌트 내부에서 함수/객체 생성 ✅ useCallback, useMemo 사용
❌ key로 index 사용 ✅ 고유한 id 사용
5. 보안
❌ 클라이언트에서 민감한 연산 ❌ XSS 취약한 innerHTML 사용 ❌ 사용자 입력 검증 없이 사용
6. Supabase
❌ RLS 정책 없이 테이블 생성 ❌ 클라이언트에서 service role key 사용 ✅ anon key + RLS 정책 조합
7. 스타일링
❌ 인라인 스타일 과다 사용 ❌ !important 남용 ✅ Tailwind 유틸리티 클래스 사용
### 사용 방법
새 기능을 작업할 때:
"@instructions/common-mistakes.md 의 실수들을 피하면서 결제 폼을 만들어주세요."
## 실전 워크플로우 종합 예제
### 시나리오: "운동" 앱의 운동 기록 기능 추가
**1단계: 비전 명확화 (Gemini 활용)**
[Google AI Studio - Gemini 2.5 Pro]
피트니스 앱에 운동 기록 기능을 추가하려고 해요. 사용자가 걸은 거리, 시간, 소모 칼로리를 기록하고 히스토리를 볼 수 있어야 해요.
이 기능을 자세하게 설계해주세요:
- 데이터 구조
- UI/UX 플로우
- 기술적 요구사항
**2단계: UI 계획 (v0.dev)**
[v0.dev에 프롬프트]
운동 기록 대시보드 UI를 만들어줘:
- 오늘의 통계 카드 (거리, 시간, 칼로리)
- 주간 그래프
- 최근 활동 리스트
- 모바일 퍼스트 디자인
**3단계: Git 브랜치 생성**
```bash
git checkout -b feature/exercise-tracking
git commit -m "feat: start exercise tracking feature"
4단계: 단계별 구현
[Cursor - 1단계]
@.cursorrules
@instructions/common-mistakes.md
Supabase에 exercise_records 테이블을 설계해주세요:
필드:
- id (uuid, primary key)
- user_id (uuid, foreign key to auth.users)
- distance (float, meters)
- duration (integer, seconds)
- calories (integer)
- started_at (timestamptz)
- ended_at (timestamptz)
- route (jsonb, optional - 경로 좌표들)
RLS 정책:
- 사용자는 자신의 기록만 CRUD 가능
마이그레이션 SQL 파일을 생성해주세요.
위치: supabase/migrations/20241027_exercise_records.sql
⚠️ 중요: 다른 테이블은 건드리지 마세요.
[Cursor - 2단계]
@components/ui/Button.tsx
@components/ui/Card.tsx
운동 통계 카드 컴포넌트를 만들어주세요:
위치: components/exercise/ExerciseStatsCard.tsx
Props:
- title: string
- value: number
- unit: string
- icon: ReactNode
디자인:
- 기존 Card 컴포넌트 스타일 따르기
- 아이콘 + 숫자 + 단위를 보기 좋게 배치
- 애니메이션 효과 (카운트업)
⚠️ 중요:
- 새 파일만 생성
- 기존 컴포넌트는 수정하지 마세요
[Cursor - 3단계]
@app/api/exercises/route.ts (참고용으로 기존 API 패턴)
@lib/supabase.ts
운동 기록을 가져오는 API를 만들어주세요:
위치: app/api/exercise-records/route.ts
GET /api/exercise-records
- 쿼리 파라미터: startDate, endDate (optional)
- 현재 사용자의 기록만 반환
- 날짜 범위 필터링
- 최신순 정렬
POST /api/exercise-records
- Body: distance, duration, calories, started_at, ended_at, route
- Zod로 검증
- user_id는 세션에서 가져오기
기존 API 패턴 따라주세요:
- 에러 처리 방식
- 응답 형식
- 인증 체크
⚠️ 중요: 정확히 요청한 기능만 구현
5단계: 코드 검토 (Gemini)
[Google AI Studio - Gemini 2.5 Pro]
당신은 보안 전문가입니다.
다음 코드의 보안 취약점을 찾아주세요:
[API 코드 붙여넣기]
[Gemini 응답 후 → Cursor]
Gemini가 발견한 보안 문제들이에요:
[피드백 붙여넣기]
이 문제들을 수정해주세요.
@app/api/exercise-records/route.ts
6단계: 테스트 & 커밋
# 테스트
npm run dev
# 문제없으면 커밋
git add .
git commit -m "feat: add exercise records API
- Create exercise_records table with RLS
- Implement GET/POST endpoints
- Add Zod validation
- Fix security issues identified by code review"
git push origin feature/exercise-tracking
7단계: 에러 발생 시
[에러 발생!]
다음 에러를 해결해주세요:
[에러 메시지]
관련 파일:
@app/api/exercise-records/route.ts
@lib/supabase.ts
참고: POST 요청 시 403 Forbidden 발생
[3번 시도 후에도 해결 안 됨]
문제 진단을 도와주세요:
1. @app/api/exercise-records/route.ts 를 분석해서
403 에러의 가능한 원인 3가지를 알려주세요.
2. 각 단계에 로그를 추가해주세요:
- 세션 확인
- 입력 검증
- Supabase 쿼리 실행
3. 로그 추가 후 코드를 보여주세요.
마무리하며
6개월간 2500개 이상의 프롬프트를 작성하며 배운 가장 중요한 교훈:
🎯 핵심 원칙
- 명확한 계획 - 코드를 작성하기 전에 생각하기
- 단계적 접근 - 한 번에 하나씩, 작고 명확하게
- 컨텍스트 관리 - 적절한 파일, 적절한 정보
- 반복적 개선 - 검토하고, 수정하고, 다시 검토하기
- 패턴 재사용 - 잘 작동하는 것을 활용하기
⚠️ 피해야 할 것들
- ❌ 거대한 프롬프트
- ❌ 모호한 요청
- ❌ Git 없이 작업
- ❌ 보안 무시
- ❌ 맹목적으로 AI 신뢰
✅ 해야 할 것들
- ✅ 상세한 요구사항 작성
- ✅ 기능을 단계별로 분해
- ✅ 자주 커밋
- ✅ 코드 검토 (AI + 사람)
- ✅ 패턴과 규칙 문서화
바이브 코딩은 마법이 아닙니다.
잘 짜여진 계획, 명확한 소통, 그리고 체계적인 접근의 결과입니다.
이 가이드를 활용해서 여러분만의 바이브를 찾아가세요! 🚀
'AI' 카테고리의 다른 글
| NextJs 16에 새로 추가된 MCP 서버에 알아보자 (0) | 2025.10.28 |
|---|---|
| 📋 Next.js 16 MCP 툴 완전 가이드 (0) | 2025.10.28 |
| Claude Code Agent Skills 완벽 가이드 (0) | 2025.10.24 |
| Microsoft Amplifier: AI 코딩 어시스턴트를 강력한 개발 환경으로 변신시키는 도구 (0) | 2025.10.16 |
| AI 엔지니어가 되는 실전 학습 로드맵: 6주 완성 (1) | 2025.09.29 |
