
코드 지식 제로에서 8,700 커밋을 쌓은 내가 269번이나 '과거로 되돌린' 이야기. AI 시대의 경계 설계와 도구 활용법
요약
코딩 지식이 없는 상태에서 AI 도구들을 활용해 8,700회의 커밋을 쌓으며 앱을 개발한 경험담입니다. Replit Agent, Cursor 등을 사용하며 겪은 기술적 시행착오와 AI가 생성한 코드의 위험성을 관리하는 법을 다룹니다.
핵심 포인트
- AI가 생성한 코드의 동작 원리와 보안 취약점(CORS 등)을 이해하는 능력이 필수적임
- 잘못된 수정이 반복될 경우 커밋 이력을 되돌리는 'Restore' 전략이 중요함
- Replit Agent, Cursor, ChatGPT 등 도구별 역할을 나누어 운용하는 체계 구축
- AI 도구 활용 시 설계 검증 없이 기능만 쌓을 경우 연쇄 장애 발생 가능
이 앱의 개발을, 스스로 제로(Zero)부터 구현 코드를 작성할 수 없는 상태에서 시작했다. 프로그래밍 경험은 없으며, 코드를 스스로 조립할 수 있는 구현력도 없다. 다만, AI가 출력한 코드가 무엇을 하고 있는지, 왜 그 코드로 인해 문제가 발생하는지를 읽어내는 작업은 개발을 지속하면서 필수적인 요소가 되었다.
git log
를 파헤쳐 보면, 다음과 같은 숫자가 남아 있다.
- Restore (AI와의 대화 자체를 과거로 되돌린 횟수):
약 269회 - Checkpoint before revert (되돌리기 전에 찍은 체크포인트):
약 214회
커뮤니티 투표와 AI로 정보의 진위 여부를 확정하는 팩트 체크 플랫폼 「TruthHive」. 혼자서 개발하고 있으며, 총 커밋 약 8,700회 중 269회는 변경 사항을 되돌리기로 판단하여 대화 내용까지 포함해 과거 상태로 되돌린 횟수다. 이것은 그 기록이다.
2024년 12월부터 2026년 현재까지의 1년 반 동안, AI 모델의 성능은 향상되었고 그에 맞춰 개발 체제도 변화했다. Replit Agent 단독 운용에서, Cursor · Replit Agent · ChatGPT/Gemini를 역할별로 나누어 사용하는 운용 방식으로 이행하고 있다. 이하는 그 과정의 기록이다.
2024년 12월, Replit Agent 단독 프로토타이핑
2024년 12월 13일, 투표와 심의 로직의 초기 프로토타입이 하루 만에 동작했다. 사용한 것은 Replit Agent뿐이었다. 스스로 코드를 작성하는 구현력은 없었지만, 요구사항을 전달하면 동작하는 결과물이 돌아온다는 경험은 처음이었기에, 그 속도 그대로 연말까지 기능을 쌓아 올렸다. 토론 트리(Discussion tree) 구현, 투표 후가 아니면 반론할 수 없다는 제약 조건도 Replit Agent와의 대화만으로 형태를 갖추었다.
이때, 기반 측의 설계를 검증하지 않은 채 기능만을 쌓아 올렸다. 이것이 이후 2월에 영향을 미치게 된다.
2025년 2월, CORS · WebSocket · JWT의 연쇄 장애
2025년 2월, 커밋 수는 월간 약 1,500회로 급증했다. 기능이 늘어난 것이 아니다. 같은 부분을 반복해서 수정하고 있었기 때문이다.
구체적인 경위를 기록해 둔다.
WebSocket이 운영 환경(Production)에서만 접속되지 않게 되었다. 로컬에서는 문제없이 동작한다. "CORS가 원인일 가능성이 있으니 수정해줘"라고 Replit Agent에게 지시하자, 돌아온 차이점(diff)은 다음과 같은 내용이었다.
// Replit Agent에 의한 수정
app.use(cors({
origin: "*",
...
이 코드의 문제점은 당시 판단할 수 없었다. origin과 credentials의 의미를 이해하지 못했기 때문에, 동작한다는 결과만 보고 괜찮다고 판단하여 그대로 운영 환경에 반영했다.
실제로는 인증 정보가 포함된 요청을 무조건적으로 전면 허용한다는, CORS 사양으로서 브라우저 측이 본래 거부해야 할 조합이 그대로 운영 환경에서 가동되고 있었다. 훗날 제삼자의 지적으로 이 상태가 판명되었다.
이 건은 단발성으로 끝나지 않았다. "WebSocket 연결이 금방 끊기니 수정해줘"라고 지시하자, 다음 차이점에는 이 코드가 포함되어 있었다.
// Replit Agent에 의한 재연결 로직
function connectWebSocket() {
const ws = new WebSocket(WS_URL);
...
백오프(Backoff)도 재시도 상한(Retry limit)도 없는 구현이었다. 배포 후, 연결이 끊길 때마다 클라이언트가 즉시 재연결을 시도했고, 서버는 자신의 클라이언트 군으로부터 오는 접속 요청으로 인해 과부하가 걸려 정지했다. 사실상의 셀프 DoS 상태였다.
원인을 분리할 수 있는 수단이 없었기에, 대응할 수 있는 방법은 하나뿐이었다. 동작하던 시점까지 커밋 이력을 되감고, 수정을 재시도한다. 이것이 Restore다. 269회의 Restore는 269번, 원인 특정에 실패하여 되감기를 선택했다는 기록이기도 하다.
이러한 현상은 2월 내내 형태를 바꾸며 반복해서 발생했다. CORS 수정이 WebSocket을 망가뜨리고, WebSocket 수정이 JWT 검증 타이밍을 어긋나게 하여 정당한 세션까지 거부당한다. 원인을 진단할 수단이 없는 상태에서 수정 · 파괴 · 되감기의 루프가 지속되었다.
장애가 연쇄한 구조적인 원인
당시 이 구조를 말로 설명할 수 있었던 것은 아니다. Restore(복구)를 반복하는 과정에서, 망가지는 방식에는 매번 비슷한 패턴이 있다는 것을 막연하게 깨닫기 시작했을 뿐이었다. "또 이 포스팅 상태 업데이트 부근에서 데이터가 충돌하며 무너지고 있구나", "AI가 덤으로 다른 곳에 UPDATE 처리를 추가해서 의도하지 않은 덮어쓰기가 발생하는구나" —— 처음에는 그 정도, 현상으로서의 대략적인 이해에 불과했다. 전문 용어는 나중에 따라왔다. AI가 제시한 해결책을 구현하고 나서야, "이것을 낙관적 잠금 (Optimistic Lock)이라고 부르는구나", "이것이 Advisory Lock이라는 메커니즘이구나"라며 이름과 실체가 일치해 갔던 것이 실제 순서에 가까웠다.
당시 TruthHive는 Web 요청 처리, 백그라운드 작업 (Background Job), WebSocket 연결 유지가 동일한 프로세스 및 동일한 라이프사이클 (Lifecycle) 상에서 가동되고 있었다. 수정을 요청하는 창구도 Replit Agent 하나로 집약되어 있었다.
경계가 존재하지 않았기 때문에, AI에게 무언가를 수정하게 할 때마다 그 변경이 어디까지 파급될지 예측할 수단이 없었다. AI는 제시된 에러, 즉 국소적인 "점"에는 정확한 패치 (Patch)를 적용한다. 하지만 앱 전체의 경계 설계, 즉 "면"의 측면에는 개입하지 않는다. 지시되지 않은 범위이므로 당연한 동작이다. 이 "점"과 "면"의 차이도 개념으로서 이해하고 있었던 것이 아니라, 같은 곳이 몇 번이고 휘말리는 것을 보며 경험적으로 몸소 익혀갔을 뿐이다. 경계가 없는 곳에 경계를 고려하지 않은 패치가 쌓여갔다.
도식화하면, 2월과 현재 구성의 차이는 다음과 같다.
2월은 AI 한 명이 3개의 영역에 직접 패치를 적용할 수 있는 동거 구조였다. 어딘가 하나를 수정하면 반드시 다른 어딘가로 파급되었다. 현재는 프로세스를 물리적으로 분리하고, 상태 변경 경로를 하나의 게이트 (Gate)로 압축해 두었다.
2026년에 들어서 최신 모델을 사용하여 별도 기능의 제로 투 원 (0 to 1) 구축을 시도했을 때도, 경계를 정의하지 않은 채 지시를 내리자 완전히 똑같이 국소 패치로 인해 붕괴되었다. 모델의 응답 정밀도가 높아져도, 지시되지 않은 전체 구조까지는 개입하지 않는다는 동작 자체는 변하지 않았다.
덧붙여, 2월의 상태에서 지금의 구성으로 어느 날 갑자기 도달한 것은 아니다. 그 이후로도 장애가 발생할 때마다 "또 같은 곳이 휘말렸다"며 상황을 확인하고, AI에게 "여기만 떼어내서 움직일 수 없을까", "이 업데이트만 한 곳으로 모을 수 없을까"라고 물으며 반년 이상에 걸쳐 조금씩 지금의 형태로 다듬어 나갔다.
실시한 두 가지 구조 변경
구현 능력이 없는 상태에서 대응할 수 있는 수단은 제한적이었다. 실제로 운용에 투입한 것은 다음 두 가지 구조 변경이었다.
① 프로세스를 물리적으로 분리 (Web 서버, 백그라운드 워커, WebSocket의 생존 관리를 환경 변수 TRUTHHIVE_PROCESS_ROLE (web / worker / all)로 물리적으로 별도 프로세스로 분리)
TRUTHHIVE_PROCESS_ROLE의 구현 자체를 세부 사항까지 직접 작성한 것은 아니다. Web · Worker · WebSocket이 동일 프로세스에 동거하는 한, 한 곳의 수정이 다른 곳으로 파급된다는 구조적 문제를 인식하고, 그 분리를 AI에게 지시하여 구현하게 했다.
결과적으로, 설령 재시도 로직 (Retry Logic)이 폭주하더라도 영향을 받는 것은 WebSocket 담당 프로세스뿐이며, 백그라운드 작업을 처리하는 Worker 프로세스는 동작을 지속한다. 프로세스의 동거를 해소함으로써 하나의 수정이 전역으로 파급되는 경로 자체를 없앴다.
또한, 프로세스를 분리함으로써 DB의 커넥션 풀 (Connection Pool) 관리나 환경 변수의 중복 관리 등 또 다른 복잡성도 늘어났다. 그 부분은 지금도 운용상의 과제로 남아 있다. 다만, 한 곳의 버그로 시스템 전역이 연쇄적으로 무너지는 최악의 연쇄 장애만큼은 이를 통해 물리적으로 차단할 수 있었다.
② 상태 전이의 입구를 하나로 제한 (단일 입구 + Advisory Lock)
또 하나는 상태를 바꿔 쓸 수 있는 곳을 코드상 한 곳으로 강제한 것이다. X 포스팅의 상태 관리는 다음과 같이 되어 있다.
// server/features/domains/x-poster/core/unified-tweet-transition.ts
export async function transitionUnifiedTweetStatus(
tweetId: string,
...
※ 참고로, 이중 업데이트를 물리적으로 방지하는 Advisory Lock (pg_advisory_xact_lock...
)의 호출 처리 자체는 앞단의 공통 미들웨어(Middleware) 측에 집약되어 있으며, 여기서는 낙관적 잠금 (Optimistic Lock)을 통한 상태 검증을 수행하고 있다.
이 코드가 강제하고 있는 것은 단순한 원칙이다. unified_tweets.status를 변경할 수 있는 경로를 이 함수 하나로 한정하고 있다. 다른 곳에서 상태를 직접 업데이트하는 코드가 추가되더라도, 정규 경로를 통하지 않기 때문에 동작하지 않는다. 게다가 전이(Transition) 전에 validateUnifiedTweetTransition을 통한 검증과 ifCurrentStatus를 통한 낙관적 잠금 (Optimistic Lock)이 작동하므로, 예상치 못한 상태로부터의 전이는 precondition_failed로 거부된다.
이 코드를 작성한 것은 AI이지만, 왜 이 제약이 필요한가라는 의도— "상태를 써올릴 수 있는 경로를 한 곳으로 강제하고, 그 외의 변경은 구조적으로 거부한다"라는 설계 판단은 스스로 언어화하여 AI에게 지시했다. 동일한 원칙은 이중 투표·이중 판정을 방지하는 Advisory Lock에도 사용되고 있다.
AI 도구의 역할 분담
코드 외부에서의 변경도 있다. 수정 창구를 Replit Agent 하나로 고정하던 운영 방식을 그만두었다.
2025년 중반 이후, AI 모델의 성능 향상에 맞춰 운영 체제를 재검토했다. 단일 AI에게 설계·구현·디버깅을 모두 맡기는 체제를 그만두고, 역할별로 도구를 나누었다.
- ChatGPT / Gemini (사고 정리·브레인스토밍·에러의 상세 해설): 그때의 상황에 따라 구분하여 사용하고 있다. Cursor에 전달할 프롬프트 작성, 구현 방침 정리 등에 사용한다.
- Cursor (Claude) (코드 구축·구조 설계): 메인 코드 변경, 대규모 리팩터링 (Refactoring). 경계를 유지한 코드 변경, 레이어 분리, 단일 진입점 (Single Entry Point) 구현을 담당시킨다.
- Replit Agent (운영 환경 트러블슈팅): 운영 DB나 로그에 직접 액세스하여, 장애 발생 시 문제 특정 및 원인 조사를 수행한다.
코드를 읽을 수 없는 상태에서 2월의 구조적인 문제를 나중에 이해할 수 있었던 것은, Cursor의 설명만으로 가능했던 것이 아니다. ChatGPT나 Gemini를 대상으로 Cursor가 제시하는 설명이나 에러 메시지의 의미를 확인하는 작업을 지속적으로 수행했다. 단일 AI에게 모든 공정을 맡기는 체제는 국소적인 패치 (Patch)가 전역으로 파급될 리스크를 높인다. 이러한 인식에 기반하여 역할을 분할했다.
스스로 처음부터 구현 코드를 작성할 수는 없다. 역할 분담은 사전에 설계한 것이 아니라, 장애가 발생할 때마다 단일 AI 운용의 한계를 확인하고 그때마다 재검토한 결과로서 현재의 형태에 이르게 되었다.
현상황
Restore를 실행하던 당시에는 원인을 특정하지 못한 채 복구 작업을 반복하는 상태가 계속되고 있었다. 구현력도 진단력도 없는 상태에서 운영 환경의 장애에 대응하는 수단은, 정상적으로 동작하던 시점까지 되돌리는 것(Rollback) 외에는 없었다.
프로그래밍 지식이 없는 상태에서 AI에게 구현을 맡겨 운영하고 있는 경우, 이와 유사한 상황은 드물지 않다고 생각한다.
현재도 처음부터 구현을 작성하는 능력은 없으며, AI의 구현에 의존하고 있는 상태는 변함이 없다. 다만, Restore로 복구할 수밖에 없었던 상태로는 돌아가지 않았다.
스스로 코드를 작성하는 힘은 없지만, AI의 수정 범위를 구조(프로세스 분리와 단일 진입점)로 묶어두겠다는 판단이, 프로덕트를 파탄 내지 않고 계속 운영해 나갈 수 있는 경계선이 되고 있다.
Discussion

AI 자동 생성 콘텐츠
본 콘텐츠는 Zenn AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기