바이브코딩이 꼬이는 순간, 문제는 코드가 아니라 사이클이었다

Share
바이브코딩이 꼬이는 순간, 문제는 코드가 아니라 사이클이었다

처음에는 단순한 문제처럼 보였습니다.

goodtek website를 만들면서 VibeOps를 붙여 쓰고 있었고, 흐름도 그럴듯했습니다. TASK를 만들고, 브랜치를 따고, Cursor로 구현하고, 커밋하고, 푸시하고, MR을 만들고, 머지하면 끝.

그런데 이상하게도 끝난 것 같은데 끝나지 않았습니다.

task done을 실행했는데 git status는 여전히 dirty였습니다. MR은 올라갔는데 로컬 TASK 문서는 또 바뀌어 있었습니다. 머지한 뒤에 develop을 당겨도 TASK 상태가 마음속 상태와 달랐습니다.

처음엔 버그라고 생각했습니다.

그런데 코드를 보고 나니 조금 달랐습니다.
이건 단순 버그라기보다, 바이브코딩에서 사람과 CLI와 Git이 각각 어디까지 책임져야 하는지 정하지 않은 문제에 가까웠습니다.

문제는 “dirty가 생긴다”가 아니었습니다.
문제는 언제 누가 무엇을 끝냈다고 말하는지가 불명확하다는 점이었습니다.

이 글은 VibeOps v4로 넘어가면서 정리한 GitFlow 기반 바이브코딩 사이클 이야기입니다.
코드 구현 세부보다, 왜 task done을 없애고 add → ship → merge → sync로 나눴는지에 초점을 맞췄습니다.


바이브코딩에서 제일 먼저 무너지는 곳

바이브코딩을 하면 속도는 빨라집니다.

Cursor가 코드를 만들고, 문서를 정리하고, 테스트 계획도 도와줍니다. 그런데 속도가 빨라질수록 이상하게 더 자주 헷갈리는 지점이 생깁니다.

“지금 이 작업은 끝난 건가?”
“커밋은 누가 하는 건가?”
“MR은 열렸는데 TASK는 왜 아직 진행 중인가?”
“머지는 했는데 로컬 브랜치는 왜 남아 있지?”
“문서가 또 바뀌었는데 이건 커밋해야 하나?”

이 질문들이 쌓이면 코드보다 운영이 더 피곤해집니다.

특히 goodtek website처럼 실제 제품을 만들면서 VibeOps를 쓰다 보니, 단순히 “AI가 코드를 잘 짜느냐”보다 더 중요한 게 보였습니다.

바이브코딩에는 명확한 lifecycle이 필요합니다.

AI가 빠르게 움직일수록, 사람은 더 선명한 체크포인트를 가져야 합니다. 그렇지 않으면 Cursor, CLI, Git, GitHub/GitLab, 문서가 각자 다른 이야기를 하게 됩니다.


예전 흐름의 착시

기존 흐름은 대략 이랬습니다.

task add
  → plan/build
  → task done
  → merge
  → next task

겉으로 보면 단순합니다.
문제는 task done이라는 이름 안에 너무 많은 일이 들어 있었다는 점입니다.

구간

실제로 일어나는 일

헷갈린 이유

구현 종료

코드와 문서 수정

아직 로컬 변경

task done

커밋, 푸시, MR 생성, 상태 변경

“done”이 너무 많은 의미를 가짐

MR 머지

integration 브랜치에 반영

CLI 밖에서 일어남

머지 후 정리

develop pull, 브랜치 삭제

별도 단계로 보이지 않음

TASK 상태

Done, MR URL, doneAt 등

문서가 다시 바뀌며 dirty 발생

가장 큰 문제는 Done이라는 말이 너무 일찍 등장한다는 것이었습니다.

작업자가 보기에는 “개발이 끝났다”일 수 있습니다.
CLI 입장에서는 “PR을 만들었다”일 수 있습니다.
Git 입장에서는 “브랜치가 머지됐다”가 끝입니다.
제품 운영 입장에서는 “develop에 반영되고 다음 작업을 시작할 수 있다”가 끝입니다.

