---
title: "7개월 바이브코딩 후 손코딩으로 돌아간 개발자의 결론"
published: 2026-05-11T01:23:51.000Z
canonical: https://jeff.news/article/2384
---
# 7개월 바이브코딩 후 손코딩으로 돌아간 개발자의 결론

한 개발자가 GPU 특화 쿠버네티스 TUI 도구 k10s를 7개월 동안 Claude 기반 바이브코딩으로 만들고, 결국 코드를 폐기한 뒤 처음부터 다시 쓰기로 했다. 빠른 기능 추가는 가능했지만 구조 설계, 상태 격리, 동시성, 타입 안전성에서 누적된 문제가 한꺼번에 터졌다는 경험담이다.

## 7개월 바이브코딩의 결론

- 이 글의 결론은 꽤 직설적임. “AI는 기능은 잘 만들지만, 아키텍처는 안 만든다”는 것
  - 저자는 GPU 특화 Kubernetes TUI 도구인 k10s를 7개월 동안 Claude와 바이브코딩으로 만들었음
  - 총 234커밋, 약 30번의 주말, Claude 토큰이 버티는 만큼 세션을 돌려가며 만든 프로젝트였음
  - 결과적으로 이 도구를 아카이브하고 처음부터 다시 쓰기로 함

- k10s는 원래 “GPU 클러스터 운영자를 위한 k9s” 같은 도구였음
  - NVIDIA 클러스터를 운영하는 사람이 GPU 사용률, DCGM 메트릭, 유휴 노드, 전력 사용량, 메모리 사용량을 터미널에서 바로 보게 하는 게 목표였음
  - 특히 시간당 32달러를 태우는 유휴 GPU 노드를 찾는 것처럼 꽤 실전적인 문제를 겨냥했음
  - Go와 Bubble Tea로 만들었고, 초반에는 실제로 잘 돌아갔다고 함

- 초반 바이브코딩은 말 그대로 마법처럼 느껴졌다고 함
  - “파드 뷰에 라이브 업데이트 추가해줘”라고 하면 Claude가 바로 구현함
  - 리소스 목록, 네임스페이스 필터, 로그 스트리밍, describe 패널, Vim 키바인딩까지 몇 주말 만에 붙었음
  - 기본적인 k9s 클론은 3주말 정도에 나왔고, 저자는 평소보다 10배 빠르게 만드는 느낌을 받음

- 문제는 핵심 기능인 GPU fleet view를 붙인 뒤 터짐
  - fleet view는 모든 노드의 GPU 할당량, 사용률, 온도, 전력, 메모리를 한 화면에 보여주는 전용 뷰였음
  - Claude는 이 기능도 한 번에 그럴듯하게 만들었음. GPU/CPU/All 탭, 할당 바, 색상 상태 표시까지 나옴
  - 그런데 다시 pods 뷰로 돌아가자 테이블이 비고, 라이브 업데이트가 멈추고, 노드 뷰에는 fleet 필터의 오래된 데이터가 남았음

> [!IMPORTANT]
> 저자가 폐기하기로 한 코드의 상징은 1690줄짜리 model.go였음. 하나의 Model 구조체가 UI, Kubernetes 클라이언트, 로그, describe, fleet, 캐시, 마우스 처리까지 전부 들고 있었음.

## 무너진 구조에서 나온 5가지 교훈

- 첫 번째 교훈은 “AI는 기능을 만들지, 구조를 만들지 않는다”임
  - k10s의 리소스 로딩 핸들러에는 fleet view만을 위한 특수 조건문이 generic path 안에 들어가 있었음
  - 새 뷰가 커스텀 동작을 필요로 할 때마다 기존 핸들러에 분기가 하나씩 추가되는 구조였음
  - 이전 뷰의 데이터가 새 뷰에 새는 걸 막기 위해 m.logLines = nil, m.resources = nil 같은 수동 초기화가 9곳이나 흩어져 있었음

- 저자가 제안한 대응은 AI에게 맡기기 전에 사람이 아키텍처 불변식을 직접 쓰라는 것임
  - 각 뷰는 독립된 View 인터페이스를 구현하고 다른 뷰의 상태에 접근하지 못하게 해야 함
  - 비동기 데이터는 AppMsg 같은 타입화된 메시지로만 들어오게 해야 함
  - 새 뷰 추가가 기존 뷰 수정을 요구하면 설계가 잘못됐다는 규칙을 CLAUDE.md나 AGENTS.md에 넣으라고 함

