AI

그래프 엔지니어링: 에이전트에게 개발 맡기는 법

타입체크도 테스트도 다 통과했는데, 왜 화면을 열면 깨질까. 에이전트에게 개발을 맡기며 겪은 문제와 그걸 그래프 엔지니어링이라는 절차로 만든 과정을 정리했다. 화면 개발을 네 층으로 가르는 법과 시안 수치를 DOM 실측으로 대조한 방법을 함께 적었다.

에이전트가 컴포넌트를 짜고, 타입체크를 돌리고, 자기가 쓴 코드를 스스로 리뷰까지 했다. 전부 통과했다는 보고를 받고 브라우저를 열었더니, 화면에 들어가는 순간 예약 슬롯이 아래로 밀렸다가 제자리로 돌아왔다.

같은 양상이 반복됐다. claude-deck을 만들며 네 번의 Phase를 돌리는 동안, 파서가 이전 대화를 삼킨 것도 새로고침에 카드가 사라진 것도 유휴 알림이 승인 카드로 둔갑한 것도 전부 서버를 올리고 브라우저를 직접 만졌을 때만 드러났다. 정적 검사가 통과시킨 코드에서만 나오는 버그가 있다.

에이전트에게 개발을 맡기며 겪은 이 문제와, 그걸 그래프 엔지니어링이라는 절차로 만든 과정을 정리했다.


1. 배경: 테스트를 통과해도 화면이 깨지는 이유

셀프 리뷰와 코드만 검수할 때 생기는 문제

작성자 스스로 자기 코드를 검토하다 보니 사각지대를 놓쳤고, 그 검토마저 화면이 아닌 코드 텍스트에만 갇혀 있었다. 지시대로 적혔는지는 읽어서 알 수 있지만, 브라우저에서 제대로 그려지는지는 화면을 직접 열어 봐야만 안다.

타입체크와 단위 테스트가 걸러 주는 건 코드가 자기 규칙을 어겼는지까지다. 명세 자체가 현실과 어긋났거나, 상태가 도착하는 순서 때문에 첫 프레임이 밀리는 종류의 결함은 그 검사망을 그대로 통과한다. 앞의 슬롯 밀림도 타입은 멀쩡했고 테스트도 초록이었다.

에이전트 하나에서 그래프 구조로 바꾼 이유

그래서 작업을 에이전트 하나에게 통째로 맡기는 방식을 버렸다. 역할마다 노드를 따로 띄우고 그 사이를 의존관계로 잇는 쪽으로 짰는데, 이 절차를 그래프 엔지니어링이라고 부른다.

  • 작성자와 검토자를 나눠 자기 코드에 관대해지는 자리를 없애고, 검증 루프가 못 보는 각도는 다른 노드가 코드로 보게 한다.
  • 규모에 맞춰 그래프 단수를 정해 “작은 작업이니까 생략”이라는 예외를 없애고, 노드 수는 줄여도 빠지는 단계는 남기지 않는다.
  • 화면을 열기 전엔 끝내지 않아 타입체크와 테스트가 통과시킨 코드에서 나오던 버그를 완료 판정 앞으로 당겨 잡는다.

에이전트에게 일을 시키는 방식은 계획 전체를 하나에게 통째로 위임하거나, 작업을 노드(node)와 엣지(edge)의 그래프로 잘게 설계하는 쪽으로 갈린다.

  • 노드는 “Task 1 작성자”, “Task 1 리뷰어”처럼 역할을 잘게 쪼갠, 책임이 하나뿐인 에이전트다.
  • 엣지는 “리뷰어는 작성자의 diff를 받아 시작한다”, “Task 3은 Task 2가 끝나야 시작한다”를 선으로 잇는 의존관계다.
단일 에이전트
  요청 ──→ [ 작성 + 검토 + 검증을 한 컨텍스트에서 ] ──→ 결과

그래프
  요청 ──→ 작성자 ──diff──→ 리뷰어 ──→ 검증 루프 ──→ 결과
           └ 노드마다 컨텍스트가 새로 시작한다

