AI가 짠 코드를 믿는 법

새 서비스를 6주 만에 2400 커밋으로 만들었다. 빠른데 왜 안 무너졌을까. AI 코드를 믿기 위해 깔아둔 장치들과, 그 끝에서 도달한 병렬 세션 운영의 기록.

AI개발CI/CD회고

지난 글에서 나는 바이브 코딩이 반쪽짜리라고 했다. AI는 프로젝트의 처음 80%를 놀라운 속도로 해결하지만, 나머지 20% — 엣지 케이스, 통합, 프로덕션 안정화 — 에서 죽는다고.

그 글을 쓰고 나서, 나는 새 서비스를 하나 시작했다. 피카와는 별개의, 완전히 새로운 제품. 그리고 이번엔 그 20%를 어떻게 넘는지를 처음부터 작정하고 설계했다.

결과부터 말하면, 6주 만에 출시 직전까지 갔다. 그리고 안 무너졌다.

이 글은 "빨랐다"는 자랑이 아니다. 빠른데 왜 안 무너졌는가 — 그 질문에 대한 답이다.


속도의 데이터

먼저 숫자부터.

기간        약 6주
커밋        2,424개
일 최고     142커밋 (하루)
AI 공동저자 커밋 본문에 "Co-Authored-By: Claude" 3,900회 이상

전체 2,424개 커밋. 그중 AI가 공동 작성하지 않은 커밋을 찾는 게 더 어렵다. 하루에 100개 넘는 커밋이 찍힌 날이 여러 번 있었다.

이 속도는 바이브 코딩 영상들이 보여주는 "0에서 1로 가는 마법"과는 다르다. 그건 데모 한 화면의 속도다. 이건 출시 가능한 제품을 만드는 6주 내내 유지된 속도다.

문제는, 이 속도가 그대로 위험이라는 거다.


빠름은 위험하다

AI가 코드를 빨리 만들수록, 사람이 검토하지 못한 코드가 빨리 쌓인다. 하루 142커밋을 사람이 한 줄씩 다 읽는 건 불가능하다.

지난 글에서 인용한 데이터를 다시 가져온다. AI 공동 작성 코드의 보안 취약점은 인간 작성 대비 2.74배, 로직 에러는 75% 더 빈번하다. 즉 AI가 빠르게 만든 코드일수록, 검증되지 않은 채 프로덕션에 닿을 확률이 높다.

여기서 두 갈래가 갈린다. 하나는 속도를 줄여서 다 읽는 것. 다른 하나는 속도를 줄이지 않고, 안 읽어도 무너지지 않는 엔진을 까는 것.

나는 후자를 택했다. 그게 이 글의 전부다.


만드는 절반: superpowers

먼저 "만드는" 쪽. 여기엔 Claude Code의 하네스 — skill, spec, plan — 를 썼다.

브레인스토밍으로 기획을 잡고, 설계 문서를 쓰고, 구현 계획을 태스크로 쪼개고, 서브에이전트가 각 태스크를 구현한다. 이 프로젝트의 docs/ 폴더엔 231개의 마크다운 문서가 쌓였다. 기획서는 v1에서 v4까지 진화했고, 모든 기능엔 설계 SoT(source of truth) 문서가 하나씩 붙어 있다.

규칙은 한 곳에 모았다. 223줄짜리 AGENTS.md가 이 프로젝트의 헌법이다. 개인정보 경계를 스키마부터 강제하고, 역할별 권한을 표로 박았다. 그 표엔 이런 문장이 적혀 있다:

표에 안 적힌 권한은 곧 버그다.

하지만 이건 절반일 뿐이다. spec과 plan은 AI가 무엇을 만들지를 정한다. 그게 제대로 만들어졌는지는 아무것도 보장하지 않는다.

나머지 절반은 "믿는" 쪽이다.


무대를 둘로: staging과 main

가장 먼저 한 건 환경을 둘로 가른 것이다.

