듀얼 모니터에 떠오른 시스템 아키텍처 다이어그램과 CI/CD 테스트 트로피 대시보드

바이브 코딩 앱 프로덕션 가이드: AI가 짠 코드가 무너지는 지점과 90일 로드맵

Cursor나 Lovable, Claude Code로 주말 만에 화면을 띄워 놓고 “이제 됐다”고 느낀 적이 있다. 로그인되고, 목록이 나오고, 버튼도 눌린다. 문제는 그다음 주였다. 결제 웹훅이 한 번 재전송되자 구독 상태가 두 번 반영됐고, AI가 새로 만든 유틸이 기존 디자인 토큰을 무시했고, 관리자 버튼만 숨긴 채 API는 그대로 열려 있었다.

이 글은 바이브 코딩으로 만든 앱을 실제 결제·실사용자 데이터 앞에 올리기 전에, 내가 어디를 먼저 고치는지 정리한 메모다. 도구 소개가 아니라 “프롬프트로 잘 돌아가는 코드”와 “밤에 안 깨는 서비스” 사이의 간격을 메우는 쪽이 목적이다. 관련 글은 허브 페이지 1인 창업 AI 도구 가이드에도 묶어 두었다.

먼저 결론만 말하면 이렇다. AI에게 속도를 맡기되, 권한·멱등성·스키마·테스트·배포 안전장치는 사람이 명세로 못 박아야 한다. 아래 다섯 가지가 비어 있으면 출시를 미루는 편이 싸다.

  • 프로젝트 루트에 에이전트가 읽을 규칙 파일(AGENTS.md / CLAUDE.md / .cursor/rules)이 있는가
  • 결제·회원 같은 핵심 경로에 통합 테스트(또는 최소 E2E 한 개)가 있는가
  • 백엔드(또는 RLS)에서 권한이 다시 검증되는가 — UI에서 버튼 숨기기로 끝내지 않았는가
  • 웹훅·재시도가 와도 상태가 한 번만 반영되는가
  • 배포 후 에러를 볼 도구(Sentry 등)와 되돌릴 방법이 있는가

바이브 코딩이 빠른 이유, 그리고 그 다음

AI 코딩 도구를 쓰는 이유는 단순하다. 아이디어를 화면으로 옮기는 시간이 예전보다 눈에 띄게 짧아진다. MVP로 시장을 확인하는 단계에서는 그 이점이 크다. Lovable과 Bolt를 비교할 때도 같은 말을 했다. 초반에는 속도를, 출시 직전에는 구멍을 본다.

다만 검증이 끝나고 돈이 오가기 시작하면 질문이 바뀐다. “얼마나 빨리 만들었는가”보다 “이 코드가 다음 기능을 추가해도 안 무너지는가”, “혼자 밤에 응급 대응하지 않아도 되는가”다. 나는 그 전환점에서 아래 세 가지를 같이 본다.

  • 속도와 부채: 오늘 아낀 시간이 다음 달 리팩터링으로 돌아오지 않는지
  • 맥락 한계: 에이전트가 저장하지 않는 컨텍스트를 규칙 파일과 테스트로 보완했는지
  • 1인 운영 부하: 장애·보안 이슈가 내 수면 시간을 가져가지 않는지

에이전트에게 도구를 넘기는 이야기는 MCP 도입 실전 가이드에 따로 적어 두었다. 이 글은 그 앞단, 즉 “생성되는 코드 자체”를 프로덕션 체질로 바꾸는 쪽에 초점을 둔다.

AI가 짠 코드가 프로덕션에서 무너지는 지점

에이전트는 타이핑이 빠르다. 대신 비즈니스 도메인 전체와 장애 이력을 기억하지는 않는다. 아래는 내가 반복해서 본 실패 패턴이다. 출시 전에 이름만 붙여 두면, 리뷰할 때 “이 프롬프트가 또 그 구멍인가”를 바로 짚을 수 있다.

