바이브 코딩, 화면은 예쁜데 왜 자꾸 터질까

자연어 프롬프트 하나로 웹앱을 뚝딱 만드는 바이브 코딩(Vibe Coding). 첫 프롬프트 결과물은 정말 감탄이 나올 정도로 깔끔해요. 그런데 두 번째, 세 번째 수정 요청을 넣기 시작하면 슬슬 앱이 이상해집니다.

가장 무서운 건 **사일런트 실패(Silent Failure)**예요. 화면상으로는 아무 문제가 없어 보이는데, 내부적으로는 완전히 망가진 상태죠. 예를 들어 장바구니의 '담기' 버튼을 눌렀을 때 "성공적으로 추가됨" 토스트가 뜨고 카운트도 올라가는데, 실제로는 DB나 스토리지에 아무것도 안 써지는 상황입니다.

이걸 디버깅하려면 결국 UI를 만지작거리고, 로그를 뒤지고, 급기야 생성된 코드를 직접 읽어야 합니다. 바이브 코딩의 취지와 정면으로 충돌하는 순간이죠. 이 글에서는 코드를 읽지 않고도 앱의 동작을 검증하는 접근법을 공유합니다.

AI coding agent generating a web app interface with hidden data flow bugs Development Concept Image

FlowCheck: UI 제약을 정적 분석 쿼리로 번역하다

컬럼비아대 DAP Lab에서 나온 FlowCheck는 "코드를 읽지 않고 앱을 검증한다"는 문제를 정면으로 다룹니다. 핵심 아이디어는 단순해요. 사용자가 UI에서 기대하는 동작을 형식 언어로 적고, 그걸 CodeQL 쿼리로 컴파일해서 실제 코드와 대조하는 겁니다.

4단계 파이프라인

  1. 제약 표현: 앱을 새 탭에 띄우고 오버레이 템플릿(When I take [action], these update: [component])에 UI 요소를 클릭해서 채웁니다.
  2. 형식 언어 변환: 템플릿을 P(event | condition) = [0, 1] 형태의 제약으로 바꿉니다. 예를 들어 프로모 코드 적용 시 총액이 반드시 쓰여야 한다면 P(w(total) | A(promo-input)) = 1이 되죠.
  3. CodeQL 컴파일: 제약의 AST를 순회해 액션, 이벤트 타입, 대상 요소를 뽑아내고, 이를 path_exists(경로 존재)와 all_paths_write(모든 경로에서 쓰기 발생) 두 개의 데이터 플로우 쿼리로 변환합니다.
  4. 검증: 생성된 코드에 쿼리를 돌려 위반 지점과 정확한 라인을 플래그합니다.
# 제약 예시: 프로모 코드 적용 시 total 쓰기가 반드시 발생해야 함
# P(write(total) | action(apply_promo)) = 1

# FlowCheck가 내부적으로 생성하는 CodeQL 쿼리 (의사 코드)
from Action a, Write w
where a.getName() = "apply_promo"
  and w.getTarget() = "total"
select a, w
// 모든 실행 경로에서 a -> w 도달 가능해야 통과

프론티어 모델과의 비교

Amazon, Twitter, Airbnb, Slack을 모델로 한 4개 앱에 30개의 미묘한 데이터 플로우 버그를 주입하고 실험했어요.

검증 방식탐지율비고
FlowCheck30/30 (100%)오탐 0건
Claude Opus 4.7 (상세 프롬프트)26/30 (87%)최고 베이스라인
DeepSeek V3 / Gemini Pro26/30 미만크로스 핸들러 취약

특히 LLM 심사자들은 핸들러 간 데이터 흐름에서 일관되게 무너졌습니다. 프로모 적용 핸들러가 장바구니 요약을 지웠는데 체크아웃 핸들러가 여전히 그걸 읽으려는 식의 버그죠. 두 핸들러가 서로를 직접 참조하지 않으니 모델은 각각을 따로 평가하고 연결 고리를 놓칩니다.