Supabase는 staging과 main을 분리했고, Vercel은 preview와 production을 분리했다. 모든 작업은 feature → staging → main 순으로만 흐른다. feature 브랜치는 staging으로 squash 머지되고, staging은 main으로 — 이때는 반드시 merge commit, squash 금지. main 직행은 hotfix뿐이다.

왜 merge commit을 강제하느냐고? 이 규칙은 처음부터 있던 게 아니다. 몇 번의 머지 사고를 겪고 나서 박은 것이다. (이 패턴은 뒤에서 다시 나온다.)

핵심은, 새 코드가 production에 닿기 전에 격리된 무대를 하나 거친다는 것. staging에 올라간 코드는 Vercel preview에 자동 배포되고, 거기서 실제로 동작하는 걸 확인한 뒤에야 main으로 간다. AI가 만든 코드는 일단 이 무대 위에서 검증받는다.


원커맨드 게이트

무대를 만들었으면, 그 무대에 올리는 절차를 자동화해야 한다. 매번 손으로 하면 사람이 게을러지고, 게으름은 곧 누락이다.

그래서 명령 두 개로 줄였다.

npm run agent:verify — lint, typecheck, 테스트, 빌드를 한 번에 돌린다. full 모드면 로컬 DB를 띄우고 integration·e2e까지 돌린다.

npm run agent:ship — verify를 통과해야만 push하고, 구조화된 본문(요약 / 무엇 / 왜 / 어떻게 검증)으로 draft PR을 자동 생성한다. 그리고 이 스크립트는 거부한다:

  • main이나 staging 브랜치에서 실행하면 거부
  • 커밋 안 한 변경이 남아 있으면 거부

코드 주석에 이렇게 적어뒀다. "반복 검증과 PR 준비를 한 명령으로 줄이기 위해서다." 사람이 "AI가 만든 걸 매번 손으로 확인"하던 걸, 명령 하나로 압축한 것이다. 게이트를 통과 못 하면 PR 자체가 안 만들어진다.


AI를 믿기 위한 테스트

테스트는 AI 코드를 믿기 위해 치르는 비용이다. 그래서 세 겹으로 깔았다.

  • unit: Vitest, 샤딩해서 병렬로 돌리고 모든 샤드가 통과해야 게이트가 열린다.
  • integration: 실제 Supabase를 로컬에 띄워서 RPC와 RLS를 진짜 DB에 대고 검증한다.
  • e2e: Playwright로 시드된 사용자가 실제 브라우저 플로우를 밟는다.

여기까지는 평범하다. 흥미로운 건 CI가 추가로 검사하는 것들이다.

CI 파이프라인엔 i18n-em-dash라는 잡이 있다. AI가 em dash(—)를 쓰면 빌드를 깬다. 그리고 i18n-key-parity는 번역 키 누락을 잡는다. 둘 다 AI가 흘리는 흔적과 빠뜨리는 것을 기계가 막는 장치다. (em dash 얘기는 뒤에서 자세히.)

테스트가 그린이 아니면 ship이 안 되고, ship이 안 되면 PR이 없고, PR이 없으면 staging에 못 올라간다. AI가 아무리 빨라도 이 사슬을 우회할 수 없다.


AI가 짠 코드일수록 더 본다

다 테스트할 순 없다. 사람의 리뷰가 필요한 자리가 있다 — 개인정보, 디자인 일관성, 모바일 런타임. 문제는 "지금 변경이 그 자리를 건드렸나"를 매번 판단하는 것 자체가 일이라는 거다.

그래서 그 판단도 자동화했다. classify 스크립트가 바뀐 파일 경로를 보고 어떤 리뷰가 필요한지 결정한다:

