Recent Posts
Recent Comments
반응형
«   2025/11   »
1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30
Archives
Today
Total
관리 메뉴

오늘도 공부

궁극의 바이브 코딩 가이드: AI 코딩 실전 경험 본문

AI

궁극의 바이브 코딩 가이드: AI 코딩 실전 경험

행복한 수지아빠 2025. 10. 27. 12:01
반응형

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 지침

  1. 변경하지 않은 코드는 절대 수정하지 마세요
  2. 각 변경사항에 대한 명확한 주석 작성
  3. 테스트 가능한 코드 작성
  4. 성능에 영향을 줄 수 있는 변경사항은 사전에 알려주세요
**템플릿 찾기**: [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 프로젝트에 연동해줘:

  1. .env.local에 필요한 환경 변수 추가
  2. lib/stripe.ts에 Stripe 클라이언트 초기화
  3. types/stripe.types.ts에 타입 정의
  4. 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

로직:

  1. 입력 검증 (Zod 사용)
  2. Supabase Auth로 현재 사용자 확인
  3. 상품 정보 검증 (products 테이블 확인)
  4. Stripe Payment Intent 생성
  5. payments 테이블에 레코드 생성 (status: 'pending')

에러 처리:

  • 인증 실패: 401
  • 상품 없음: 404
  • Stripe 에러: 500
**4단계: 결제 처리**

PaymentForm 컴포넌트에서 실제 결제를 처리하는 로직을 추가해줘:

파일: components/checkout/PaymentForm.tsx

구현:

  1. useStripe, useElements 훅 사용
  2. handleSubmit 함수:
    • stripe.confirmCardPayment() 호출
    • 성공 시: /checkout/success?payment_intent=xxx로 리다이렉트
    • 실패 시: 에러 메시지 toast 표시
  3. 로딩 상태 관리 (버튼 비활성화, 로딩 스피너)
  4. 폼 검증 (카드 입력 완료 여부)

주의사항:

  • PaymentForm은 클라이언트 컴포넌트여야 함
  • 민감한 정보는 절대 로그에 출력하지 말 것
**5단계: 결제 완료 페이지**

결제 완료 페이지를 만들어줘:

위치: app/checkout/success/page.tsx

기능:

  1. URL에서 payment_intent 파라미터 가져오기
  2. Stripe에서 PaymentIntent 상태 확인
  3. payments 테이블 업데이트 (status: 'succeeded')
  4. 사용자에게 결제 완료 메시지 표시

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개 이상의 프롬프트를 작성하며 배운 가장 중요한 교훈:

🎯 핵심 원칙

  1. 명확한 계획 - 코드를 작성하기 전에 생각하기
  2. 단계적 접근 - 한 번에 하나씩, 작고 명확하게
  3. 컨텍스트 관리 - 적절한 파일, 적절한 정보
  4. 반복적 개선 - 검토하고, 수정하고, 다시 검토하기
  5. 패턴 재사용 - 잘 작동하는 것을 활용하기

⚠️ 피해야 할 것들

  • ❌ 거대한 프롬프트
  • ❌ 모호한 요청
  • ❌ Git 없이 작업
  • ❌ 보안 무시
  • ❌ 맹목적으로 AI 신뢰

✅ 해야 할 것들

  • ✅ 상세한 요구사항 작성
  • ✅ 기능을 단계별로 분해
  • ✅ 자주 커밋
  • ✅ 코드 검토 (AI + 사람)
  • ✅ 패턴과 규칙 문서화

바이브 코딩은 마법이 아닙니다.

잘 짜여진 계획, 명확한 소통, 그리고 체계적인 접근의 결과입니다.

이 가이드를 활용해서 여러분만의 바이브를 찾아가세요! 🚀

 

반응형