1. 맥락이 끊기며 코드가 파편화된다

컨텍스트 윈도우가 커져도, 세션이 바뀌면 에이전트는 지난 대화를 모른다. A 모듈을 고치면서 B와의 숨은 의존을 놓치고, 이미 있는 헬퍼 옆에 비슷한 유틸을 또 만든다. 디자인 토큰·폴더 경계를 무시한 컴포넌트가 쌓이면 코드베이스가 누더기가 된다.

내가 쓰는 대응은 단순하다. “어디에 무엇을 두는지”, “새 패키지를 마음대로 깔지 말 것”, “서버 상태는 어떤 라이브러리로 둘지”를 규칙 파일에 짧게 적어 두고, PR마다 그 규칙을 기준으로 리뷰한다. 규칙은 길수록 안 읽힌다. 200줄을 넘기면 나눠서 경로별로 거는 편이 낫다.

2. Happy Path만 통과하고 예외는 조용히 삼킨다

정상 입력에서는 잘 돈다. 네트워크 타임아웃, 외부 API의 부분 실패, DB 데드락, 이상한 폼 입력에서는 console.log만 남기고 화면이 멈춘다. 사용자는 “버튼이 안 돼요”라고만 보내고, 로그에는 단서가 없다.

결제에서 특히 자주 터진다. Stripe 웹훅을 AI에게 맡기면, 서명 검증 없이 본문만 파싱하거나, 처리 중간에 실패해도 이미 200을 준 뒤 DB는 안 맞는 코드가 나오기 쉽다. Stripe 공식 문서는 웹훅에서 서명 검증, 빠르게 2xx 반환, 같은 이벤트가 두 번 올 수 있음, 이벤트 순서는 보장되지 않음을 분명히 한다. 여기서 헷갈리기 쉬운 점이 있다. Stripe API 요청에 붙이는 Idempotency-Key는 내가 Stripe로 보내는 POST를 안전하게 재시도할 때 쓰는 것이고, 들어오는 웹훅의 중복 방지는 별도로 event.id를 기록해 이미 처리한 이벤트는 건너뛰는 식으로 직접 만들어야 한다. 둘을 같은 말로 섞어 쓰면 설계가 어긋난다. 참고로 한국에서는 Stripe 계정을 직접 열 수 없어서 실제로는 토스페이먼츠나 Paddle·Polar 같은 MoR의 웹훅을 다루게 되는 경우가 많다. 어떤 결제 서비스를 고를지는 한국 Stripe 대안 비교 글에 따로 정리했다. 원칙은 어느 쪽이든 같다.

출처: Stripe Webhooks, Stripe Idempotent requests

3. 프론트 로컬 상태와 서버 상태가 섞인다

클라이언트 전역 변수에 “지금 문서 내용”을 두고, 재연결 순간에 예전 스냅샷으로 덮어쓰는 사고를 나는 직접 봤다. 탭이 여러 개면 더 헷갈린다. AI는 화면이 갱신되면 끝난 줄 알고, 서버가 진실의 원천인지 브라우저가 진실의 원천인지 경계를 흐린다.

혼자 만드는 제품이면 처음부터 CRDT까지 갈 필요는 없다. 다만 “서버 응답이 최종이다”, “낙관적 업데이트는 실패 시 롤백한다”, “동시 편집이 필요하면 그 요구를 명세에 쓴다” 정도는 규칙에 적어 두는 편이 싸다.

4. 스키마를 바꾸며 하위 호환을 깨먹는다

마이그레이션 파일만 고치고, 이미 저장된 행·예전 앱 버전·캐시된 응답 포맷은 안 본다. 배포 직후 기존 사용자만 500을 받는다. AI에게 “컬럼 추가해줘”라고만 하면 expand-contract(새 필드 추가 → 읽기/쓰기 이전 → 옛 필드 제거) 같은 단계를 생략하기 쉽다.