단일 에이전트 대신 그래프 구조를 선택한 이유는 명확하다.

  • 컨텍스트 격리. 한 에이전트가 계획 전체를 끌고 가면 뒤로 갈수록 앞에서 정한 걸 잊는다. 노드마다 새 에이전트가 자기 태스크만 들고 시작하면 앞선 태스크의 맥락이 섞이지 않는다.
  • 작성자와 검토자의 분리. 자기가 쓴 코드는 자기가 관대하게 본다.
  • 병렬성 판단. 그래프를 그려 보면 무엇을 동시에 못 하는지가 먼저 보인다. claude-deck Phase 1에서는 두 계획이 파일 네 개를 공유해서 전부 직렬로 묶었다.

단일 에이전트 쪽 강점도 인정해야 한다. 컨텍스트가 하나로 이어져 있으니 노드 사이에 무언가를 넘길 일 자체가 없는데, 그래프는 그 전달을 매번 비용으로 치른다. 리뷰어를 띄워 놓고 기다리는 시간이 공짜가 아닌 데다, 실제로 두 번은 리뷰어 노드가 결과를 아예 돌려주지 않았다. 한 번은 노드가 죽어 있었고, 한 번은 일을 다 해 놓고도 전달만 비어서 재요청을 받고서야 결과를 보냈다. 노드를 나눈 대가는 이렇게 전달 실패라는 형태로 돌아온다.

그럼에도 그래프를 택했다. 비교한 것은 어느 쪽이 빠르냐가 아니라, 틀렸을 때 그걸 누가 발견하느냐였다. 전달이 비면 재요청으로 메울 수 있고 비용은 한 번 더 물어보는 정도지만, 검토자가 없어서 놓친 결함은 화면을 열기 전까지 아무도 안 본다.


2. 화면 개발: ‘화면 하나’를 4단계로 나누기

명세부터 실측까지 4단계로 쪼개기

화면 하나를 만드는 일은 하나가 아니다. 시안에서 수치를 읽는 일, 컴포넌트를 그리는 일, API 응답을 상태에 붙이는 일, 그게 브라우저에서 시안대로 나오는지 재는 일이 한 요청에 얽혀 있다. 병원 어드민 예약 현황판 작업이 그랬다. 잠금 구간 덮개를 그리는 일과 그 구간의 예약 건수를 서버에서 받아 배지로 띄우는 일은 같은 화면이지만 다른 일이다.

이걸 에이전트 하나에게 통째로 맡기면 뒤로 갈수록 앞에서 정한 규격을 잊어버리고, 버그가 나도 어디서 난 건지 가려내지 못한다. 화면이 밀렸는데 그게 스타일 문제인지 상태가 늦게 도착한 문제인지, 한 노드 안에서는 구분이 안 된다.

그래서 화면 개발은 네 층으로 갈라 노드를 붙인다.

1  기대값 뽑기   (Spec)
      ↓
2  화면 그리기   (UI)
      ↓
3  상태 붙이기   (Logic)
      ↓
4  실화면 재기   (Visual)
  • 기대값 뽑기. 시안에서 수치를 먼저 꺼내 확정한다. 모달 폭 1080, 스크롤 목록 컨테이너 518, 목록과 하단 버튼 사이 갭 24. 색도 여기서 RGB로 뽑아 둔다. 활성 텍스트가 (44, 55, 68), 비활성이 (171, 175, 180)이다. 이 단계에서 숫자를 안 뽑으면 4층에 가서 “회색으로 보인다”는 감상밖에 안 남는다.
  • 화면 그리기. 목 데이터만으로 렌더한다. API는 아직 안 붙인다. 목록이 찼을 때와 비었을 때가 각각 시안대로 나오는지가 이 층의 전부다.
  • 상태 붙이기. 응답과 이벤트를 컴포넌트에 결합한다. 잠금 구간에 예약이 1건이면 1명 배지가 뜨고 0건이면 안 뜨는 분기, 항목 추가 폼을 제출하면 목록에 반영되고 자동 체크되는 흐름이 여기서 붙는다.
  • 실화면 재기. 브라우저를 띄워 getBoundingClientRect로 재고, 1층의 기대값과 숫자로 맞춘다.

