AI가 내 플러그인의 80%를 작성했다. 6개월 후, 나는 그것을 유지보수할 수 없었다.
요약
AI 코딩 에이전트를 사용하여 작성된 코드의 유지보수성 저하 문제를 다룹니다. 설계 결정 사항이 채팅 기록에만 남고 코드에 반영되지 않는 문제를 해결하기 위해 결정 기록을 저장소에 남기는 워크플로우를 제안합니다.
핵심 포인트
- AI가 생성한 코드의 중복과 구조적 결함 방지 필요
- 설계 결정 이유(Why)를 코드 저장소에 기록할 것
- CLAUDE.md를 에이전트 지침 및 결정 기록용으로 활용
- docs/decisions.md를 통한 지속적인 의사결정 문서화
6개월 만에 그 플러그인을 다시 열었을 때 가장 먼저 발견한 것은 동일한 날짜 형식 지정 (date-formatting) 로직이 세 군데에 존재한다는 것이었습니다.
하나는 유틸리티 함수 (utility function)에, 하나는 클래스 메서드 (class method)에, 그리고 하나는 템플릿 (template) 안에 인라인 (inline)으로 있었습니다. 모두 조금씩 달랐습니다. 누군가 보고한 버그, 즉 가끔씩 어긋나는 디스플레이 문제는 세 개 중 가장 오래된 것에서 발생했습니다. 이를 수정하려면 먼저 어떤 복사본을 건드려야 하는지, 그리고 나머지 두 개도 수정해야 하는지를 파악해야 했습니다. 버그 자체는 어렵지 않았습니다. 내 코드의 형태를 읽어내는 것이 어려웠을 뿐입니다. 나는 AI가 코드의 약 80%를 빠르게 작성하도록 내버려 두었고, 명세서 (spec)를 작성한 적이 없었습니다.
나는 WordPress 플러그인을 제작하며 거의 매일 코딩 에이전트 (coding agents)를 사용하기 때문에, 누구에게 사용을 중단하라고 말하려는 것이 아닙니다. 이것은 속도를 유지하면서도 그 결과물을 계속 유지보수할 수 있는 방법에 관한 이야기입니다. 아래는 내가 변경한 사항들을 실제 도움이 된 정도에 따라 나열한 것입니다. 상위 두 가지만으로도 미래의 내가 훨씬 덜 고통스러워질 것입니다.
맥락을 위한 설정
버전은 빠르게 변하므로, 아래 이름들은 "내가 확인했을 때는 사실이었으나, 당신의 환경에서 확인하십시오"라고 간주하십시오.
- Claude Code (
claude --version) 및 Codex CLI (codex --version), 둘 다 매일 사용함 - PHP 8.3 / WordPress, 대상은 플러그인 및 테마
- 지속적인 지침 파일 (Persistent instruction files): Claude Code를 위한
CLAUDE.md, Codex CLI를 위한AGENTS.md
이 중 완벽한 워크플로우는 없습니다. 이것은 한 번 데인 사람이 그 후에 만들어낸 것입니다.
왜 6개월 뒤에 읽기 어려워지는가
해결책을 제시하기 전, 세 문장의 진단입니다. 속도가 문제는 아닙니다. 속도가 조용히 깎아먹는 것이 문제입니다.
설계 결정의 이유는 오직 채팅창에만 존재하며, 창을 닫으면 사라집니다. 당신은 차이점 (diffs)을 읽지 않고 수락하며, 따라서 코드가 마치 자신의 것처럼 느껴지게 만드는 시간을 전혀 쓰지 않습니다. 그리고 구조를 세션마다 에이전트에게 넘겨주기 때문에, 동일한 작업이 세 가지 다른 방식으로 작성됩니다. 그 세 번째 사례가 바로 나의 세 가지 날짜 함수입니다.
아래의 모든 내용은 그 세 가지 구멍 중 하나를 메우는 방법입니다.
1. 모든 결정을 저장소 (repo)에 기록하라
이 방법이 가장 큰 도움이 되었습니다. 채팅 속에서 증발해 버리는 "이유(why)"를 가져와 코드 옆에 두는 것입니다. 두 가지 습관이 있습니다.
첫째, CLAUDE.md의 용도를 변경했습니다. 이것은 단순히 에이전트(agent)를 위한 지침 파일이 아닙니다. 뒷부분은 미래의 나를 위한 결정 기록입니다. 무엇을 했는가가 아니라, 왜 했는지, 그리고 거절한 선택지와 그 이유를 기록합니다. 나중에 가장 중요한 것은 거절된 선택지입니다. 왜냐하면 미래의 나는 똑같은 "좋은 아이디어"를 떠올려 그것을 다시 만들다가 똑같은 구멍에 빠질 것이기 때문입니다.
## Rules (for the agent)
- Prefix every function, hook, and option key with `tmfs_`
- Stop and ask before changing any public API
...
둘째, 기능을 추가할 때마다 docs/decisions.md에 한 단락씩 작성합니다. 기능을 마친 후에 전체 사양서(spec)를 작성하려고 시도해 보았지만, 결코 지속되지 않았습니다. 기억에 의존해 작성된 사양서는 절반은 틀린 것이며, 작성하는 과정이 너무 무거워 계속 미루게 되었습니다. 기능을 마무리하는 추진력이 남아 있을 때 작성하는 한 단락은 실제로 작성하게 됩니다.
### 2026-06-05 Rate limit
- Cap outbound API calls at 20/min. Their limit is 30/min; left headroom.
- The agent said no limit was needed. Added it anyway; the queue jammed once before.
...
이 부분에 대한 경고가 있습니다. 에이전트에게 "결정 로그를 작성해 줘"라고 요청하면, 읽기에는 좋지만 실제 이유가 아니라 코드를 역공학(reverse-engineered)하여 만들어진 결과물을 얻게 됩니다. 그럴듯한 설명과 실제 이유는 비슷해 보이지만 서로 다릅니다. 초안을 가져온 다음, 당신이 실제로 생각했던 내용으로 다시 쓰세요. 6개월이 지난 시점에는 오직 실제 이유만이 도움이 됩니다.
2. 리뷰 게이트(review gate)를 작게 유지하라
읽지 않은 디프(diff)를 수락하는 습관을 깨기 위해 설정도 하나 변경했습니다.
저는 defaultMode: "acceptEdits"를 실행합니다 (이 설정에 대해서는 별도로 작성했습니다). 이 설정은 프롬프트(prompt)를 줄여주어 기분이 매우 좋지만, 유지보수 측면에서는 은연중에 코드를 읽지 않도록 유도합니다. 그래서 저는 과하게 교정하여 모든 것을 읽으려고 시도했습니다. 하지만 그것은 속도를 떨어뜨렸고, 저는 점심시간쯤 되어 포기했습니다. 극단적인 방식은 지속될 수 없습니다.
결국 정착된 방법은, 항상 읽어야 하는 짧은 디프 목록을 지정하고 나머지는 에이전트가 자동으로 수락(auto-accept)하도록 두는 것이었습니다.
항상 읽어야 하는 항목 (인간 게이트)
- Public API: 훅(hook) 이름, 함수 시그니처(function signatures), REST 경로(routes)
- DB 스키마(schema), 테이블(tables), 옵션 키(option keys)
...
이것들은 나중에 되돌리려면 비용이 많이 드는 변경 사항들입니다. 이름이 바뀐 훅은 호출자(callers)를 조용히 망가뜨리며, 누락된 이스케이프(escape)는 6개월 뒤 보안 취약점으로 나타납니다. 내부 리팩터링(refactors)이나 테스트 추가는 틀려도 비용이 적게 들거나 테스트가 이를 잡아낼 수 있습니다. 영향 범위(blast radius)를 기준으로 선을 그음으로써, 읽어야 할 부담을 계속 유지할 수 있을 만큼 작게 만들었습니다.
3. 구조와 명명(naming)은 직접 관리하기
'세 개의 복사본 문제(three-copies problem)'는 세션마다 에이전트에게 구조를 맡기는 데서 발생했습니다. 에이전트에게 내버려 두면 에이전트는 확장해 나갑니다. 새로운 함수, 새로운 파일, 이미 존재하는 헬퍼(helper)가 있는데도 두 번째 헬퍼를 만드는 식입니다. 그래서 저는 프레임(frame)을 고정하고 에이전트가 그 안에서 움직이게 합니다. 동일한 규칙을 AGENTS.md와 CLAUDE.md 모두에 넣습니다. 왜냐하면 하나에만 규칙이 있으면 도구를 전환하는 순간 스타일이 바뀌기 때문입니다.
## 명명 및 구조
- 최상위 레벨은 admin / public / includes입니다. 다른 것을 추가하지 마세요.
- 클래스(class)를 추가하기 전에 유사한 기존 클래스가 있는지 확인하세요.
...
"수행하지 말고 제안하라(propose, don't do)"는 부분은 제값을 합니다. 에이전트가 스스로 파일을 분할하면, 제가 찾고 있는 코드가 이동해 버려 찾을 수 없게 됩니다. WordPress 플러그인의 경우 접두사(prefix) 규칙이 이미 존재하므로, 제 머릿속에 있는 암묵적인 관례(implicit conventions)를 파일에 적는 것이 작업의 대부분입니다. 암묵적으로 남겨두면 에이전트에게도 전달되지 않고, 미래의 저에게도 전달되지 않습니다.
4. 주석과 커밋에는 오직 "이유(why)"만 담기
에이전트들은 주석을 추가하는데, 대부분의 주석은 코드가 무엇을 하는지(what)를 말합니다. 이는 6개월 뒤에는 쓸모가 없어지며, 코드는 바뀌었는데 주석은 바뀌지 않는 순간 거짓말이 되어버립니다. 리뷰 과정에서 저는 "무엇을(what)" 하는지에 대한 주석은 삭제하고, 코드 자체만으로는 보여줄 수 없는 "왜(why)"에 대한 내용을 추가합니다.
// 시그니처(signature)에 hash_equals를 사용합니다. ==는 타이밍 공격(timing attack)을 유발할 수 있으며
// 한 글자씩 깨뜨릴 수 있습니다.
if ( ! hash_equals( $expected, $given ) ) {
...
왜 ==가 코드 어디에도 작성되지 않았는지에 대한 이유가 없습니다. 미래의 내가 "여기서는 ==를 써도 괜찮겠어"라고 생각하며 코드를 단순화하려 할 때, 이 두 줄이 그 손길을 멈추게 합니다. 지침을 "주석을 추가하라"에서 "이유가 명확하지 않은 곳에만 주석을 달고, 동작을 설명하는 것은 생략하라"로 변경하면 노이즈를 줄일 수 있습니다.
커밋 메시지(Commit messages)도 마찬가지입니다. 에이전트(Agent)에게 맡겨두면 "Fix bug."라고 적힙니다. 첫 줄은 무엇을 했는지(이것은 위임해도 괜찮습니다)를 나타내며, 저는 본문에 왜 그렇게 했는지에 대한 문장을 한 줄 추가합니다.
fix: cut FX fetch off at a 3s timeout
Checkout was hanging on the FX API. Returning the page beats an exact rate.
몇 달 후 git blame이 해당 줄을 가리킬 때, 그 이유가 바로 거기에 있게 됩니다.
5. 테스트를 읽기 쉬운 명세(Spec)로 겸할 수 있게 하라
명세(Spec)가 작성되지 않는다면, 테스트가 명세가 되도록 하세요. 저는 테스트 이름을 테스트 대상 함수가 아니라 동작(Behavior)에 따라 명명합니다.
// 이전
public function test_verify() { ... }
...
테스트 목록을 훑어보는 것만으로 플러그인이 무엇을 약속하는지 읽을 수 있습니다. 문서와 달리 테스트는 동작이 변경되면 실패하기 때문에 내용이 어긋날(drift) 수 없습니다. 에이전트에게 테스트 작성을 시킬 때, 저는 "커버리지를 높여줘"가 아니라 "이것이 보장해야 하는 동작들을 동작 이름으로 작성해줘"라고 요청합니다. 내부 구현을 추적하는 테스트는 리팩터링(Refactor)할 때마다 깨져서 결국 주석 처리되고 맙니다. 외부를 확인하는 테스트는 약속을 지켜내며 살아남습니다. 6개월이 지난 시점에서, 살아남은 것은 오직 그런 테스트들뿐이었습니다.
WordPress는 실제 데이터베이스(DB)와 훅(Hooks)이 관여하기 때문에 이 중 일부를 구현하기 어렵게 만듭니다. 그런 경우에는 강요하지 않습니다. 대신 docs/ 폴더에 WP-CLI 시퀀스나 수동 체크리스트를 남겨둡니다. 목표는 커버리지 그 자체가 아니라, 6개월 뒤에도 살아남아 재현 가능한(Reproducible) 것을 남기는 것입니다.
여전히 효과가 있었던 가벼운 습관들
의존성(Dependency)을 추가할 때마다 결정 로그(Decision log)에 논리적 근거를 남기는 것입니다. 에이전트는 항상 최신 버전을 찾으려 하지만, 사용자가 오래된 PHP 버전을 사용하는 경우 최신 버전이 항상 안전한 것은 아닙니다. "8.0에서 실행될 수 있도록 8.1 이상의 문법이 필요한 라이브러리는 피함"과 같은 외부 제약 조건은 코드 자체만으로는 절대 드러나지 않습니다.
그리고 README 상단에 미래의 나를 위해 작성한 다섯 줄의 문장: 이것이 무엇인지, 어디서부터 읽기 시작해야 하는지, 결정 사항들이 어디에 있는지, 무엇이 쉽게 망가질 수 있는지에 대한 내용입니다. 에이전트(Agents)는 방대한 양의 README를 작성하지만, 방대하다는 것은 읽는 데 에너지가 소모된다는 뜻이며 결국 읽히지 않게 됩니다. 짧은 지도가 돌아오는 길(복기하는 과정)에서는 훨씬 더 유용합니다.
두 번째 에이전트가 유용한 한 가지
비교 대상이 아니라, 유지보수 도구로서의 활용입니다. 저는 Codex CLI가 Claude Code가 작성한 코드를 읽게 하고, 그것을 설명하며 유지보수의 취약점을 지적하도록 시킵니다 (그 반대도 마찬가지입니다). 작성자의 설명은 의도(intent)가 본인에게 보이기 때문에 느슨해지기 마련입니다. 반면 의도를 모르는 에이전트는 미래의 내가 그러하듯 코드를 냉정하게 읽습니다. 에이전트의 설명이 막히는 지점이 바로 미래의 내가 막히는 지점이며, 그곳이 바로 주석이 누락된 곳입니다. 한 번은 "이것은 훅(hook) 실행 순서에 의존하지만, 그 가정이 코드에 명시되어 있지 않다"라고 지적했는데, 이는 정확히 맞았습니다.
주의할 점: 두 번째 에이전트의 설명 또한 사실(fact)이 아닙니다. 존재하지 않는 캐시를 사용한다고 말한 적도 있습니다. 에이전트의 출력물을 당신이 간과한 것을 드러내는 수단으로 사용하되, 반드시 직접 검증하십시오.
제대로 정착하지 못한 것들의 솔직한 목록
사후 명세서(After-the-fact specs): 절반은 틀렸고, 너무 무거우며, 끊임없이 미뤄집니다. 모든 디프(diff) 읽기: 작업 속도를 따라가지 못했습니다. 제대로 된 ADR(Architecture Decision Record) 템플릿: 형식을 기억해내는 것 자체가 마찰(friction)이었기에 사흘 만에 버려졌습니다. 마찰은 기록되지 않음을 의미합니다. 모든 커밋에 무거운 이유 작성하기: 너무 과했습니다. 이제는 나중에 이유가 중요해질 때만 한 줄을 작성합니다.
공통점은 그것들이 너무 무겁거나 지나치게 완벽주의적이었다는 것입니다. 살아남은 것은 마치 편법을 쓰는 것처럼 보일 정도로 가볍게 만든 것들이었습니다. 부끄러울 정도로 작게 만들면 계속해서 실행하게 됩니다. 이것이 6개월의 시간이 내게 가장 명확하게 가르쳐준 것입니다.
다음의 나에게 남기는 메모
모든 것을 느리게 만들지는 않았습니다. 나는 오직 경계선(seams)에서만 멈춥니다. 무언가를 결정할 때, 경계를 변경할 때, 기능을 마무리할 때 말입니다. 그 지점에서 속도를 늦추고, '왜'에 대한 한 줄을 남긴 뒤, 나머지는 에이전트가 빠르게 실행하도록 두십시오.
놀라운 점은 "왜(why)"를 기록하는 것이 프롬프트(prompt)의 품질도 높여준다는 사실이었습니다. 내가 무엇을 결정하고 있는지 이름을 붙일 수 있게 되면, 에이전트(agent)에게 전달하는 지시 사항이 더 이상 모호하지 않게 됩니다. 미래의 나를 위해 남겨두는 기록이 조용히 현재의 나를 가속화해 줍니다.
여기 흩어져 있는 세 개의 날짜 함수는 여전히 세 개로 남아 있습니다. 이들을 하나로 합칠지, 아니면 전체를 폐기할지는 아직 결정하지 못했습니다. 하지만 지금 내가 작성하고 있는 코드는, 미래의 내가 길을 잃지 않고도 차근차근 살펴볼 수 있을 것이라 생각합니다. 낯선 이의 필체로 쓰인 집 안에 서서 오직 열쇠 하나만을 쥐고 있는 상황은, 처음 한 번이면 충분했습니다.
원문은 Zenn에 일본어로 작성되었습니다. 저는 WordPress plugins를 개발합니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기