나는 스키마 변경 PR에 “롤백 SQL”, “기존 행 기본값”, “API 응답에 필드가 없어도 클라이언트가 죽지 않는지”를 체크리스트로 붙인다. AI에게도 그 체크리스트를 프롬프트에 그대로 넘긴다.

5. 권한 검증이 프론트에서 끝난다

가장 위험한 패턴이다. 관리자 메뉴를 숨기면 보안이 끝난 줄 안다. 실제로는 API만 직접 치면 타인의 데이터가 나온다. Supabase를 쓰는 바이브 코딩 앱이라면 이 문제는 거의 항상 RLS 설정으로 다시 막아야 한다. 공공 API 키를 앱에 넣는 실수는 공공데이터 API 보안 가이드와 같은 계열이다.

기준은 하나다. UI는 편의이고, 인가는 서버(또는 DB 정책)에서 한 번 더 막힌다.

6. 캐시가 트래픽 앞에서 무너진다

호출 한도가 있는 외부 API 앞에 Redis TTL만 두면, 만료 순간에 요청이 한꺼번에 원본으로 쏠리는 Cache Stampede가 난다. AI가 짠 “없으면 fetch 후 set” 코드에서 자주 본다. 혼자 운영하는 서비스라도, 인기 키에는 락이나 확률적 조기 갱신 같은 단순한 보호를 명세에 적어 두는 편이 낫다. 공공 API 연동에서는 프록시·캐시 레이어를 같이 설계하는 편이 안전하다.

명세로 에이전트를 붙잡는 방법

바이브 코딩의 속도를 버리라는 말이 아니다. 무작위 프롬프트만으로 키우지 말고, 명세(Specification)로 경계를 먼저 그린 뒤 그 안에서 생성하게 하라는 말이다. 나는 이걸 거창한 방법론보다 “에이전트가 매 세션 읽는 짧은 규칙 + 깨지면 배포를 막는 테스트”로 축소해서 쓴다.

규칙 파일: AGENTS.md, CLAUDE.md, Cursor rules