네 층이 항상 직렬은 아니다. 1층이 끝나 수치와 타입이 확정되면 2층과 3층은 파일이 안 겹치는 한 병렬로 돈다. 모달 UI를 그리는 노드와 폼 검증 훅을 쓰는 노드가 그런 경우인데, 둘 다 1층이 확정한 기대값만 보고 시작하면 되기 때문이다. 반대로 층을 건너뛰면 비용이 바로 온다. 화면이 안 그려진 상태에서 API부터 붙이면 콘솔 에러가 렌더 문제인지 응답 문제인지 섞여서, 에이전트가 엉뚱한 자리를 고치기 시작한다.

화면 그리기와 상태 연결을 분리해 버그 잡기

서두에 적은 슬롯 밀림이 이 분리로 잡혔다. 스타일 문제로 보였지만 원인은 3층에 있었다. 시간축 시작 시각이 API 응답으로 정해지는데, 응답 전에는 축을 기본값 09:00으로 그리고, 응답이 오면 07:00으로 축이 늘어나 이미 그린 내용이 밀린 뒤, useEffect가 뒤늦게 08:00으로 스크롤을 옮긴다. 세 프레임이 그대로 눈에 보였다.

그리는 층과 붙이는 층이 한 덩어리였으면 “왜 밀리지”에서 멈췄을 텐데, 갈라 놓으니 원인이 렌더가 아니라 응답 도착 순서라는 게 바로 나왔다. 응답이 다 온 뒤에 본문을 그리고 스크롤 조정을 useLayoutEffect로 옮겨 첫 프레임부터 scrollTop이 108로 고정되게 했다. 타입체크와 테스트로는 절대 안 걸리는 종류다.


3. 검증 및 스킬화: 작업 크기별 구분과 수치 검증

작업 크기에 따른 단계 구분과 절대 원칙

방법론을 스킬로 굳히면서 정한 구조는 단순하다. 아래 원칙은 작업 규모와 상관없이 고정이고, 그래프의 크기만 변한다.

  • 작성자와 검토자는 항상 분리한다.
  • 실화면 검증이 종결 조건이다. 그래프 통과는 완료가 아닌데, 에이전트는 명세를 코드로 옮기는 일까지는 잘해도 명세가 현실과 어긋났는지는 못 보기 때문이다.

처음엔 “규모가 작으면 그래프를 안 돌린다”로 설계했다가, 그러면 작은 작업은 검증도 빠지느냐는 질문에 답이 없어서 뒤집었다. 판정 기준을 “그래프냐 아니냐”에서 “몇 단으로 짜느냐”로 바꾼 것이다.

티어기준그래프
S태스크 1개, 파일 ~3개본세션 작성 → 독립 리뷰어 1노드 → 검증 루프
M독립 태스크 2~4개태스크별 작성자→리뷰어(→수정 1라운드) 파이프라인
L계획서 단위 5+ 태스크풀 그래프: 게이트 → 커밋 → 리뷰 2라운드 → 실패 전파 차단

같은 표를 노드와 엣지로 펴면 규모별 그래프의 모양이 이렇게 드러난다.

S   본세션 작성 ──→ 리뷰어 1노드 ──→ 검증 루프 ──→ 종결


M   작성자 1 ──→ 리뷰어 1 ──→ (수정 1R) ──┐
    작성자 2 ──→ 리뷰어 2 ──→ (수정 1R) ──┼──→ 검증 루프 ──→ 종결
    작성자 3 ──→ 리뷰어 3 ──→ (수정 1R) ──┘
    └ 파일이 겹치면 직렬, 안 겹치면 병렬


L   태스크1 작성 ─→ 게이트 ─→ 커밋 ─→ 리뷰1R ─→ 수정 ─→ 리뷰2R ─┐
    태스크2 작성 ─→ 게이트 ─→ 커밋 ─→ 리뷰1R ─→ 수정 ─→ 리뷰2R ─┴─→ 검증 루프 ─→ 종결
    └ 앞 태스크가 실패하면 뒤 태스크를 시작하지 않는다