전부 다 끝이라는 말을 쓰지만, 끝의 위치가 다릅니다.

이 상태에서 task done 하나로 모든 걸 표현하려고 하면, 반드시 어딘가에서 의미가 새어 나갑니다.


dirty는 증상이었고, 원인은 상태 모델

가장 눈에 띈 증상은 git status였습니다.

task done 이후에 TASK md가 dirty로 남는 경우가 있었습니다. 이유는 단순했습니다.

MR을 만든 뒤에야 MR URL을 알 수 있습니다.
그 URL을 TASK 문서에 쓰면 파일이 바뀝니다.
그다음 Status를 Done으로 바꾸면 또 파일이 바뀝니다.
그 변경을 다시 커밋하지 않으면 로컬만 dirty가 됩니다.

처음에는 이걸 고치기 위해 “closure commit”을 추가했습니다.
즉, MR 생성 후 바뀐 TASK 메타데이터를 한 번 더 커밋하고 푸시하는 방식입니다.

이건 분명 개선이었습니다.

하지만 작업을 계속 보니 더 근본적인 질문이 남았습니다.

애초에 Done을 파일에 써야 하나?MR이 머지됐다는 Git 사실을 상태로 보면 안 되나?문서 상태와 Git 상태를 꼭 같은 파일에 섞어야 하나?

여기서 설계가 한 번 더 바뀌었습니다.


v4에서 버린 것: task done

상태 모델 심플리케이션

결국 VibeOps v4에서는 task done을 제거했습니다.

이름부터 문제였습니다.
done은 너무 편한 단어라서 오히려 위험했습니다.

개발이 done인지, PR 제출이 done인지, 리뷰가 done인지, 머지가 done인지, 로컬 정리가 done인지 구분되지 않았습니다.

그래서 하나의 큰 명령을 쪼갰습니다.

task add
  → 사람이 계획하고 구현
task ship
  → 원격에 제출하고 MR/PR 생성
task merge
  → integration 브랜치에 머지
task sync
  → 로컬을 integration 기준으로 정리
task add
  → 다음 작업 시작

이제 각 명령은 하나의 문장으로 설명됩니다.

명령어

의미

책임

task add

새 슬라이스 시작

TASK 생성, 브랜치 생성

task ship

리뷰 받을 상태로 제출

커밋, 푸시, MR/PR 생성

task merge

integration에 반영

gh/glab merge 또는 UI 머지

task sync

로컬 정리

develop pull, task 브랜치 삭제

task release

develop → main

제품 릴리스 흐름

핵심은 이겁니다.

ship은 “끝났다”가 아니라 “보냈다”입니다.merge는 “합쳐졌다”입니다.sync는 “내 로컬도 그 사실을 따라갔다”입니다.

이렇게 나누니 훨씬 덜 헷갈렸습니다.


상태는 두 개만 남기기

초기에는 상태도 많았습니다.

In Progress, Review, Done, Merged.
나쁘지 않아 보이지만, 실제로 써보면 상태가 많을수록 애매한 순간도 많아집니다.

그래서 TASK md에 쓰는 상태는 두 개만 남겼습니다.

TASK md 상태

의미

언제 바뀌나

In Progress

작업 중

task add

Shipped

제출 완료

task ship

머지 여부는 md에 억지로 쓰지 않습니다.
MR이 머지됐는지는 GitHub/GitLab이 이미 알고 있습니다.
브랜치가 삭제됐는지는 Git이 압니다.

즉, md는 사람이 읽는 작업 문서에 가깝게 두고, Git 사실은 Git과 호스트에서 판단합니다.

TASK 문서는 작업의 설명과 결과를 담고, lifecycle의 최종 판정은 Git 사실에서 유도합니다.

이 기준을 잡으니 task sync도 더 단순해졌습니다.

예전처럼 sync가 TASK md를 Done으로 바꾸지 않습니다.
그냥 integration 브랜치를 최신으로 맞추고, 끝난 task 브랜치를 정리합니다.


새 lifecycle을 한눈에 보기