- 두 번째 교훈은 “AI의 기본 산출물은 god object”라는 것
  - 하나의 Model 구조체가 모든 상태를 들고, 500줄짜리 Update 함수가 110개 switch/case로 이벤트를 처리하고 있었음
  - s 키 하나도 로그 뷰에서는 autoscroll, pods 뷰에서는 shell, containers 뷰에서는 container shell로 동작함
  - Enter 키 처리도 contexts, namespaces, logs, generic drill-down이 전부 같은 flat switch 안에서 m.currentGVR.Resource 문자열 비교로 갈라짐

- 이 구조에서는 키 하나가 무슨 일을 하는지 지역적으로 알 수 없음
  - keyMap에는 로그 전용, describe 전용, fleet 전용, pods 전용 키가 한 구조체에 섞여 있었음
  - Autoscroll과 Shell이 둘 다 s 키를 쓰는 충돌도 “현재 리소스가 뭐냐” 조건문으로 버티는 식이었음
  - 새 기능을 붙이는 가장 쉬운 방법이 기존 전역 키 핸들러에 if문을 더하는 것이었고, AI는 당연히 그 길을 택함

- 세 번째 교훈은 “속도감이 스코프를 망가뜨린다”는 것
  - 원래 목표는 GPU 클러스터 운영자를 위한 좁은 도구였음
  - 그런데 AI가 pods, deployments, services, command palette, mouse support, contexts, namespaces를 쉽게 붙여주니 범용 Kubernetes TUI가 되어버림
  - 기능은 공짜처럼 보였지만, 실제 비용은 전역 상태와 조건문, 키 충돌, 복잡도 증가로 누적되고 있었음

> [!NOTE]
> 저자는 “AI는 무한한 구현 예산을 주는 게 아니라 무한한 라인 수 예산을 준다”고 표현함. 복잡도 예산은 여전히 사람이 관리해야 한다는 얘기임.

- 네 번째 교훈은 positional data가 시한폭탄이라는 것
  - k10s는 Kubernetes 리소스를 OrderedResourceFields []string 형태로 납작하게 만들었음
  - fleet 정렬 로직은 ra[3]을 Alloc, ra[2]를 Compute, ra[0]을 Name으로 가정했음
  - 컬럼 순서가 JSON 설정에서 하나만 바뀌어도 컴파일러는 아무 말 안 하는데 정렬과 렌더링은 조용히 틀어질 수 있음

- 저자는 구조화된 데이터는 render 직전까지 typed struct로 유지해야 한다고 말함
  - FleetNode라면 name, instance_type, compute_class, alloc 같은 필드가 있어야 함
  - 정렬은 row[3] 같은 인덱스가 아니라 node.alloc 같은 이름 있는 필드로 해야 함
  - AI는 []string이 테이블 렌더링에 바로 들어가니까 자주 선택하지만, 장기 유지보수에는 타입이 훨씬 강한 안전장치가 됨

- 다섯 번째 교훈은 “AI는 상태 전이를 소유하지 않는다”임
  - Bubble Tea의 기본 아이디어는 Update가 메시지를 받아 상태를 바꾸는 구조인데, k10s는 tea.Cmd 안의 goroutine에서 Model 필드를 직접 만졌음
  - 그 closure는 m.resources, m.table, m.viewWidth를 읽고 쓰는데 View도 메인 goroutine에서 같은 필드를 읽음
  - 락도 없고 mutex도 없으니 전형적인 data race가 생길 수 있는 구조였음

- 올바른 방식은 배경 작업이 UI 상태를 직접 바꾸지 않고 메시지만 보내는 것임
  - watcher, scraper, API call은 데이터를 가져와 typed message로 main loop에 넘겨야 함
  - 실제 상태 변경은 main event loop에서만 일어나야 함
  - render나 view 함수는 부작용 없이 순수하게 현재 상태를 그리기만 해야 함

## 그래서 저자는 뭘 바꾸나