세 그림에서 검증 루프는 전부 맨 끝에 한 번씩 붙는다. 규모가 줄면 노드 수가 줄 뿐, 빠지는 단계는 없다는 뜻이다. 앞 절의 네 층은 여기에 겹쳐 놓인다. 층은 무엇을 나눌지를 정하고, 티어는 그걸 몇 개의 노드로 펼지를 정한다. 모달 하나를 붙이는 M티어 작업이라면 1층이 확정한 뒤 2층과 3층이 각각 작성자·리뷰어 쌍을 갖고 병렬로 돌다가, 결합한 결과를 하나의 검증 루프로 닫는 모양이 된다.

눈대중 대신 DOM 실측값으로 검증하기

검증 루프에서 축소 스크린샷 육안 비교는 이미 한 번 실패했다. 그 비교로는 배경색 4% 차이, 버튼 변형, 배지 색을 전부 놓치거나 오판했기 때문에 규칙을 바꿨다. 시안 수치와 DOM 실측을 숫자로 대조한다.

병원 어드민 화면 작업에서 이 루프를 그대로 돌렸다. 디자이너가 시안에 적어 둔 규격을 기대값으로 놓고, 브라우저에서 getBoundingClientRect로 같은 자리를 재서 맞추는 방식이다.

항목시안DOM 실측
모달 폭10801080
스크롤 목록 컨테이너518518
목록→하단 버튼 갭24 / 2424 / 24
빈 상태 A 모달 전체 높이481483
빈 상태 B 모달 전체 높이570573

비활성 상태 판정도 눈 대신 픽셀로 했다. 시안 두 프레임에서 같은 버튼의 텍스트 색을 추출하면 (44, 55, 68)과 (171, 175, 180)이 나오는데, 앞이 활성 팔레트고 뒤가 비활성 팔레트다. “회색으로 보인다”는 감상 대신 RGB 값이 판정 근거가 된다. 두 프레임 중 어느 쪽이 규칙이고 어느 쪽이 시안 누락인지도 이 값으로 갈랐다.

기능 검증도 같은 루프다. 새로 붙인 항목 추가 폼은 실제로 값을 넣고 제출해서, 목록에 반영되고 자동 체크되는 것까지 브라우저에서 확인한 뒤 테스트 데이터를 지웠다.

그래도 2~3픽셀 차이는 남는데, 제목 영역의 라인하이트 계산이 시안과 브라우저에서 미세하게 다르기 때문이다. 이 정도는 오차로 받아들이고 넘어간다. 수치 대조의 목적은 0px 일치가 아니라 “지정한 값이 다른 스타일에 밀려 무시되는” 종류의 사고를 잡는 것이다.

키워드로 자동 실행되게 스킬 등록하기

이 방법론이 “개발해줘”라는 말에 자동으로 실리길 기대했는데 안 됐다. 알고 보니 훅 설정 쪽에 문제가 겹쳐 있었다. 프롬프트 키워드로 스킬을 켜 주는 훅이 스킬 디렉토리를 스캔하지 않고 하드코딩된 목록만 평가하고 있었고, 그 목록에 이 방법론은 없었다. 코드 그래프 컨텍스트를 주입하는 별개 훅도 있었지만, 그건 인덱스가 없는 git worktree에서 조용히 no-op이었다.

그래서 스킬 파일을 만들어 키워드 목록에 직접 등록했는데, 훅이 읽는 것은 이 딕셔너리 한 줄이다.

SKILL_KEYWORDS = {
    # ...
    "graph-engineering": ["개발", "구현", "만들어", "그래프엔지니어링", "그래프 엔지니어링", "graph engineering"],
}

“개발”, “구현”, “만들어”처럼 넓게 잡았다. 일상 단어라 오발동이 있겠지만, 스킬이 켜져도 티어 판정이 게이트 역할을 하므로 오발동 비용은 지시문 로드 정도다. 반대로 좁게 잡으면 정작 필요한 순간에 안 뜨는데, 이번이 그랬다.

