계속해서 되풀이되는 버그
요약
Claude Code를 이용한 '바이브 코딩' 과정에서 발생하는 반복적인 버그 패턴과 그 한계를 분석합니다. 개별 수정은 정확하지만, 이전 작업의 맥락을 통합적으로 파악하지 못해 발생하는 연속성 결여 문제를 다룹니다.
핵심 포인트
- Claude Code의 개별 수정은 정확하지만 전체적인 맥락 유지가 어려움
- 채팅 기반 개발 방식은 이전 작업의 패턴을 인지하는 데 한계가 있음
- 연속적인 컨텍스트 부재가 반복적인 보안 설정 누락 등의 버그를 유발함
무언가 잘못되었다는 첫 번째 신호는 크래시(crash)가 아니었습니다. 그것은 하나의 패턴이었습니다.
blockly-platform은 제가 Claude Code를 사용하여 처음으로 엔드 투 엔드(end to end)로 구축한 실제 프로젝트였습니다. 이는 대학 프로그래밍 연습을 위한 Blockly 기반 플랫폼으로, 전적으로 Claude Code의 Telegram 채널을 통해 구동되었습니다. 에디터를 열지도 않았고, 제 로컬 머신에 리포지토리(repo)를 체크아웃(checked out)하지도 않았으며, 오직 채팅 스레드만 존재했습니다. 저는 제가 원하는 것을 설명했고, Claude Code는 제가 직접 본 적 없는 박스(box) 위에서 그것을 구축했으며, 저는 배포된 앱을 클릭하며 결과를 판단했습니다.
3월 22일, 홈 페이지가 빈 화면으로 나타났습니다. GET /api/exercises/published가 403 에러를 반환하고 있었습니다. 저는 채팅창에 이 사실을 말했고, 몇 메시지 후 Claude Code는 문제가 해결되었다고 했습니다. 해당 엔드포인트(endpoint)가 Spring Security의 permitAll() 리스트에 추가되지 않았던 것이 원인이었습니다. 저는 다음 단계로 넘어가 카테고리 필터를 시도했습니다. 역시 빈 화면이었고, 역시 403 에러였으며, 역시 동일한 permitAll() 리스트에서 누락되어 있었습니다. 동일한 파일, 동일한 유형의 수정, 다른 라인이었을 뿐입니다. 그다음은 연습 문제 상세 페이지였습니다. 같은 이야기였고, 같은 날, 세 번째 발생이었습니다. 사흘 뒤에는 '좋아요' 버튼이 작동하지 않았습니다. 근본 원인(root cause)은 또다시 동일했습니다. POST /api/exercises/*/like 역시 화이트리스트(whitelisted)에 등록되지 않았던 것입니다. 네 번, 하나의 파일, 하나의 반복되는 공백이었습니다.
이 중 그 어떤 것도 어려운 버그는 아니었습니다. 각각의 문제는 개별적으로 보면 유능한 엔지니어가 두 번 생각할 필요도 없이 한 줄로 수정할 수 있는 수준이었습니다. 패턴을 인지한 후 저를 괴롭혔던 것은, 문제가 발생하는 '그 순간'에는 그것을 알아차리지 못했다는 점이었습니다. 스크롤하며 살펴볼 디프(diff)도 없었고, 파일을 훑어보며 "잠깐, 우리 방금 똑같은 유형의 문제를 두 번이나 고치지 않았나?"라고 생각할 수도 없었습니다. 저에게는 채팅 로그와 만져볼 수 있는 라이브 앱뿐이었습니다. 제가 앉아 있는 위치에서 보기에 네 번째 수정은 첫 번째와 정확히 똑같아 보였습니다. 문제가 해결되었다는 메시지 말입니다.
그 순간 저는 문제가 모델에 있는 것이 아니라는 의심을 하기 시작했습니다. 문제는 모델도, 저도 아닌, 아무도 살펴볼 수 있는 '무언가'를 가지고 있지 않다는 점이었습니다.
채팅 전용 바이브 코딩(vibe coding)이 무너지는 이유
이 패턴이 단순히 "AI가 실수를 했다"는 것보다 더 흥미로운 이유는 다음과 같습니다. 네 번의 수정 사항 모두가 각각은 정확했다는 점입니다. Claude Code는 에러를 읽고, 누락된 permitAll() 항목을 찾아 추가한 뒤 다음 단계로 넘어갔습니다. 매번, 개별적으로 보았을 때 정확한 진단과 정확한 수정이 이루어졌습니다. 이 네 번의 턴 중 어느 하나를 따로 떼어 평가하더라도 통과할 것입니다.
실패한 것은 단일 결정이 아니었습니다. 네 개의 결정들을 서로 연결해 줄 무언가가 없었다는 점이 문제였습니다. 일주일 동안 같은 종류의 구멍 때문에 SecurityConfig.java를 세 번째로 건드리는 인간 엔지니어라면, "파일 전체를 한번 확인해 보자"라고 생각하며 멈춰 설 것입니다. 이는 그들이 Claude Code보다 똑똑해서가 아니라, 이전에 같은 경험을 해봤고 그것을 기억하기 때문입니다. 채팅 전용, 텔레그램 기반의 바이브 코딩(vibe coding)은 양측 모두에게 그러한 연속성을 제공하지 않습니다. Claude Code는 현재 턴의 에러를 보고 현재 턴의 에러를 수정합니다. 저는 "수정됨"이라는 메시지를 보고 제 할 일 목록의 다음 항목으로 넘어갑니다. 우리 중 누구도 시간이 흐름에 따라 파일의 형태를 유지하며 파악하지 못하는데, 그 이유는 우리 중 누구도 파일을 보고 있지 않기 때문입니다. 저는 휴대폰 화면을 보고 있고, 모델의 컨텍스트(context)는 해당 턴의 윈도우(window) 안에 있는 내용일 뿐, 이 파일에 반복적인 문제가 있다는 축적된 감각이 아닙니다.
이것이 첫 번째 층위입니다. 두 번째 층위는 저를 더 걱정스럽게 만들었는데, 왜냐하면 채팅 전용 모델 내부에는 명확한 해결책이 전혀 없었기 때문입니다. Claude Code의 결정 중 일부는 제가 승인하기 위해 저에게 돌아왔습니다. 하지만 저는 코드 구조를 읽지 않았습니다. 리포지토리(repo)를 열어본 적도, SecurityConfig.java를 본 적도, 연습 문제(exercises), 제출물(submissions), 그리고 채점(grading)이 어떻게 맞물려 있는지 본 적도 없었습니다. 그래서 결정 사항이 제 품에 떨어졌을 때, 저는 모델이 가진 것과 거의 동일한 정보, 즉 현재 메시지 외에는 아무것도 없는 상태로 승인을 하고 있었습니다. "인간 참여형(Human in the loop)" 방식은 인간이 모델이 갖지 못한 무언가를 가지고 있을 때만 감독 기능을 추가할 수 있습니다. 저에게는 그것이 없었습니다.
이 주장의 경계를 명확히 하자면, 채팅 전용 드라이빙 (chat-only driving)이 항상 틀렸다는 뜻은 아닙니다. 작고 일회성인 작업이라면 괜찮습니다. 추적할 가치가 있는 것이 없으니까요. 실패 모드 (failure mode)는 복잡성과 이해관계가 높아질 때, 즉 프로젝트에 움직이는 구성 요소가 충분히 많아져서 "시간에 따른 파일의 형태"를 실제로 추적할 가치가 생길 때 구체적으로 나타납니다.
해당 리포지토리 (repo)는 단순히 제 기억뿐만 아니라 다른 방식으로도 이를 뒷받침합니다. blockly-platform은 빈 리포지토리에서 23일 동안 80번의 커밋 (commit) 만에 병합된 작동 가능한 플랫폼이 되었습니다. 매우 빨랐고, 그 결과가 고스란히 드러납니다. 그 어디에도 설계 문서 (design doc)가 없었고, 아키텍처 노트 (architecture note)도 없었으며, 왜 특정 결정이 내려졌는지를 기록한 것도 아무것도 없었습니다. 오직 RUNBOOK.md, TODO.md, 그리고 사후에 발견된 사항들의 목록처럼 읽히는 ERROR_LOG.md만이 존재할 뿐이었습니다. Claude Code에게 "엔드포인트 (endpoint)를 추가할 때 보안 설정을 잊지 마세요"와 같은 프로젝트 자체의 규칙을 알려주었을 CLAUDE.md 파일은 4월 2일이 되어서야 존재하게 되었습니다. ERROR_LOG.md에 기록된 날짜가 있는 모든 항목, 즉 네 번의 permitAll() 누락을 포함한 총 18개의 항목은 모두 3월 22일에서 3월 28일 사이에 집중되어 있습니다. 가드레일 (guardrails)은 버그를 방지하기 전이 아니라, 방지했어야 할 버그들이 발생한 후에야 도착했습니다.
재구축 (The rebuild)
이를 대체한 프로젝트인 programming-learning-platform은 blockly-platform의 풀 리퀘스트 (pull request)가 병합된 바로 그날 시작되었습니다. 이것은 제가 의도적으로 설계한 우연이 아닙니다. 첫 번째 프로젝트가 끝나고 그로부터 얻은 교훈이 적용되기 시작한 실제 시점입니다. 또한 이것은 별개의 아이디어도 아닙니다. 이 프로젝트 자체의 CLAUDE.md는 이를 Blockly와 Python 연습을 위한 플랫폼이라고 설명하며, 이는 완전히 새로운 것을 처음부터 시작한 것이 아니라 동일한 도구를 바닥부터 다시 구축한 것입니다.
이번에는 두 가지를 동시에 변경했습니다. 첫째는 가시성(visibility)입니다. IntelliJ IDEA의 원격 개발(remote development) 모드로 전환했는데, 프로젝트를 체크아웃하여 이전과 동일한 종류의 원격 서버(remote box)에서 실행하면서도 이제는 실제 에디터가 연결된 상태였습니다. 덕분에 파일 트리(file tree)를 볼 수 있었고, 클래스를 열거나 코드가 반영되기 전에 차이점(diff)을 읽을 수 있었습니다. 둘째는 프로세스(process)입니다. superpowers 기술을 활성화하고 이를 프로젝트의 CLAUDE.md에 타협 불가능한 원칙으로 작성했습니다. "모든 작업: 브레인스토밍(Brainstorm) → 계획(Plan) → 구현(Implement, TDD). 브레인스토밍 생략 금지. 계획 없는 코드 작성 금지. 실패하는 테스트가 작성되기 전의 구현 금지."라고 명시했습니다. 또한, 첫 번째 시도에서 충분히 명확하지 않았을 경우를 대비해 프로젝트의 명시적인 레드라인(red lines) 항목 아래에 "Superpowers 생략 금지"를 다시 한번 기재했습니다.
그 차이는 단순히 제가 느낀 감각이 아니라, 저장소(repo)의 형태에서 나타납니다. programming-learning-platform은 458개의 커밋(commit)이 쌓여 있으며, 3개월이 지난 지금까지도 여전히 활발하게 운영되고 있습니다. 여기에는 실제 docs/ 트리가 존재합니다. 제품 요구사항 문서(PRD), 역할별 사용자 흐름(user flows), 아키텍처 문서(architecture doc), 우선순위별로 나뉜 기능 명세서(feature specs) 등이 포함되어 있습니다. 그리고 docs/superpowers/plans/ 디렉토리에는 첫 2주부터 이번 달 초까지 작성된 날짜가 기록된 44개의 구현 계획(implementation plans)이 보관되어 있습니다. 각 계획은 해당 코드가 작성되기 전에 수행된 브레인스토밍 및 계획 단계의 기록입니다. blockly-platform의 유일한 기록이 이미 잘못된 일들의 로그(log)뿐이었다면, 이 프로젝트는 구현되기 전 의도적으로 순서에 따라 내려진 결정들의 기록(paper trail)을 가지고 있습니다.
이 방식은 가장 중요한 측면에서 효과가 있었습니다. 제가 드디어 실제로 읽을 수 있는 차이점(diff)과 그 차이점을 검증할 계획(plan)을 갖게 됨으로써, 더 이상 눈먼 승인(approving things blind)을 하지 않게 되었기 때문입니다.
또한 예상치 못한 마찰(friction)도 있었습니다. 정확히 말씀드리자면, 이것은 제품 자체에 대한 판결이 아니라 특정 시점에 특정 설정으로 작업하며 겪은 저 개인의 경험입니다. IntelliJ IDEA의 원격 개발 클라이언트(remote development client)는 실제 연산이 완전히 서버에서 이루어짐에도 불구하고, 원격 작업을 위한 얇은 창(thin window) 역할을 하도록 설계된 것치고는 제 로컬 머신에서 여전히 무겁게 느껴졌습니다. 이는 제가 예상했던 것보다 눈에 띄게 더 무거웠습니다. 그리고 JetBrains Claude Code 플러그인은 가시성(visibility)이 제공해야 할 단 한 가지 요소인 '명확한 소통'을 방해하는 빈틈들이 있었습니다. 메시지에 이미지를 첨부할 수 없었습니다. 레이아웃이 깨진 스크린샷도, 주석이 달린 다이어그램도 보낼 수 없었습니다. 그래서 5초면 보여줄 수 있었을 것들을 설명하는 데 몇 단락의 글이 필요했고, 그 과정에서 설명이 틀릴 여지도 더 많아졌습니다.
가시성과 프로세스는 진짜 문제를 해결했습니다. 하지만 그것을 감싸고 있는 도구는 여전히 저와 싸우고 있었습니다.
저와 싸우지 않는 설정
유사한 원격 설정을 사용하던 한 친구가 프로젝트 중간에 IntelliJ 대신 VS Code를 사용해 보라고 제안했습니다. 구체적으로는 JetBrains 전용 원격 프로토콜과 자체 플러그인을 사용하는 대신, VS Code의 Remote-SSH 확장을 사용하여 원격 서버에서 programming-learning-platform을 직접 관리하고, 동일한 연결을 통해 Claude Code 확장을 실행하는 방식이었습니다.
가시성(visibility)이나 프로세스(process)에 관한 그 어떤 것도 변하지 않았으며, 바로 그 점이 핵심입니다. 파일 트리(file tree)는 여전히 존재했고, 제가 무엇인가를 승인하기 전에 디프(diffs)는 여전히 제 앞에 나타났으며, superpowers의 brainstorm-plan-TDD 루프는 변함없이 이어졌습니다. 왜냐하면 이 루프는 현재 열려 있는 에디터가 아니라 프로젝트의 CLAUDE.md에 살아있기 때문입니다. 변한 것은 이 두 가지를 감싸고 있는 도구였습니다. 클라이언트(client)는 더 가벼워졌습니다. IDEA가 그랬던 것처럼 제 로컬 머신(local machine)에서 눈에 띄게 무겁게 느껴지는 현상이 사라졌습니다. 그리고 Claude Code 확장은 제가 메시지에 스크린샷을 직접 첨부할 수 있게 해주었는데, 이는 시각적 블록 에디터(visual block editor)와 채점 UI(grading UI)를 중심으로 구축된 플랫폼에서 생각보다 훨씬 중요한 요소입니다. 망가진 Blockly 툴박스(toolbox)나 정렬이 어긋난 채점 패널(grading panel)은 말로 세 단락을 설명하는 대신, 5초간의 스크린샷과 두 단어짜리 질문만으로 해결될 수 있기 때문입니다.
저는 이전에도 왜 이 특정 조합 — VS Code의 Remote-SSH와 Claude Code 확장의 결합 — 이 장시간 지속되며 자리를 비워도 되는 작업에 신뢰할 만한 가치가 있는지 그 메커니즘에 대해 작성한 적이 있습니다: 프로세스는 노트북이 절전 모드로 들어가도 유지되며, 연산(compute)은 로컬 머신에 전혀 닿지 않습니다. 여기서 그 내용을 다시 반복하지는 않겠습니다. 이 글에서 중요한 것은 더 좁은 범위입니다. 이것은 가시성, 프로세스, 그리고 도구 자체가 세 가지 중 두 가지가 나머지 하나와 충돌하는 대신, 모두 같은 방향으로 나아간 첫 번째 설정이었습니다.
라스트 마일 (The last mile)
가시성과 프로세스가 모두 견고해진 상태에서, 마지막 조각은 제가 실제로 자리를 비울 수 있게 해주는 것이었습니다. 즉, Claude Code가 언제 저를 필요로 하는지 알고, 책상에 앉아 있지 않고도 응답할 수 있는 능력이었습니다.
그 _언제(when)_는 claude-code-notify입니다. 이는 터미널을 계속 지켜보고 있거나, 혹은 TDD (Test-Driven Development) 중심의 긴 턴(turn)이 제 승인을 기다리며 내내 막혀 있었다는 사실을 20분 뒤에나 깨닫는 상황에 지쳐서 제가 직접 만든 도구입니다. 이 도구는 정말 중요한 순간들, 즉 백그라운드 작업을 포함하여 한 턴이 진정으로 완료되었을 때, 혹은 저의 입력이 필요하거나 에러가 발생했을 때 정확히 Telegram으로 저에게 알림을 보냅니다. 그 _어떻게(how)_는 Remote Control (/remote-control)입니다. 이를 통해 노트북을 다시 여는 대신 휴대폰의 Claude 앱에서 프롬프트를 승인하고 질문에 답할 수 있습니다. Claude가 필요하다는 알림을 받고 주머니 속에서 답하는 것 — 이것이 바로 워크플로우(workflow)이며, 기저의 원격 세션(remote session)이 답변할 때까지 유지되지 못할 경우 조용히 실패할 수 있는 한 가지 방식에 대해서는 별도로 작성해 두었습니다.
이 레이어(layer)가 무엇이고 무엇이 아닌지에 대해 정확히 짚고 넘어갈 가치가 있습니다. 이것은 permitAll() 문제를 해결한 것이 아닙니다. 가시성(visibility)과 'superpowers'의 코드 작성 전 계획 수립(plan-before-code) 원칙이 그것을 해결했습니다. notify와 Remote Control이 추가하는 것은 둘 중 어느 하나도 놓치지 않으면서 키보드에서 떨어져(away) 있을 수 있는 능력입니다. 저는 여전히 diff를 확인하고, Claude Code는 여전히 계획을 따르지만, 단지 이 과정들이 계속 진행되는 동안 제가 그 자리에 앉아 있을 필요가 없을 뿐입니다. 첫 번째 프로젝트의 Telegram 전용 설정에 이와 동일한 '자리 비움 레이어(walk-away layer)'를 결합했다면, 아무것도 해결되지 않았을 것입니다. 그저 당신이 평가할 수 없는 결정이 이미 내려졌다는 알림을 더 빨리 받게 되었을 뿐일 것입니다. 순서가 중요합니다. 가시성과 프로세스가 우선이며, 자리 비움의 편의성은 그것을 대신하는 것이 아니라 그 토대 위에 구축되어야 합니다.
버그, 다시 한번
저는 네 번째 permitAll() 누락—좋아요 버튼에 있는 것—이 처음 발생했을 때가 아니라 나중 설정에서 발생했다면 어떤 일이 일어났을지 가끔 궁금합니다. 제 솔직한 추측은, 여전히 작성되었을 것이라는 것입니다. 왜냐하면 새로운 엔드포인트에 보안 규칙을 잊는 것은 사람이나 모델 모두에게 쉽게 할 수 있는 실수이기 때문입니다. 하지만 그것을 잡아내기 위해 세 번의 이전 발생이 필요하지 않았을 것입니다. 왜냐하면 계획 단계에서 '엔드포인트를 추가하고 그 보안 규칙을 추가'를 하나의 단위로 말했어야 했고, diff가 양쪽 절반 모두를 보여주었거나—아니면 제가 승인하기 전에 한쪽만이라도 보여주었어야 했기 때문입니다. 버그는 일주일간의 채팅 메시지에서 _패턴_을 알아차릴 인간을 필요로 하지 않았을 것입니다. 그것은 단지 _하나의 diff에 있는 공백(gap)_을 알아차릴 인간, 또는 모델을 필요로 했을 것이며—훨씬 더 작은 요구사항이고, 실제로 가시성이 가능하게 만드는 것입니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기