0. 들어가며
지난 글에서 UI/UX 개편 이슈 15건을 로컬 Aider에 위임하며 143번을 돌린 기록을 남겼습니다. 그때 얻은 결론은 프롬프트가 정밀해질수록 실패의 원인이 모델에서 나에게로 옮겨간다는 것이었습니다.
이번 글은 그 다음 사이클입니다. finance-tracker 가계부 앱의 시각 디자인 전면 개편을 처리했습니다. 외부 디자인 도구에서 나온 리뉴얼안을 받아 토큰 체계를 갈아엎고, 아이콘을 교체하고, 차트 몇 개를 새로 만들었습니다.
지난번과 다른 점이 하나 있습니다. 이번에는 넘겨받은 설계안 자체가 틀린 경우가 섞여 있었습니다. 그래서 실패의 원인이 세 갈래로 갈렸습니다. 모델, 나, 그리고 설계안.
측정한 것부터 적습니다.
1. 숫자
| 항목 | 값 |
|---|---|
| 처리한 이슈 | 9건 |
| 올린 PR | 12건 |
| 위임 횟수 | 8회 |
| 위임 산출물의 결함 | 6건 |
| 그중 내 지시가 원인 | 1건 |
| 그중 환경 문제 | 1건 |
| 테스트 증가 | 288 → 410 (서버 328 + 클라이언트 82) |
결함 6건의 내역은 이렇습니다.
| 유형 | 건수 | 재위임으로 해결 |
|---|---|---|
| 지시한 것을 빠뜨림 | 2 | 1회차 통과 |
| 범위 초과 (무관한 코드 삭제) | 2 | 1건 해결, 1건은 선별 적용 |
| 내 지시가 틀림 | 1 | 지시를 고쳐서 통과 |
| 도구 타임아웃 | 1 | 파이프라인 설정 변경 |
2. 지시가 틀렸다는 걸 알아채는 데 한 라운드가 더 들었다
이모지를 아이콘으로 바꾸는 작업이었습니다. 파일 상단 주석에 이런 문장이 남아 있었습니다.
// 스크린리더가 "돼지 저금통"처럼 이모지 이름을 읽어봤자 잡음이다.이제 이모지를 안 쓰니 이 문장을 고쳐야 했습니다. 그래서 이렇게 지시했습니다.
근거만 이모지에서 인라인 SVG로 바꿔 써라.
돌아온 결과입니다.
// 스크린리더가 "돼지 저금통"처럼 아이콘 이름을 읽어봤자 잡음이다.기계적으로 단어만 바꿨습니다. 그런데 아이콘 이름은 이제 savings, payments 같은 영문 식별자입니다. "돼지 저금통처럼 아이콘 이름을"이라는 표현이 성립하지 않습니다.
처음엔 모델이 대충 했다고 생각했습니다. 다시 읽어보니 아니었습니다. 저는 예시까지 손대라고 적지 않았습니다. "근거만 바꿔라"라고 했으니 예시는 그대로 두는 게 지시에 맞는 동작이었습니다.
플레이북에 이 경우가 이미 분류돼 있었습니다.
지시 자체가 틀렸음이 드러남
→ 재위임 전에 지시를 고친다. 모델은 잘못이 없다.
이 경우를 "위임 실패"로 집계하면 통계가 왜곡된다.
지시를 이렇게 고쳤습니다.
"돼지 저금통"은 이모지 이름이다. 이 파일은 더 이상 이모지를 쓰지 않는다.
그 예시를 문장에서 없애라. 예시 없이도 이유는 성립한다.
한 번에 통과했습니다.
지난 글에서 "실패가 지시하는 쪽으로 옮겨간다"고 썼는데, 이번엔 그 이동을 한 건 단위로 관측한 셈입니다. 중요한 건 이걸 실패 통계에 넣지 않는 것입니다. 넣으면 모델이 못 하는 일과 내가 못 쓴 지시가 한 덩어리가 되고, 다음에 무엇을 고쳐야 할지 알 수 없게 됩니다.
3. 프롬프트로 막히지 않는 것이 있다
두 번, 같은 형태로 당했습니다.
1000행이 넘는 파일에 "여기 한 곳만 고쳐라"를 시켰더니 지시와 무관한 섹션을 통째로 지웠습니다. 한 번은 설정 화면의 텍스트 버튼 세 개를, 다른 한 번은 대시보드의 「Top 5」 섹션 42줄을 날렸습니다.
두 번 다 프롬프트에 이렇게 적혀 있었습니다.
하지 말 것
- 다른 파일을 수정하지 마라.
- 위 세 곳 외에는 한 글자도 바꾸지 마라.
지켜지지 않았습니다. 그리고 **프롬프트를 아무리 고쳐도 같은 형태가 반복될 것**이라고 판단했습니다. 이건 지시의 문제가 아니라 큰 파일을 통째로 컨텍스트에 넣고 국소 편집을 요구하는 형태 자체의 문제로 보입니다.
그래서 파이프라인을 바꿨습니다.
큰 파일에 국소 편집을 위임할 때:
산출물을 그대로 받지 않는다.
지시한 추가분만 골라내 원본에 다시 얹는다.
삭제 헝크는 기본적으로 버린다 — 지시에 삭제가 없었다면 그것은 산출물이 아니다.
이게 "직접 고치지 않는다"는 원칙과 충돌하지 않느냐는 질문이 나올 수 있습니다. 저는 아니라고 봅니다. **새로 쓰는 코드가 없고, 범위 안의 것을 선별할 뿐**입니다. 모델이 만든 것 중 지시한 것만 취하는 통합 작업입니다.4. 넘겨받은 설계안이 자기 자신과 모순됐다
이번 사이클의 입력은 외부 도구가 만든 디자인 명세였습니다. 문서 네 개, 화면 16장. 그대로 구현하면 되는 줄 알았는데 검토하다 세 가지가 걸렸습니다.
4.1 같은 색을 두 이름으로 부르면서 섞지 말라고 했다
명세에 이런 문장이 있었습니다.
예산 주의(80%)에 pending을 쓰지 않는다. 둘을 섞으면 "아직 안 쓴 돈"과 "너무 많이 쓴 돈"이 같은 색이 된다.
옳은 지적입니다. 그런데 실제 값이 이랬습니다.
warn-fill #D9A63C
pending-fill #D9A63C ← 같음
warn-text #96650F
pending-text #96650F ← 같음문서가 막으려던 상황이 값 수준에서 이미 벌어져 있었습니다. 게다가 다른 절에서는 주의색을 "레드 계열의 옅은 단계, 노랑을 쓰지 않는다"고 설명하는데 #D9A63C는 hue 41도의 앰버입니다. 같은 값을 pending 쪽에서는 "화면의 유일한 따뜻한 색"이라고 부릅니다.
서술과 값이 어긋나 있었습니다. warn을 레드 계열로 다시 잡았습니다.
4.2 문서마다 대비값이 달랐다
명세는 WCAG 대비비를 표로 정리해 두었습니다. 친절해 보였는데, 같은 조합의 숫자가 문서마다 달랐습니다.
| 조합 | 문서 A | 문서 B |
|---|---|---|
#96650F on 흰 배경 |
5.04 | 5.25 |
| 다크 액센트 | 6.00 | 5.25 |
| 다크 버튼 글자색 | #101820 |
#12161C |
마지막 줄은 비율이 아니라 색상값 자체가 다릅니다.
전부 다시 계산했습니다. WCAG 상대휘도 공식을 그대로 구현해서 모든 조합을 실측했습니다. 그랬더니 명세가 놓친 AA 미달이 다섯 건 더 나왔습니다.
가장 뼈아픈 건 캡션 색이었습니다. 명세는 #5F7387이 흰 배경에서 4.90:1이라 통과라고 적었습니다. 맞습니다. 그런데 이 앱의 캡션은 흰 카드 위에만 얹히지 않습니다.
흰 카드 #FFFFFF → 4.90 통과
페이지 #F1F5FA → 4.47 미달
가라앉은 면 #EDF1F7 → 4.32 미달흰 배경 위에서만 재면 통과합니다. 실제로 얹히는 최악의 배경 기준으로 다시 잡았습니다.
4.3 결론은 맞고 근거가 틀린 경우
명세는 "흰 글자를 액센트 블루에 얹으면 3.71:1이라 버튼 배경으로 쓰면 안 된다"고 했습니다. 실측은 4.39였습니다. 그래도 본문 AA 기준 4.5에는 못 미치므로 결론은 유지했습니다.
이 셋을 정리하면서 배운 게 하나 있습니다. 외부에서 받은 명세의 숫자는 근거가 아니라 주장입니다. 재계산하는 데 30분이 안 걸렸고, 그 30분이 미달 다섯 건을 잡았습니다.
5. 없는 결함을 찾았다고 이슈를 열었다
이번 사이클에서 가장 부끄러운 대목입니다.
차트가 하나도 렌더되지 않는 현상을 발견했습니다. 파고들어 보니 컨테이너가 이렇게 남아 있었습니다.
<div class="recharts-responsive-container" style="width: 100%; height: 220px;">
<div style="width: 0px; overflow-x: visible;"></div>
</div>내부 폭이 0입니다. 접힌 섹션 안에서 폭 0으로 마운트되고 펼쳐도 복구되지 않는 것으로 판단했습니다. 가설을 일곱 개 세워 전부 배제했고, 재현 절차와 영향 범위를 정리해 이슈를 열었습니다.
나중에 다른 작업을 검증하다 이걸 봤습니다.
document.visibilityState → 'hidden'
window.innerWidth → 0
window.innerHeight → 0브라우저 패널이 숨겨져 있었습니다. 뷰포트가 0×0이면 폭 0을 재는 게 정상 동작입니다. 반응형 분기도 전부 깨져서, 데스크톱 전용 요소가 display: none으로 잡혔습니다. 제가 "이 요소가 안 보인다"고 판단했던 것들 상당수가 그냥 화면이 0픽셀이었기 때문입니다.
이슈에 정정을 남겼습니다.
기록을 남기는 이유는, 이 잘못된 진단을 근거로 "접힌 섹션에서는 차트를 마운트하지 말자" 같은 구조 변경을 하면 있지도 않은 문제를 고치는 셈이 되기 때문이다.
그리고 유효한 뷰포트(1280×900)에서 다시 쟀습니다.
recharts-surface 5개
recharts-bar-rectangle 9개
recharts-line-curve 2개
recharts-area-area 2개차트는 잘 그려집니다. 접힌 섹션을 펼친 뒤에도 문제없습니다. 그 이슈는 오진으로 확정하고 닫았습니다.
여기서 하나 더 배웠습니다. width: 0px 인 내부 div 는 유효한 뷰포트에서도 그대로입니다. recharts 가 크기를 재려고 두는 요소일 뿐, 실패의 징후가 아니었습니다. 저는 정상 동작 두 개를 겹쳐 놓고 결함이라고 읽었습니다.
다만 같은 재확인에서 진짜 버그 하나는 살아남았습니다. 같은 화면 같은 시점에 막대 9개, 라인 2개, 영역 2개가 그려지는데 파이만 <path> 가 0개입니다. 다른 차트가 전부 정상이므로 레이아웃도 환경도 아닙니다. 그 이슈는 유효한 것으로 확정하고 배제한 가설 여덟 개를 정리해 남겼습니다.
한 번의 재측정이 하나는 지우고 하나는 확정했습니다. 처음부터 제대로 쟀다면 지운 쪽에 들인 시간은 통째로 아꼈을 것입니다.
교훈은 단순합니다. 시각 검증을 시작하기 전에 뷰포트가 유효한지 먼저 잰다. 세 줄이면 됩니다.
6. 하네스가 조용히 아무것도 검증하지 않고 있었다
5번을 조사하다 곁가지로 하나 더 나왔습니다.
차트 렌더를 확인하는 테스트를 붙이려고 기존 셋업을 봤더니 이렇게 돼 있었습니다.
// recharts가 컨테이너 크기를 재는 데 쓴다. jsdom에는 없어서 차트가 0×0으로
// 렌더되고 자식이 아예 그려지지 않는다.
globalThis.ResizeObserver = class {
observe() {}
unobserve() {}
disconnect() {}
};주석은 문제를 정확히 알고 있습니다. 그런데 스텁이 콜백을 부르지 않습니다. 컨테이너는 크기를 영영 배우지 못하고, 차트가 든 화면은 테스트에서 늘 빈 채로 통과합니다.
고쳐서 크기를 통보하게 했더니 컨테이너까지는 만들어졌습니다. 하지만 도형은 여전히 비어 있었고, 여러 차트 유형이 전부 같았습니다. 즉 이 환경에서는 차트 렌더를 검증할 수 없습니다.
여기서 중요한 건 스텁을 고친 게 아닙니다. 셋업 주석에 이 문장을 남긴 것입니다.
다만 이것으로도 차트 내부까지 그려지지는 않는다. 차트가 실제로 그려지는지는 jsdom이 아니라 브라우저에서 확인해야 한다.
남기지 않으면 다음 사람이 같은 자리에서 같은 시간을 씁니다.
7. PR이 머지됐다고 표시되는데 코드는 없었다
의존관계가 있는 PR을 쌓았습니다. 토큰 체계가 base이고 그 위에 두 모듈이 올라가는 구조였습니다.
머지 순서를 이렇게 안내했습니다. "토큰 먼저, 그 다음 나머지 둘."
16초 간격으로 이런 일이 벌어졌습니다.
13:10:39 토큰 PR → develop (base 브랜치가 소진됨)
13:10:55 모듈 A → 소진된 base 브랜치
13:11:14 모듈 B → 소진된 base 브랜치토큰 PR이 develop으로 들어가면서 base 브랜치는 더 이상 아무 데로도 흘러가지 않게 됐습니다. 그 뒤 머지된 두 PR의 커밋은 그 죽은 브랜치에만 쌓였습니다.
GitHub은 둘 다 MERGED로 표시합니다. 실제로 머지되긴 했으니까요. 대상만 develop이 아니었습니다.
발견은 테스트 수로 했습니다. 머지 후 develop이 294건이었는데, 두 모듈이 들어갔으면 326이어야 했습니다.
이건 명백히 제 안내 부족입니다. "토큰 먼저"라고만 했고, 스택 PR은 깊은 것부터 머지해야 한다는 조건을 적지 않았습니다. 복구 PR을 따로 내면서 그 사실을 PR 본문에 적었습니다.
8. 종료코드는 아무것도 보장하지 않는다
지난 사이클 플레이북의 원칙 3이 이것이었습니다. "실행됐음을 종료코드가 아니라 산출물 변화로 확인한다." 셸 단어분할 때문에 위임이 실행조차 안 된 채 종료코드만 남은 사례에서 나온 규칙입니다.
이번엔 다른 도구에서 같은 형태가 나왔습니다.
리뷰어를 지정하는 CLI 명령이 PR URL을 출력했습니다. 성공처럼 보였습니다. 상태를 다시 조회하니 리뷰어가 비어 있었고, API를 직접 때려보니 이랬습니다.
422 Review cannot be requested from pull request author.1인 저장소라 작성자 본인을 리뷰어로 지정할 수 없었던 것입니다. CLI는 이 거부를 삼키고 URL만 뱉었습니다.
그리고 셸 단어분할 버그가 또 나왔습니다. 이번엔 이슈 생성 명령에서였습니다.
L="--label enhancement --label frontend"
gh issue create $L ... # 전부 단일 인자로 묶여 실패지난 사이클에서 두 번 반복한 그 버그입니다. 도구가 달라도 같은 자리에서 넘어집니다.
9. 그래서 무엇이 남았나
플레이북에 원칙 다섯 개를 더했습니다. 프로젝트 이름은 빼고 방법론만 남기는 별도 저장소에 씁니다.
| 원칙 | 요지 |
|---|---|
| 7 | 큰 파일의 국소 편집은 무관한 블록을 함께 지운다. 산출물을 그대로 받지 말고 선별한다 |
| 8 | 검증 환경이 오염되면 없는 결함을 만들어낸다. 시각 검증 전에 뷰포트를 먼저 잰다 |
| 9 | 하네스가 조용히 아무것도 검증하지 않을 수 있다. 검증하지 못하는 것을 문서에 남긴다 |
| 10 | 스택 PR은 깊은 것부터 머지한다. 애초에 스택을 만들지 않는 편이 낫다 |
| 11 | 명령이 성공을 보고해도 산출물은 안 바뀔 수 있다. 상태를 바꾼 뒤 다시 조회한다 |
10. 마치며
지난 글의 결론은 "프롬프트가 정밀해질수록 실패가 지시하는 쪽으로 옮겨간다"였습니다. 이번 사이클에서 그 명제는 유지됐지만, 옮겨간 실패가 향하는 곳이 하나 더 늘었습니다.
검증 자체가 틀릴 수 있다는 것.
없는 결함을 찾아 이슈를 열고, 가설 일곱 개를 배제하고, 재현 절차까지 정리했습니다. 그 모든 작업이 0×0 뷰포트 위에서 이뤄졌습니다. 결과물의 품질을 아무리 엄격하게 검사해도, 검사하는 자가 서 있는 바닥이 기울어져 있으면 소용이 없습니다.
위임을 늘릴수록 제가 하는 일은 코드를 쓰는 것에서 판정하는 것으로 옮겨갑니다. 그렇다면 다음에 정밀하게 만들어야 할 것은 프롬프트가 아니라 판정의 근거입니다.