스킬 본문에는 앞에서 정한 원칙과 티어 표를 그대로 박아 뒀다.

어떤 티어든 불변 원칙 두 가지를 지킨다:

1. **작성자와 검토자를 분리한다.** 자기가 쓴 코드는 자기가 관대하게 본다.
2. **실화면 검증이 종결 조건이다.** 그래프 통과 ≠ 완료.

| 티어 | 기준 | 그래프 |
| --- | --- | --- |
| S | 독립 태스크 1개, 수정 파일 ~3개 | 본세션 작성 → 독립 리뷰어 1노드 → 검증 루프 |
| M | 독립 태스크 2~4개 | 태스크별 작성자→리뷰어(→수정 1라운드), Workflow 실행 |
| L | 계획서/Phase 단위 5+ 태스크 | 풀 그래프(게이트·리뷰 2라운드·실패 전파 차단), Workflow 실행 |

이 표는 앞에 옮긴 표와 한 군데 다른데, 스킬 파일은 M·L에 “Workflow 실행”을 붙여 뒀지만 본문 표에는 그 말이 없다. 옮기다 빠뜨린 게 아니라 실제로 어긋나도 되는 자리라서 그대로 뒀다.

티어 판정 기준(태스크 수, 파일 수)은 아직 감이라 사이클이 쌓이면 숫자를 조정할 것이다. 그래프를 아무리 촘촘하게 짜도 노드들이 읽는 것은 결국 코드다. 노드는 코드가 지시대로 됐는지는 확인해도 그 지시가 화면에서 맞는지는 확인하지 못하는데, 검증 루프를 종결 조건으로 못 박은 이유가 그것이다.

한 사이클에서 리뷰어가 잡은 것들

claude-deck에 기능 여섯 개를 붙인 사이클을 M티어로 돌렸다. 그래프를 짜기 전에 본 건 태스크의 성격이 아니라 파일 목록이다. 사이드바 그룹과 워크스페이스 셀렉트는 같은 페이지 파일과 세션 스토어를 건드려서 직렬로 묶었고, diff 뷰 개편은 그 둘과 파일이 안 겹쳐 병렬로 돌렸다.

작성자와 리뷰어를 나눈 값은 리뷰 결과로 돌아왔다. 리뷰어 노드가 잡은 것들이다.

  • 파일 API의 심볼릭 링크 경로 탈출. 경로 검사가 문자열 비교뿐인데 fs.readdir·stat·writeFile이 전부 링크를 따라가서, 리포 안 링크 하나로 트리 나열과 파일 읽기와 쓰기까지 밖으로 샜다. 리뷰어는 지적만 하지 않고 실제 API에 쏴 재현까지 붙여 왔다.
  • Tailwind forms의 background 단축 속성 충돌. 플러그인이 base 레이어에서 select에 그려 넣은 chevron 배경 이미지가, 커스텀 스타일의 단축 속성에 밀려 지워졌다.
  • git status --porcelain의 한글 경로 인용. 옥탈 이스케이프로 인용된 경로가 실제 경로와 안 맞아 상태 색만 조용히 빠졌다.
  • Date.now()로 굳는 경과 시간 라벨. 반응형 값이 아니라 화면을 연 순간에서 안 움직이는데, 하필 이벤트가 없는 세션만 갱신이 안 되니 그 화면이 알리려던 정보가 정확히 고정된다.

넷 다 코드만 읽어서는 정상으로 보인다. 셀프 리뷰로는 안 나왔을 것들이라, 노드를 나눈 비용은 이 사이클에서 회수됐다.

검증 루프가 잡은 두 건은 앱의 결함이 아니었다.

먼저 도구가 만든 증상이다. 그룹 배정 팝오버가 안 열려서 앱 버그로 몰고 갔는데, 원인은 브라우저 자동화 도구의 press 명령이 남긴 키 입력 폭주였다. 초당 수천 회 keydown이 들어가며 클릭이 연쇄로 발생하고 있었다. 화면에서 본 증상이 앱의 것인지 도구의 것인지 가르는 단계가 하나 더 필요했다.