Command Lifecycle Diagram

VibeOps v4의 일상 루프는 이렇게 정리됩니다.

vibeops
├─ task add
│  └─ TASK 생성 + task/* 브랜치
├─ HIL
│  └─ 계획, 구현, 로컬 테스트, 문서 확인
├─ task ship
│  └─ commit + push + MR/PR
├─ task merge
│  └─ integration 브랜치에 반영
├─ task sync
│  └─ develop pull + task 브랜치 정리
└─ task add
   └─ 다음 슬라이스 시작

여기서 HIL은 그냥 “사람이 중간에 본다” 정도가 아닙니다.

제품 판단이 필요한 지점은 일부러 자동화하지 않는다는 의미입니다.

예를 들어 Scope가 맞는지, Acceptance Criteria가 너무 넓지 않은지, Cursor가 만든 코드가 실제 의도와 맞는지, 머지를 해도 되는지 같은 판단은 여전히 사람의 일입니다.

CLI가 해도 되는 일과 사람이 봐야 하는 일을 나누면 다음과 같습니다.

구간

CLI가 해도 되는 일

사람이 봐야 하는 일

시작

TASK 번호, 브랜치 생성

작업 목표 확정

구현

없음

설계, 코드, 테스트 판단

제출

커밋, 푸시, MR 생성

Result, Test Result 확인

머지

명시적 merge 명령

리뷰, 승인, 충돌 판단

정리

pull, 브랜치 삭제

다음 작업 시작 여부

이 표를 만들고 나서야 머리가 조금 정리됐습니다.

바이브코딩에서 중요한 건 “AI에게 얼마나 맡길까”가 아니라, 어디에서 멈춰서 사람이 확인할까였습니다.


task ship이라는 이름을 고른 이유

push, submit, ship 중에 고민했습니다.

push는 Git 명령과 너무 가깝습니다.
실제로는 단순 push만 하는 게 아니라 MR/PR 생성까지 포함합니다.

submit은 정확하지만 조금 딱딱합니다.
도구 이름으로는 나쁘지 않지만, 빌드 로그의 리듬이 살짝 무겁습니다.

ship은 적당히 제품 개발 느낌이 있습니다.
다만 production deploy와 혼동될 수 있어서 의미를 고정했습니다.

VibeOps에서 ship은 배포가 아니라 “리뷰 가능한 상태로 보낸다”입니다.

task ship = commit + push + MR/PR
task merge = integration에 반영
task release = develop → main

이렇게 나누면 말이 덜 꼬입니다.

“이 TASK ship했어?”는 “MR 올렸어?”에 가깝습니다.
“merge했어?”는 “develop에 들어갔어?”입니다.
“release했어?”는 “main으로 냈어?”입니다.

작은 이름 차이 같지만, 매일 쓰는 CLI에서는 꽤 중요합니다.


task merge는 develop까지만

여기서 또 하나 정리한 원칙이 있습니다.

task merge는 main으로 가지 않습니다.
기본 대상은 integration 브랜치, goodtek website 기준으로는 보통 develop입니다.

main은 별도 release 흐름으로 둡니다.

흐름

대상

의미

task merge

task/* → develop

작업 슬라이스 통합

task release

develop → main

제품 릴리스

hotfix

main 기준 별도

나중에 분리 가능

이걸 섞으면 위험합니다.

TASK 하나를 바로 main에 넣는 흐름이 자연스러워질 수 있고, GitFlow의 장점이 흐려집니다. 그래서 일상 TASK 루프와 제품 릴리스 루프를 분리했습니다.

슬라이스는 develop에 모이고, 제품은 release로 나갑니다.


dirty를 없애는 게 목표는 아니었다

처음에는 git status clean이 목표처럼 보였습니다.

물론 clean은 중요합니다.
하지만 더 중요한 건 왜 dirty인지 설명 가능한 상태입니다.

v4에서 원하는 상태는 이렇습니다.

task add
  → In Progress

task ship
  → Shipped
  → MR/PR 생성
  → task 브랜치에 커밋

merge
  → integration에 반영
  → md는 그대로 Shipped

task sync
  → integration 최신화
  → task 브랜치 삭제
  → md 수정 없음

이제 task sync가 md를 다시 건드리지 않습니다.
즉, 머지 이후에 TASK 문서를 Done으로 바꾸느라 생기는 dirty는 사라집니다.

물론 사용자가 sync 이후에 05-current-state.md나 로그를 수정하면 dirty가 생깁니다. 그건 문제라기보다 새로운 변경입니다.

이 차이가 중요합니다.

도구가 만든 dirty와 사람이 의도적으로 만든 dirty는 다르게 봐야 합니다.


실제로 얻은 교훈

이번 업데이트에서 제일 크게 배운 건 CLI 명령을 많이 만든 게 아닙니다.

명령을 쪼개면서 작업의 의미를 다시 나눈 것입니다.

체크리스트로 줄이면 이렇습니다.

  • done처럼 여러 의미를 가진 단어는 조심한다.
  • MR 생성, 머지, 로컬 정리는 서로 다른 단계다.
  • TASK md는 사람이 읽는 상태를 담고, Git 사실은 Git에서 유도한다.
  • HIL은 감으로 넣는 게 아니라 명령 사이에 명확히 둔다.
  • dirty를 없애기보다, dirty가 생기는 이유를 예측 가능하게 만든다.
  • main 릴리스는 task merge와 분리한다.

이 정도만 정리해도 바이브코딩의 체감 안정성이 꽤 달라집니다.

Cursor가 코드를 잘 만들어도, 작업 흐름이 흐릿하면 결국 사람이 수습해야 합니다.
반대로 lifecycle이 선명하면, AI가 만든 변경도 훨씬 다루기 쉬워집니다.


v4의 최종 일상 루프

goodtek website 기준으로 지금 가장 자연스러운 흐름은 이렇습니다.

vibeops task add

# Cursor에서 계획, 구현, 테스트, 문서 확인

vibeops task ship TASK-NNN

# GitHub/GitLab에서 리뷰
# 또는 CLI로 명시적 merge

vibeops task merge TASK-NNN

vibeops task sync TASK-NNN

vibeops task add

가끔 제품 릴리스가 필요할 때만 별도로 갑니다.

vibeops task release

이제 “이 작업 끝났나?”라는 질문도 조금 더 구체적으로 바뀝니다.

질문

확인할 것

작업 중인가?

TASK md가 In Progress인가

제출했나?

TASK md가 Shipped인가

리뷰 중인가?

MR/PR이 열려 있는가

통합됐나?

MR/PR이 merged인가

로컬도 정리됐나?

task sync가 끝났는가

배포 대상인가?

release 흐름인가

이 구분이 생기니, VibeOps가 단순한 TASK 생성 도구가 아니라 바이브코딩용 작업 운영 레이어에 가까워졌습니다.


결국 남은 기준

이번 v4 업데이트를 강행한 이유는 기능 욕심 때문이 아니었습니다.

오히려 반대였습니다.
하나의 명령이 너무 많은 걸 알고 있었고, 하나의 상태가 너무 많은 의미를 담고 있었습니다.

그래서 줄였습니다.

task done을 없애고,
상태를 두 개로 줄이고,
머지는 Git 사실로 보고,
sync는 문서를 만지지 않게 했습니다.

이제 VibeOps의 사이클은 조금 더 말이 됩니다.

add는 시작, ship은 제출, merge는 통합, sync는 정리, release는 제품 반영.

이 정도로 쪼개 놓으니, Cursor와 함께 개발할 때도 사람이 어디서 개입해야 하는지 더 분명해졌습니다.

● goodtek은 앞으로도 이런 식으로 만들면서 고칠 생각입니다.
완벽한 워크플로를 먼저 설계하고 시작하는 게 아니라, 실제로 꼬인 지점에서 기준을 뽑아내는 방식입니다.

이번에는 dirty한 working tree가 그 신호였습니다.
다음에는 또 다른 작은 불편이 구조를 바꾸게 될지도 모릅니다.

Read more