더 재밌는 건 프롬프트를 상세하게 쓰면 오히려 성능이 떨어질 수 있다는 점이에요. 디테일이 늘어나면 모델이 코드를 더 꼼꼼히 읽는 대신, 코드를 더 신뢰하게 되어 버그를 "의도된 동작"으로 합리화하기 시작합니다.

Developer verifying LLM-generated shopping cart app with static analysis queries IT Technology Image

주의사항: 만능은 아니다

FlowCheck가 100%를 찍었다고 해서 모든 상황에 쓸 수 있는 건 아닙니다. 몇 가지 한계를 짚고 갈게요.

  • 제약을 사람이 써야 한다: 결국 "무엇이 올바른 동작인가"를 사용자가 정의해야 합니다. 요구사항이 모호하면 검증도 모호해져요.
  • CodeQL 의존성: 정적 분석 엔진 위에 얹혀 있으니 지원 언어와 프레임워크에 제약이 있습니다. React/JS 생태계 중심으로 검증된 연구라는 점을 감안하세요.
  • 동적 상태·비동기 경합은 못 잡음: 정적 분석은 실행 시점의 레이스 컨디션이나 타이밍 이슈를 잡아내지 못합니다.
  • UI-백엔드 경계: 클라이언트에서 서버 API로 넘어가는 구간의 실제 저장 여부는 별도 통합 테스트가 필요합니다.

정적 분석은 "코드가 의도대로 흐르는가"를 결정론적으로 확인하는 도구이지, "제품이 옳은가"를 판단하는 도구는 아닙니다.

한국 개발 생태계에서의 적용 맥락

국내 SI·스타트업 환경에서 바이브 코딩은 빠른 프로토타이핑 용도로 이미 널리 쓰이고 있어요. 문제는 납품 후 유지보수입니다. 외주 개발에서 AI 생성 코드를 그대로 넘기면, 발주처는 사일런트 실패를 발견할 방법이 마땅치 않죠. FlowCheck 같은 제약 기반 검증을 QA 프로세스에 끼워 넣으면 "화면은 되는데 저장이 안 되는" 유형의 클레임을 상당히 줄일 수 있습니다.

다만 사내 레거시(JSP, PHP 기반)가 많은 환경에서는 CodeQL 적용 범위가 제한적이니, 신규 프로젝트나 React/Next.js 기반 코드베이스부터 도입하는 걸 권장해요.

CodeQL data flow graph tracing UI action to storage write in generated code Dev Environment Setup

마무리: 다음 단계

바이브 코딩의 진짜 병목은 "생성"이 아니라 **"검증"**입니다. FlowCheck는 그 병목을 UI 수준의 의도 표현으로 우회하는 영리한 접근이에요. 실무에 바로 적용하기 어렵더라도, 이 아이디어 자체는 팀의 QA 전략에 시사하는 바가 큽니다.

  • 당장 할 수 있는 것: 핵심 사용자 플로우마다 "이 액션 후 반드시 이 데이터가 저장돼야 한다"는 체크리스트를 만들고, E2E 테스트에 매핑하세요.
  • 다음 단계: CodeQL이나 Semgrep 같은 정적 분석 도구를 CI에 붙여 데이터 플로우 규칙을 최소한으로라도 강제하는 습관을 들이면 좋습니다.
  • 연구 트렌드: LLM을 심사자로 쓰는 접근은 프롬프트가 상세해질수록 신뢰 편향이 생긴다는 점을 꼭 기억하세요.

근거자료: Coding Agents Keep Shipping Silent Failures — Here Is How to Catch Them

함께 보면 좋은 글

본 콘텐츠는 신뢰할 수 있는 출처를 바탕으로 AI 도구를 활용하여 초안이 작성되었으며, 편집자의 검토를 거쳐 발행되었습니다. 전문가의 조언을 대체하지 않습니다.