다음은 내가 고른 관측값이 틀린 경우다. 세션 보드에서 지목한 세션이 저장된 패널 구성에 덮이는 문제를 고치고 재검증했는데 계속 실패로 나왔다. 코드를 다시 뒤졌지만 틀린 건 판정 기준이었다. localStorage에 저장되는 패널 구성 값을 읽어 판정했는데, 그 키는 복원이 끝난 뒤에만 쓰이므로 로드 직후에는 테스트하려고 심어 둔 옛 값이 그대로 남아 있다. Storage.prototype.setItem을 후킹해 보니 그 키에는 쓰기가 한 번도 없었다. 사이드바에서 활성 행이 몇 번째인지로 기준을 바꿔 다시 재고 통과를 확인했다. 지연 저장되는 캐시를 상태의 대리 지표로 쓰면 멀쩡한 수정이 계속 실패로 보인다.

둘 다 실화면을 직접 조작해 보지 않았으면 몰랐을 것들이다. 그래프 통과가 완료가 아니라는 말은 코드 밖에도 틀릴 자리가 있다는 뜻이기도 하다. 도구가 만든 증상은 앱 버그로 오인되고, 잘못 고른 관측값은 멀쩡한 수정을 실패로 만든다.

화면이 없는 작업에도 같은 절차가 돈다

QA 테스트케이스를 만드는 스킬을 고친 사이클은 대상이 코드가 아니라 지시문이었다. 고칠 화면이 없으니 검증 루프를 “스킬을 실제로 실행해 산출물을 본다”로 바꿔 돌렸다. 노드는 넷이다. 본세션이 문서를 고치고, 리뷰어가 diff를 읽고, 생성 스킬과 평가 스킬을 각각 샘플 명세로 돌리는 검증 노드 둘을 병렬로 띄웠다.

리뷰어는 3라운드에 걸쳐 29건을 잡았다. 가장 무거운 것은 넘침 처리 속성 판정이 느슨해서, 시안에 whitespace-nowrap만 있어도 “끝이 …으로 잘린다”를 확정 케이스로 낼 수 있다는 지적이었다. 그 클래스는 말줄임이 아니라 옆 요소를 밀어내는 동작이라, 그대로 두면 없는 동작을 검증하라고 쓴 케이스가 나간다. 문서를 읽는 것만으로 잡히는 종류다.

그런데 스킬을 실제로 돌려보고서야 나온 결함이 둘 더 있었다. 리뷰 29건에는 없던 것들이다.

  • 두 규칙이 같은 요소에 다른 답을 냈다. 실재성 판정 표는 whitespace-nowrap 단독을 보류로 지목하는데, 넘침 계산 규칙은 “안 넘치면 확정”이라고 말한다. 칩처럼 폭 제한 없이 글자만큼 늘어나는 요소에서는 넘침 자체가 안 생기므로 두 규칙이 충돌한다. 실행 노드가 진료실명 칩에서 정확히 막혀 보고해 왔다.
  • 예시가 반드시 실패하는 측정을 유도했다. 기대결과 예시에 “병원명과 버튼 간격 8px”을 썼는데, 시안 헤더는 양끝 정렬이라 실제 간격은 약 960px이다. 8px은 버튼의 왼쪽 여백이다. 예시를 그대로 따르면 측정이 어긋난다.

둘 다 문장만 읽으면 멀쩡하다. 각 규칙은 그 자리에서 맞는 말이고, 충돌은 두 규칙이 같은 요소에 동시에 닿을 때만 드러난다. 실행 노드가 산출물을 뽑으면서 밟은 경로가 그 자리였다.

수치도 그때 나왔다. 축 정의를 극값만 남기도록 좁히자 확정 케이스가 11건에서 7건으로 줄었고, 병원명 30자는 540px로 영역 폭 240px를 넘어 말줄임이 확정됐다. 시안 길이 10자는 180px라 “잘리지 않고 전부 표시된다”가 됐다. 케이스가 줄어든 것이 곧 커버리지 손실은 아니다. 정상 길이는 이미 정책 케이스가 덮고 있었고, 나머지는 같은 결과를 내는 중복이었다.