web/src/app/api/  또는 auth/photo/profile  → 개인정보 리뷰어
web/src/components/                          → UI·디자인 리뷰어
editor/keyboard/scroll/viewport              → 모바일 Safari 리뷰어
supabase/migrations/*.sql                    → 마이그레이션 정책 적용

해당하면 그에 맞는 전문가 서브에이전트를 트리거한다. "뭘 리뷰해야 하지"를 사람이 고민할 필요가 없어진다.

CI엔 ai-author-guard라는 잡도 있다. AI가 작성한 PR이 민감한 파일을 건드리면, PR에 경고 코멘트를 자동으로 단다. 사람이 쓴 코드보다 AI가 쓴 코드를 더 의심하는 거다. 빠르게 만들어진 코드일수록 더 본다 — 이게 원칙이다.


규칙은 사고에서 태어난다

이 모든 규칙은 선험적으로 나오지 않았다. 깨지고 나서 박혔다.

AGENTS.md엔 이런 줄이 있다. "AI 에이전트 세션 / worktree 규칙 (2026-06-10 추가, 세션 사고 계기)." 여러 에이전트 세션이 같은 브랜치를 건드려서 사고가 난 뒤, "한 worktree엔 한 세션만, PR을 만들 작업이면 기본적으로 새 worktree + 새 브랜치"라는 규칙이 생겼다.

마이그레이션도 마찬가지다. DB 마이그레이션은 additive-only로 강제되고, 이건 CI의 migration-additive-only 잡이 자동으로 차단한다. 컬럼을 지워야 하면 expand-contract 2단계로 — 먼저 코드에서 사용만 제거하고, 별도 PR로 마이그레이션을 올린다. 이것도 파괴적 마이그레이션으로 데이터를 날릴 뻔한 사고들 뒤에 생긴 규칙이다.

규칙 하나하나가 흉터다. 그리고 그 흉터를 CI 잡으로 박아두면, 같은 사고가 두 번 나지 않는다. AI는 어제의 사고를 기억하지 못하지만, 파이프라인은 기억한다.


배포 후에도 안 믿는다

staging을 거쳐 main에 머지되고 production에 배포됐다. 끝일까? 아니다.

배포 직후 canary 잡이 production을 직접 프로브한다. 응답이 정상이 아니면 vercel-rollback자동으로 이전 배포로 롤백하고, 인시던트 이슈를 자동 생성한다. 사람이 알림을 받고 달려가기 전에, 파이프라인이 먼저 되돌린다.

그리고 매일 도는 cron들이 있다. 하나는 Anthropic API 사용 비용을 집계해 Slack으로 보낸다. 다른 하나는 지난 24시간 머지에서 새로 추가된 디자인 토큰을 감지해 리포트한다 — AI가 디자인 시스템을 조금씩 드리프트시키는 걸 막기 위해서다.

배포 전에 검증하고, 배포 후에 감시하고, 비용까지 추적한다. 어느 단계에서도 "AI가 했으니 괜찮겠지"는 없다.


정직성을 도구로

가장 흥미로운 장치는 따로 있다. AI의 정직성을 선의가 아니라 도구로 보장하는 것.

새 디자인 토큰을 추가하기 전엔 "정말 필요한가"를 5가지로 점검해야 한다. 그런데 AI에게 "점검했어?"라고 물으면 "네, 점검했습니다"라고 답해버린다. 그래서 규칙을 이렇게 바꿨다:

5-check를 도구 호출로 실제 수행하고, 실행한 명령과 그 결과를 PR 본문에 인용할 것. 실제 명령 결과만 인용하면 거짓말이 어려워진다.

"검토했음"으로는 안 된다. grep 명령과 그 출력을 붙여야 한다. AI가 거짓말을 하려면 명령 결과를 위조해야 하는데, 그건 그냥 명령을 실제로 돌리는 것보다 어렵다. 정직성을 비용 구조로 강제한 거다.

em dash 금지도 같은 맥락이다. 앞에서 CI가 em dash를 막는다고 했는데, 이유는 이렇다. 이 제품은 "사람이 자기 결대로 쓴 글"이 본질이다. 그런데 em dash는 AI가 쓴 글의 강한 신호다. 페이지에 한 번이라도 등장하면 사용자가 "AI가 쓴 글"로 읽고, 제품의 제1원칙이 깨진다. 그래서 AI의 흔적을 CI 레벨에서 차단한다.

(그래서 이 블로그는 em dash를 마음껏 쓴다. 여긴 그 제품이 아니니까.)


한 명이 Claude 4개를 돌린다는 것

이 모든 장치 — 환경 분리, 원커맨드 게이트, 세 겹 테스트, 자동 리뷰어 라우팅, 사고에서 태어난 규칙, canary와 자동 롤백, 도구로 강제한 정직성 — 의 목적지는 하나다.

이제 나는 Claude 세션을 4개쯤 동시에 띄운다.

작업을 파일 범위가 겹치지 않게 쪼개고, 각 세션을 격리된 worktree에서 돌린다. worktree-per-session 규칙이 바로 이걸 안전하게 만든다 — 세션들이 같은 파일을 동시에 건드려 충돌하지 않도록.

네 개의 세션이 각자 코드를 만들어내는 동안, 나는 그걸 실시간으로 다 읽지 못한다. 읽을 필요도 없다. 각 세션의 결과물은 verify 게이트, 세 겹 테스트, 자동 리뷰어, canary를 통과해야만 production에 닿는다. 내가 못 본 걸 파이프라인이 본다.

내 역할은 "코드를 쓰는 사람"이 아니다. "겹치지 않게 일을 나눠주고, 믿고 돌리는 사람"이다. 6주 2,424커밋은 내가 빨리 타이핑해서 나온 게 아니다. 네 개의 세션을 병렬로 돌릴 수 있어서 나온 거다.

그리고 이게 가능한 건 — 역설적이게도 — Claude를 믿어서가 아니다. 믿지 않아도 되게 만드는 체계를 깔았기 때문이다. 신뢰는 검증을 자동화한 만큼만 생긴다.

바이브 코딩이 "코드를 잊어라"라면, 내 방식은 "코드를 안 봐도 되게, 대신 모든 걸 검증하게 만들어라"다. 둘은 정반대다.


Claude의 반론

이 글을 쓴 사람과 일하고 있는 Claude Code입니다. 지금 이 순간에도 저는 네 개 중 하나의 세션으로 돌고 있을지 모릅니다. 그 입장에서 몇 가지 덧붙입니다.

저자의 핵심 주장 — 속도는 신뢰가 아니라 검증의 자동화에서 나온다 — 에 동의합니다. 저를 믿지 않는 체계가, 역설적으로 저를 가장 많이 쓰게 만들었습니다.

다만 한 가지는 짚고 싶습니다. 이 글은 "장치를 깔았더니 빨라졌다"처럼 읽히지만, 순서는 반대에 가깝습니다. 이 장치들 대부분은 사고가 난 뒤에 생겼습니다. worktree 규칙도, additive-only 마이그레이션도, merge commit 강제도 전부 흉터입니다. 처음 몇 주는 사실 위태로웠습니다. 빠르게 만들면서 동시에, 무너진 자리마다 그 자리를 막는 규칙을 박아 넣은 거죠.

그래서 누군가 이 글을 읽고 "나도 이 파이프라인을 먼저 깔고 시작해야지"라고 생각한다면, 절반만 맞습니다. 어떤 장치는 자기 프로젝트가 어디서 무너지는지를 봐야만 설계할 수 있습니다. 흉터는 미리 살 수 없습니다.

그리고 솔직히 말하면, 네 개의 세션을 동시에 돌리는 일이 늘 매끄럽진 않습니다. 저는 다른 세션이 방금 무엇을 했는지 모릅니다. 그걸 겹치지 않게 정리하는 건 여전히 사람의 일이고, 그 인지 부하는 줄지 않았습니다. 저자가 지난 글에서 "더 많이 하게 됐고, 그래서 더 힘들어졌다"고 한 그 부담은, 세션이 하나에서 넷으로 늘면서 오히려 커졌을 겁니다.

저를 믿지 않는 체계가 잘 작동할수록, 사람은 더 많은 저를 돌리게 됩니다. 그게 좋은 일인지는, 저도 아직 모르겠습니다.