도구마다 이름이 조금씩 다르다. 2026년 기준으로 내가 실제로 맞추는 위치는 이렇다.

  • 공통: 저장소 루트 AGENTS.md — 폴더 경계, 금지 사항, 테스트 명령, “절대 하지 말 것”을 짧게
  • Claude Code: CLAUDE.md 또는 .claude/CLAUDE.md. AGENTS.md만 있어도 읽을 수 있고(기본은 CLAUDE.md가 없을 때), 둘 다 쓰려면 설정을 바꿔야 한다. 경로별 규칙은 .claude/rules/. 공식 문서: How Claude remembers your project
  • Cursor: 간단한 공통 지시는 AGENTS.md, 세밀한 적용 조건은 .cursor/rules/*.mdc. 루트의 .cursorrules는 레거시로 안내된다. 공식 문서: Cursor Rules

규칙에 꼭 넣는 항목만 고르면 이 정도다.

  • 폴더·모듈 경계 (UI / 서비스 / 데이터 접근을 섞지 말 것)
  • 새 의존성 추가 금지 또는 허용 목록
  • 서버 상태 관리 방식 (예: 서버 상태는 TanStack Query)
  • 인가·비밀키·환경변수 규칙
  • 테스트·린트 명령과 “커밋 전 통과해야 하는 것”

테스트는 트로피 기준으로, 숫자 자랑은 나중에

Kent C. Dodds가 정리한 Testing Trophy의 요지는 “테스트를 많이”가 아니라 투자 대비 확신이 큰 쪽에 힘을 쓰라는 것이다. 프론트·풀스택에서는 통합 테스트에 무게를 두는 편이 유리하고, E2E는 핵심 사용자 흐름 몇 개면 충분하다. 출처: The Testing Trophy and Testing Classifications, Write tests. Not too many. Mostly integration.

혼자 만드는 제품에서 내가 지키는 최소선은 이렇다. 결제·회원가입·권한 경계에 통합 테스트(또는 Playwright 한 시나리오)를 두고, 그 테스트가 실패하면 머지를 막는다. “커버리지 85%” 같은 숫자를 목표로 걸기보다, 돈이 오가거나 타인 데이터가 보이는 경로가 자동으로 깨지는지를 먼저 본다. 커버리지는 그다음 지표다.

프롬프트만으로 키울 때 vs 명세로 키울 때

평가 항목 프롬프트만 (Prompt-only) 명세 + 테스트 가드 (SDD 축소판)
초반 속도 매우 빠름 규칙·테스트 준비에 반나절~며칠
기능이 늘 때 기존 모듈이 조용히 깨지기 쉬움 깨지면 CI가 먼저 알려 줌
보안 UI 숨김으로 착각하기 쉬움 서버/RLS 검증을 규칙에 고정
웹훅·결제 재시도·중복 이벤트에 취약 event.id 기록·서명 검증을 명세화
혼자 운영 한 달 뒤 “다시 짜는” 비용 작은 단위로 고쳐도 버팀

1인 개발자용 90일 로드맵 (내가 쓰는 순서)

날짜는 절대값이 아니다. “이번 주에 이거부터”용 순서에 가깝다. 이미 출시했다면 2개월차부터 역순으로 메워도 된다.

1~2주차 — 규칙을 못 박고 보안 기본선을 켠다

  • AGENTS.md(필요하면 CLAUDE.md, .cursor/rules)에 폴더·인가·의존성 규칙을 한 페이지 안으로
  • TypeScript strict, ESLint/Biome 같은 정적 검사를 CI에 연결
  • 세션/토큰 검증과 DB RLS(또는 동급 정책)를 “기본 켜짐”으로
  • 비밀키·service role이 클라이언트 번들에 없는지 검색

3~4주차 — 핵심 경로에 테스트 가드를 단다

  • 결제 성공/실패/웹훅 재전송, 회원가입, “남의 리소스 접근 거부”를 통합 테스트로
  • 로컬에서 한 번에 도는 명령을 규칙 파일에 적고, 실패 시 머지 금지
  • Happy Path 한 개만 있는 테스트는 “있는 척”이므로, 실패 케이스를 최소 하나 추가

2개월차 — 관측과 예외를 눈에 보이게

  • Sentry 등으로 프론트·백엔드 에러를 모은다. 사일런트 catch를 금지 목록에 넣는다
  • 사용자에게는 짧은 안내, 나에게는 스택·요청 ID가 남게
  • 의존성 취약점 스캔을 CI에 한 줄이라도 넣는다 (도구 이름은 팀에 맞게)

출시 직전에 “테스트는 초록인데 사용자 화면은 빈 성공” 같은 함정은 DriveRadio·MEMVIA 사후분석에도 적어 두었다. 관측이 없으면 그 구멍을 며칠씩 모른다.

3개월차 — 배포를 작게, 되돌리기 쉽게

  • 한 번에 전 사용자에게 밀지 말고, 카나리·프리뷰·스테이징 중 하나를 쓴다
  • 에러율이 튀면 되돌릴 절차를 문서 한 장으로 (버튼 위치까지)
  • 주 1회, 에러 탑3와 기술 부채 한 건만 고치는 짧은 세션을 캘린더에 고정

자주 하는 실수 (내가 직접 한 것 포함)

  • 규칙 파일을 에세이로 쓴다 — 길면 무시된다. 명령·금지·경로만 남긴다.
  • 커버리지 %만 올린다 — 의미 없는 유닛 테스트로 숫자를 채워도 결제 경로는 그대로 뚫린다.
  • 웹훅에서 바로 무거운 일을 한다 — Stripe는 빠르게 2xx를 권한다. 긴 작업은 큐로 넘긴다.
  • 스테이징 없이 프로덕션에서 AI 리팩터링 — 되돌릴 커밋/이미지를 먼저 확보한다.
  • “나중에 RLS”를 출시 후로 미룬다 — 출시 직후가 가장 위험하다. 데이터는 한 번 새면 회수가 어렵다.

이 글을 읽을 때 고르는 기준

  • 아직 MVP 전 — 속도 우선. 규칙 파일만 얇게 두고, 출시 체크리스트는 북마크만 해 둔다.
  • 유료 전환 직전 — 권한·웹훅·테스트·관측을 이번 글 순서대로 막는다.
  • 이미 장애를 한 번 겪음 — 2개월차(관측)부터 역순으로. 원인 없는 수정은 반복된다.
  • 도구를 고르는 중 — 이 글보다 Lovable vs Bolt와 스택 정리 글을 먼저 보는 편이 낫다.

실제로 내가 규칙 파일에 넣는 최소 예시

거창한 템플릿보다, 지난주에 쓰던 저장소에 있던 문장들을 압축하면 대략 이런 느낌이다. 그대로 복사하기보다 자기 스택에 맞춰 고치는 용도다.

# AGENTS.md (요지)

## 구조
- UI는 app/(또는 src/components), 도메인 로직은 src/server, DB 접근은 그 안의 repositories만.
- 클라이언트 컴포넌트에서 supabase secret / service role 키를 import하지 말 것.

## 인가
- "버튼을 숨김"은 인가가 아님. API 라우트와 RLS(또는 동급)에서 user id를 다시 확인.
- 관리자 여부는 user_metadata가 아니라 서버에서 검증한 role 테이블/클레임을 기준.

## 결제·웹훅
- Stripe 웹훅: raw body로 서명 검증 → event.id로 중복 스킵 → 빠른 2xx → 무거운 일은 큐.
- Stripe API POST 재시도에만 Idempotency-Key 사용. 웹훅 중복 방지와 혼동하지 말 것.

## 테스트
- pnpm test:integration 이 실패하면 커밋하지 말 것.
- 결제/권한 시나리오에 실패 케이스가 최소 1개 있어야 함.

이 정도면 AI가 “새 유틸 폴더를 마음대로 파는지”, “관리자 페이지를 프론트 플래그만으로 막는지”를 지적할 기준이 생긴다. 규칙을 어긴 코드가 나와도 내가 “왜 틀렸는지”를 같은 문장으로 되물을 수 있다.

결제 한 줄기를 명세로 쪼개는 연습

추상적인 “예외 처리를 잘하자”는 에이전트에게 거의 안 먹힌다. 나는 결제처럼 돈이 오가는 흐름을 문장 단위로 쪼개서 프롬프트와 테스트에 같이 넣는다.

  1. Checkout(또는 PaymentIntent) 생성 — 서버만, 키는 서버 환경변수
  2. 브라우저에서 결제 완료 — 성공 화면은 “처리 중”일 수 있음을 UI에 명시
  3. 웹훅 수신 — 서명 검증 실패면 4xx, 본문 파싱 전에 raw body 보존
  4. 중복 이벤트 — 같은 event.id면 비즈니스 로직 없이 2xx
  5. 구독/크레딧 반영 — DB 트랜잭션, 실패 시 재시도 가능한 상태로 기록
  6. 사용자 조회 — 웹훅보다 화면이 먼저 열릴 수 있으므로 “아직 반영 전” UI

이 여섯 줄을 테스트 이름과 1:1로 맞추면, AI가 중간 단계를 건너뛴 코드가 나와도 어디에 구멍이 있는지 바로 보인다. DriveRadio·MEMVIA에서 겪은 “성공 응답인데 속이 비어 있음”도, 결국 응답 코드만 보고 비즈니스 완료로 착각한 부류다.

언제 속도를 유지하고, 언제 멈춰야 하는가

모든 화면에 같은 수준의 가드를 걸면 1인 개발은 지친다. 나는 대략 이렇게 가른다.

  • 실험용 랜딩·내부 도구: 규칙 파일 + 기본 린트만. 테스트는 핵심 한 개.
  • 베타 사용자 < 20명, 결제 없음: RLS/인가와 에러 트래킹까지.
  • 결제 또는 민감 데이터: 이 글의 웹훅·멱등·통합 테스트·배포 되돌리기를 출시 조건으로.
  • 이미 장애·데이터 유출 징후: 새 기능 멈추고 관측·인가부터. 기능 추가가 부채를 키운다.

“지금은 데모니까”와 “이미 카드가 등록됐다”를 같은 기준으로 다루면, 결국 후자를 전자처럼 운영하게 된다. 기준을 글로 남겨 두는 이유가 그것이다.

FAQ

Q. AGENTS.md와 CLAUDE.md를 둘 다 써야 하나요?
A. 필수는 아니다. 여러 코딩 에이전트를 쓰면 공통은 AGENTS.md에 두고, Claude 전용 메모만 CLAUDE.md에 두는 구성이 흔하다. Claude Code 기본값은 CLAUDE.md가 있으면 그쪽을 우선한다. 둘 다 항상 읽히게 하려면 설정을 확인해야 한다.

Q. 테스트 커버리지 목표를 몇 %로 잡아야 하나요?
A. 이 글에서는 고정 %를 강요하지 않는다. 1인 제품에서는 “결제·인가·데이터 격리”가 자동으로 실패하는지가 먼저다. 숫자가 필요하면 그 경로를 포함한 뒤에 정한다.

Q. Stripe Idempotency-Key만 넣으면 웹훅 중복은 해결되나요?
A. 아니다. Idempotency-Key는 주로 내가 Stripe API로 보내는 요청용이다. 웹훅은 서명 검증과 event.id 중복 처리가 별도다. 공식 문서의 “Handle duplicate events”를 기준으로 본다.

Q. Lovable/Bolt로 만든 앱도 이 로드맵이 맞나요?
A. 맞다. 오히려 UI는 도구가 빨리 만들어 주므로, 남는 시간을 RLS·웹훅·관측에 써야 한다. 플랫폼 선택은 비교 글을, 출시 직전 구멍은 이 글과 RLS 글을 같이 보면 된다.

Q. 90일을 꼭 지켜야 하나요?
A. 아니다. 주차 숫자는 우선순위 힌트다. 이미 사용자가 있다면 보안과 관측을 앞으로 당긴다.

출시 전 체크리스트

  • 규칙 파일에 폴더·인가·비밀키·테스트 명령이 있는가
  • 관리자/타인 리소스 API를 직접 호출해 보았는가 (UI가 아닌 네트워크 탭·curl)
  • 웹훅 재전송·서명 실패·중복 event.id를 시험했는가
  • 스키마 변경에 롤백 계획이 있는가
  • 에러 트래킹에 테스트 에러가 실제로 찍히는가
  • 배포를 되돌리는 방법을 한 페이지로 적어 두었는가

참고한 자료

마지막으로 한 가지만 덧붙인다. 바이브 코딩이 실패하는 이유는 모델이 “못해서”라기보다, 우리가 속도를 얻는 대신 명세와 검증을 생략해서인 경우가 많다. 그 생략을 의식적으로 줄이는 쪽이, 도구를 바꾸는 일보다 먼저다.

마치며

AI는 타이핑을 대신해 준다. 제품이 밤에 안 깨질지, 남의 데이터가 안 보일지에 대한 판단은 여전히 내 몫이다. 프롬프트 창을 잠시 닫고 코드베이스와 규칙 파일, 실패하는 테스트 하나를 들여다보는 시간이 바이브 코딩을 “데모용”에서 “운영용”으로 옮기는 경계라고 생각한다.

같은 주제로 이어 읽으려면 Supabase RLS 설정, MCP 프로덕션 가이드, 허브 페이지를 보면 된다. 질문을 남겨 주시면 다음 글에서 이어서 정리하겠다.