문서를 고치는 작업에도 검증 루프가 종결 조건인 이유가 여기 있다. 지시문은 읽을 때가 아니라 따를 때 모순이 드러난다.

같은 사이클에서 규칙을 하나 더 얻었다. 문서에 실행 가능한 코드를 넣으면 그 코드도 검증 대상이다.

측정 산출물이 특정 사내 플러그인 전용 입력이었는데, 전사 필수 스킬이 아니라 설치하지 않은 사람은 산출물을 만들어도 쓸 수 없었다. 도구 중립적인 표로 바꾸고 브라우저 콘솔에 붙여넣는 스니펫을 함께 넣었다. 리뷰어는 이 스니펫을 정적으로 읽고 결함 여럿을 잡았지만, 목 페이지 네 종류에서 실제로 돌리자 리뷰가 통과시킨 버그가 다섯 개 더 나왔다.

  • 앵커를 최말단 요소로 찾으면 말줄임 스타일이 걸린 바깥 요소를 놓친다. 정상 화면인데 네 행이 실패로 뒤집혔다
  • 요소 지정이 조상만 탐색해서, 문서가 안내한 복구 경로가 정작 그 상황에서 막혔다
  • 줄 수가 클램프로 잘려 안 보이는 줄까지 셌다
  • 같은 콘솔에서 두 번 실행하면 변수 재선언으로 죽었다
  • 안쪽 셀렉터가 여럿 맞으면 첫 번째를 조용히 쟀다

전부 DOM이 있어야만 드러나는 것들이다. 코드를 읽어서는 어느 것도 보이지 않았다. 문서 안의 코드는 문서가 아니라 코드로 취급해야 한다.

검증 루프도 틀린 값을 낸다. 블로그 목록에서 썸네일을 걷어내고 1열 세로로 바꾼 S티어 사이클에서 그 경우를 만났다.

카드 스타일을 display: block에 padding: 20px으로 고쳤는데, 개발 서버에서 잰 getComputedStyle 값은 display: flex에 padding: 0px이었다. 파일을 다시 열어봐도 코드는 맞았다. 서버에 직접 요청해 받은 CSS도 display:block;padding:20px이었다. 브라우저 안에서만 옛 규칙이 살아 있었다. 서버를 재기동하고 쿼리스트링으로 캐시를 우회해도 값은 그대로였고, 프로덕션 빌드를 띄워 다시 재고서야 block과 20px이 나왔다.

개발 서버의 HMR이 마크업은 갱신하면서 scoped CSS는 옛 것을 남겨둔 탓이다. 코드가 맞는데 측정이 틀린 상태다. 이 방향의 오류가 더 위험한 건 실측값을 믿고 코드를 고치러 가면 멀쩡한 코드를 망가뜨리기 때문이다. 첫 반응도 전역 스타일에 충돌 규칙이 있나 뒤지는 것이었다.

규칙을 하나 얹었다. 실화면 검증은 개발 서버가 아니라 프로덕션 빌드에서 잰다. 코드와 실측이 어긋나면 코드부터 의심하지 말고 측정 경로를 먼저 확인한다.

리뷰어는 화면에 안 보이는 회귀를 짚었다. 썸네일을 안 보이게 해달라는 요청이라 thumbnail 필드를 스키마째 지우는 선택지가 있었는데, 그러면 og:image가 글마다 다른 대표 이미지 대신 블로그 공통 기본값으로 폴백된다. 화면 어디에도 그려지지 않으니 육안으로도 실측으로도 안 잡힌다. 렌더만 걷어내고 메타 경로는 남겼다.

최종 수치는 목록·태그·상세 세 화면 모두 이미지 0개, 카드 폭 1152px 단일 값, 카드 간격 24px 균일, 390px 폭에서 가로 스크롤 없음이다. 리뷰 지적 5건 중 4건을 고쳤다. 컴포넌트 이름은 CardGrid인데 내용은 flex 세로 스택이던 불일치, 3열 그리드 시절에만 의미가 있던 flex: 1 같은 no-op 규칙이 거기 들어 있었다.