- 저자는 k10s를 Rust로 다시 쓰기로 함
  - Rust가 무조건 더 좋아서라기보다, 본인이 Rust에서는 코드 냄새를 더 빨리 알아차릴 수 있기 때문이라고 설명함
  - AI가 그럴듯한 코드를 내놓을 때 사람이 “이거 이상한데?”라고 느끼는 감각이 여전히 필요하다는 얘기임

- 가장 큰 변화는 설계를 사람이 먼저 한다는 점임
  - 첫 프롬프트 전에 인터페이스, 메시지 타입, 소유권 규칙, 범위 제한을 문서로 적겠다고 함
  - AI가 잘못 결정하던 아키텍처 판단을 더 이상 즉석에서 맡기지 않겠다는 것
  - 바이브코딩을 버린다기보다, AI가 들어올 수 있는 경계와 규칙을 사람이 먼저 잡는 쪽으로 바뀐 셈임

```mermaid
sequenceDiagram
    participant 개발자
    participant Claude
    participant 전역Model
    participant UI뷰
    participant 배경작업
    개발자->>Claude: 새 기능 추가 요청
    Claude->>전역Model: 필드와 조건문 추가
    UI뷰->>전역Model: 같은 상태를 읽고 렌더링
    배경작업->>전역Model: goroutine에서 상태 직접 변경
    전역Model-->>UI뷰: 이전 뷰 데이터와 상태가 섞임
    개발자->>개발자: 구조를 읽고 재작성 결정
```

---
## 기술 맥락

- 이 글의 핵심 선택은 AI에게 기능 구현을 맡기더라도 아키텍처 결정은 사람이 먼저 고정해야 한다는 거예요. 왜냐하면 LLM은 보통 지금 프롬프트를 만족시키는 가장 짧은 경로를 찾고, 장기적으로 상태 소유권이 어떻게 썩어가는지는 직접 책임지지 않거든요.

- k10s에서 특히 위험했던 건 뷰별 상태가 분리되지 않았다는 점이에요. 로그, describe, fleet, 리소스 테이블이 하나의 Model 안에 있으면 새 기능을 붙일 때마다 기존 상태를 수동으로 비워야 해요. 이런 구조는 한 번만 초기화를 빼먹어도 이전 화면 데이터가 다음 화면에 섞이는 식으로 터져요.

- typed struct를 쓰라는 조언도 단순한 취향 문제가 아니에요. []string으로 테이블 데이터를 들고 있으면 row[3]이 정말 GPU 할당량인지 컴파일러가 확인해줄 방법이 없어요. 반대로 FleetNode.alloc처럼 이름 있는 필드를 쓰면 컬럼 순서가 바뀌어도 비즈니스 로직이 조용히 깨질 가능성이 줄어요.

- 동시성 규칙은 더 빡세게 봐야 해요. 배경 goroutine이 UI 상태를 직접 바꾸면 99%는 멀쩡해 보여도 1%에서 이상한 화면 깨짐이나 레이스가 나올 수 있어요. 그래서 배경 작업은 메시지만 보내고, 메인 루프만 상태를 바꾸게 하는 게 TUI든 GUI든 오래 버티는 구조예요.

## 핵심 포인트

- k10s는 234커밋, 약 30주말 동안 Claude 세션으로 만들어졌지만 1690줄짜리 model.go와 500줄 Update 함수, 110개 switch/case를 가진 거대한 상태 객체로 커짐
- AI는 기능을 빠르게 만들었지만 아키텍처를 스스로 잡지 못했고, 새 기능마다 기존 전역 상태와 키 핸들러에 조건문이 추가됨
- 저자는 CLAUDE.md나 AGENTS.md에 아키텍처 불변식, 상태 소유권, 범위 제한, 타입 기반 데이터 표현, 동시성 규칙을 명시해야 한다고 정리함

## 인사이트

이 글은 ‘AI 코딩 별로임’이 아니라 ‘AI가 잘하는 영역과 사람이 절대 놓치면 안 되는 영역’을 꽤 구체적으로 보여줌. 특히 장기 프로젝트에서 속도 지표가 복잡도 예산을 가려버린다는 대목은 팀 단위 AI 도입에도 그대로 적용됨.