코드가 아닌 스킬에도 리뷰어를 붙였다

CS 조사 절차를 스킬 파일 3개(166줄)로 만든 S티어 사이클이다. 코드가 아니라 절차 문서라 리뷰어가 할 일이 적을 거라 봤는데 지적이 17건 나왔고 16건을 고쳤다. 높음 3건이 모두 “규칙은 적었는데 강제되지 않는” 종류였다. 사람 승인을 원칙으로 적어놓고 개선 루프에서는 에이전트가 기준 파일을 직접 고치게 했고, 조회 부하 가드를 적어놓고 대시보드를 여는 순간 저장된 기본 범위로 쿼리가 먼저 도는 경로를 막지 않았다. 처리량 기준도 화면에 문구가 안 뜨면 조용히 통과되는 구조였다.

문서형 산출물의 결함은 문장끼리의 모순에서 나온다. 한 섹션의 원칙이 다른 섹션의 절차에서 뒤집히는데, 쓴 사람은 두 문장을 따로 읽는다. 실화면 검증은 브라우저 연동이 막혀 자동 조회 경로를 끝내 돌리지 못했고, 미검증 항목으로 남겼다.


4. 마치며

결과가 아니라 절차를 표준화하는 것

그래프 엔지니어링이 모든 개발의 정답이라고 생각하지 않는다. 에이전트 하나에게 계획을 맡기던 방식을 대체하는 것도 아니다. 작업에 따라 골라 쓸 선택지가 하나 늘어난 쪽에 가깝다.

유효 범위는 좁은데, 작성자 혼자 보면 관대하게 넘어갈 코드가 있고 명세와 현실이 어긋났을 때 화면을 열어야만 그게 드러나는 작업에 한한다. 두 조건 중 하나도 걸리지 않으면 노드를 나누는 비용만 남는데, 그런 작업에 그래프를 씌우는 것은 애초에 목표가 아니었다.

그대로 옮겨 쓰려면 노드를 띄울 수단과 티어를 판정할 규칙, 실화면을 만질 자동화 도구라는 세 자리를 채워야 한다. 이 조건만 갖춰지면 도구 조합이 무엇이든 같은 절차가 돈다. 실제로 한 사이클에서는 M티어가 쓰기로 한 워크플로 도구가 막혀 에이전트 도구로 리뷰어를 분리했는데, 그때도 노드와 엣지는 그대로였다. 티어가 정하는 건 그래프의 모양이지 실행 도구가 아니다.

지금 방법론은 ~/.claude/skills/graph-engineering/에 스킬로 등록돼 “개발”, “구현”, “만들어” 같은 키워드에 뜬다. 사이클을 돌릴 때마다 판정 기준이 바뀌거나 원칙이 하나 더 늘어나면 그 변경을 스킬 파일에 남기고, 틀린 것도 같은 자리에 적는다. (2026년 8월 26일 갱신)

방법론이라고 부르지만 결과가 같아진다는 뜻은 아니다. 같은 작업을 두 번 시켜 같은 코드가 나온다는 보장은 없고 그건 목표도 아니었다. 표준화한 것은 절차다. 작성자와 검토자를 나누는 것, 화면 개발을 네 층으로 가르고 규모에 맞춰 그래프 단수를 정하는 것, 그래프를 통과해도 화면을 열기 전엔 끝내지 않는 것. 매번 즉흥으로 정하던 이 세 가지를 매번 같게 만든 것이 전부다.

에이전트가 짜고 스스로 리뷰까지 통과시킨 화면이 밀린 것도, 파서가 이전 대화를 삼킨 것도, 유휴 알림이 승인 카드로 둔갑한 것도 코드 안에서는 아무도 못 봤다. 지금은 그런 결함이 완료 판정 앞에 놓인다. 화면을 열기 전엔 끝난 게 아니니까.