한 줄 요약 — AI 에이전트는 작업마다 저장소를 처음부터 다시 탐색합니다. 코드 그래프 도구 graft를 붙여 그 탐색을 "한 번 만든 지도 읽기"로 바꿨고, LLM 요약(
--deep) 없이 구조 그래프만으로도 grep/read 왕복이 눈에 띄게 줄었습니다.
지난 글에서는 수백 턴짜리 마라톤 세션과 컨텍스트에 남는 스크린샷이 토큰을 먹는 구조를 다뤘습니다. /clear, /compact, 서브에이전트로 세션을 짧고 가볍게 유지하는 방법이었습니다.
그런데 세션을 짧게 끊을수록 다른 비용이 보이기 시작했습니다. 새 세션의 Claude Code는 매번 아무것도 모르는 상태에서 출발합니다. 어제 이미 훑어본 CI4 프로젝트를 오늘 다시 grep하고, 파일을 열고, 호출을 따라갑니다. 세션을 끊어서 아낀 토큰의 일부를 다시 탐색에 쓰고 있었던 셈입니다.
이 글은 그 탐색 비용을 줄이려고 graft를 도입한 기록입니다. 설치와 연동, --deep 연동에서 겪은 시행착오, 그리고 CI4 프로젝트에서 실제로 쓸 만했던 부분과 한계를 정리했습니다.
문제: 에이전트는 매번 처음부터 지도를 그린다
에이전트가 코드를 고치기 전에 하는 일은 거의 같습니다. 단어 하나를 grep하고, 걸린 파일을 Read로 통째로 열고, 그 안에서 본 클래스 이름으로 다시 grep합니다. 막다른 길이면 되돌아가서 다른 파일을 엽니다.
grep "TokenService"— 결과 수십 줄이 컨텍스트에 들어옴Read app/Services/TokenService.php— 필요한 건 메서드 하나인데 파일 전체가 들어옴- 호출한 쪽을 찾으려고 다시
grep— 또 수십 줄 - 컨트롤러, 필터, 설정 파일을 차례로
Read - 그제야 수정할 위치가 확정됨
이 루프가 비싼 이유는 세 가지입니다.
- 반복된다. 어제 그린 지도를 세션이 끝나면 버리고, 오늘 다시 그립니다.
- 통째로 읽는다. 시그니처 하나를 확인하려고 수백 줄짜리 파일 전체를 컨텍스트에 올립니다.
- 누적된다. 지난 글에서 본 것처럼 한 번 들어온 탐색 결과는 이후 모든 턴에 다시 실려 갑니다. 탐색 초반의
Read몇 번이 세션 끝까지 비용을 냅니다.
사람은 코드베이스에 한 번 온보딩하면 됩니다. 에이전트는 세션마다 온보딩을 다시 합니다. 이 온보딩을 파일로 저장해 두고 읽게 하자는 것이 코드 그래프의 발상입니다.
graft: 저장소 안에 두는 코드 지도
graft(npm @nanonets/graft)는 코드베이스를 한 번 분석해 저장소 안 graft/ 폴더에 그래프로 써 두는 도구입니다. 임베딩이나 벡터 DB가 아니라 링크로 이어진 마크다운 파일과 JSON이라, 에이전트는 평소 파일을 읽듯 그래프를 읽습니다.
그래프는 두 층으로 만들어집니다.
|
층 |
만드는 방법 |
담는 것 |
비용 |
|---|---|---|---|
|
구조 그래프 ( |
tree-sitter 파싱 |
함수·클래스·메서드 심볼, 호출 엣지, 파일별 카드 |
모델 호출 없음, 키 불필요 |
|
의미 층 ( |
LLM 2패스 (파일 요약 → 노드 묶기) |
노드별 요약, 핵심 코드 발췌(crux), 노드 간 타입 링크 |
내 API 키로 과금 |
두 층 모두 콘텐츠 해시로 캐싱됩니다. 바뀐 파일만 다시 처리하고, 질의할 때마다 작업 트리와 비교해 구조 그래프를 먼저 갱신합니다. 커밋 전 수정까지 반영된 지도를 읽는다는 뜻입니다.
공식 README는 효율 벤치마크에서 같은 에이전트 대비 토큰 42%, 도구 호출 46%, 소요 시간 60% 절감을 제시합니다. SWE-bench Verified 50건에서는 해결률이 54%에서 66%로 올랐다고 밝힙니다. 다만 이 수치는 TypeScript·Python·Go 같은 언어 중심의 측정이고, 제 환경인 PHP/CI4에서는 조건이 다릅니다. 이 차이는 뒤에서 다룹니다.
설치와 Claude Code 연동: 명령 두 줄
설치는 전역 CLI 하나와 init 한 번이 전부입니다.
npm install -g @nanonets/graft
cd ~/work/ci4-board
graft init --dry-run # 무엇을 건드리는지 먼저 확인
graft init --agents claude
--dry-run을 먼저 돌린 이유는 이미 공들여 만든 .claude/ 설정이 있기 때문입니다. 훅, 스킬, 서브에이전트를 직접 구성해 둔 상태라 덮어쓰기가 걱정됐습니다. 확인해 보니 init은 기존 settings.json에 자기 블록만 병합하고, CLAUDE.md는 건드리지 않았습니다.
graft init이 Claude Code에 붙여 주는 것은 다음과 같습니다.
|
항목 |
위치 |
하는 일 |
|---|---|---|
|
스킬 |
|
탐색할 때 grep/read 대신 graft를 먼저 쓰라는 지침 |
|
훅 |
|
세션 시작 시 저장소 지도 주입, 프롬프트마다 관련 노드 주입, 수정 후 영향 범위 표시와 그래프 재동기화 |
|
MCP 서버 |
|
|
|
상태줄 |
|
그래프 크기, 보강 비율, 낡은 노드 경고 |
graft/ 폴더는 .gitignore에 자동으로 들어갑니다. node_modules처럼 각자 생성하는 캐시이고, 커밋하는 것은 .claude/ 연동 설정뿐입니다. MCP 서버는 Claude Code를 재시작해야 잡힙니다.
--deep 연동기: Gemini로 붙였다가 무료 티어에서 멈추다
결론부터 말하면 --deep은 포기했습니다. 과정에서 막힌 지점은 다른 도구에서도 똑같이 만날 만한 것들이라 순서대로 남깁니다.
graft는 LLM 프로바이더로 openai(OpenAI 호환 엔드포인트 전부)와 anthropic 두 가지 형식만 받습니다. Gemini는 OpenAI 호환 엔드포인트를 제공하므로 openai로 붙일 수 있습니다.
# .env — export 없이 KEY=VALUE
GRAFT_PROVIDER=openai
GRAFT_BASE_URL=https://generativelanguage.googleapis.com/v1beta/openai
GRAFT_API_KEY=AIza...
GRAFT_MODEL=gemini-3.6-flash
|
순서 |
증상 |
원인 |
해결 |
|---|---|---|---|
|
1 |
|
|
|
|
2 |
|
바디 없는 404는 경로를 못 찾은 것 |
|
|
3 |
JSON 바디가 있는 404 ( |
엔드포인트는 맞고 모델명이 틀림 |
|
|
4 |
|
무료 티어 RPM/RPD 초과 |
프록시, 범위 축소, 결제 연결 검토 |
|
5 |
Groq으로 갈아타면? |
속도는 빠르지만 무료 한도는 비슷 |
같은 벽이라 판단, 중단 |
교훈은 두 가지였습니다. 첫째, 404는 바디 유무로 단계를 가릅니다. 바디가 없으면 경로, 있으면 모델명입니다. 둘째, --deep의 호출 패턴은 무료 티어와 맞지 않습니다. 파일마다 요약 호출이 한 번씩 나가므로 CI4 프로젝트 하나로도 분당 한도를 금방 넘깁니다.
그래도 손해는 없었습니다. 두 LLM 패스는 콘텐츠 해시로 캐싱되므로 성공한 파일은 남아 있습니다. 나중에 결제를 연결하면 남은 파일부터 이어서 처리됩니다. Flash 계열이면 첫 풀빌드 비용은 크지 않고, 이후는 바뀐 파일만 다시 돕니다.
키 없이 쓰는 구조 그래프: grep/read를 무엇으로 바꾸나
--deep 없이 graft build만 돌려도 탐색 명령 대부분은 그대로 동작합니다. 모두 tree-sitter 결과만 쓰므로 모델 호출도, 비용도 없습니다. 탐색 루프의 각 단계를 이렇게 바꿀 수 있습니다.
|
하려는 일 |
기존 방식 |
graft |
줄어드는 것 |
|---|---|---|---|
|
처음 보는 저장소 파악 |
|
|
디렉터리 묶음, 허브 함수, 핫스팟을 정해진 토큰 안에 한 번에 |
|
작업 관련 위치 찾기 |
키워드 |
|
순위가 매겨진 노드와 |
|
파일 API 확인 |
파일 전체 |
|
본문 없이 시그니처만, README 기준 약 1/10 토큰 |
|
누가 호출하나 |
메서드 이름 |
|
호출·참조하는 쪽 목록, |
|
패턴 전수 검색 |
|
|
결과를 감싸는 심볼별로 묶고 결합도 순 정렬 |
체감상 차이가 가장 컸던 것은 skeleton입니다. 서비스 클래스의 메서드 시그니처만 필요할 때 파일 전체를 읽지 않아도 됩니다. 에이전트가 "이 클래스에 뭐가 있지?" 하고 수백 줄을 올리던 장면이 한 번의 짧은 응답으로 바뀌었습니다.
두 번째는 세션 시작 훅입니다. 새 세션이 저장소 지도를 들고 시작하니, 첫 몇 턴의 ls와 둘러보기 Read가 거의 사라졌습니다. 지난 글에서 세션을 자주 끊기로 한 결정과 잘 맞물렸습니다. 세션을 끊어도 지도는 남기 때문입니다.
CI4/PHP에서 만나는 한계: 끊긴 엣지
graft는 언어를 두 등급으로 나눕니다. TypeScript, Python, Go, Java는 파일을 넘나드는 호출과 import까지 해석하는 full-fidelity 등급입니다. PHP는 Broad 등급으로, 심볼과 이름 기반 호출 엣지까지만 잡습니다. 언어 서버로 엣지를 보강하는 --lsp 옵션도 아직 PHP는 대상이 아닙니다.
CI4에서는 이 차이가 꽤 드러납니다. CI4는 객체를 서비스 로케이터로 꺼내 쓰는 일이 많기 때문입니다.
$auth = service('auth'); // 문자열 키로 꺼냄
$users = model(UserModel::class); // 헬퍼 함수 경유
$auth->attempt($credentials); // 수신자 타입을 정적으로 알 수 없음
사람에게는 service('auth')가 Config\Services::auth()라는 게 당연합니다. 하지만 정적 파서에게는 문자열일 뿐입니다. 그래서 graft callers attempt를 돌리면 같은 이름의 다른 메서드가 섞이거나, 실제 호출이 빠질 수 있습니다. 매직 메서드(__call, __get)를 거치는 모델 호출도 마찬가지입니다.
그래서 CI4에서는 도구별로 신뢰도를 다르게 둡니다.
- 믿고 쓴다:
map,skeleton,grep. 파일과 심볼 단위라 로케이터 문제와 무관합니다. - 참고만 한다:
callers,ask. 결과가 비었다고 "호출하는 곳 없음"으로 결론 내지 않고, 필요하면grep으로 한 번 더 확인하게 합니다. --deep이 메울 자리: 구조 엣지가 끊긴 곳은 LLM 요약 노드가 "이 필터는 auth 서비스를 쓴다" 같은 의미 링크로 이어 줄 수 있습니다. PHP 프로젝트에서--deep의 가치가 오히려 더 큰 이유입니다.
체감 정리, 그리고 직접 재 보는 법
정리하면, 구조 그래프만으로도 탐색 단계의 도구 호출과 큰 Read가 줄었습니다. 특히 새 세션 첫 몇 턴과, 클래스 구조를 파악하는 구간에서 차이가 분명했습니다. 반면 CI4의 로케이터 호출을 따라가는 작업에서는 여전히 grep이 필요했습니다.
|
구간 |
체감 |
이유 |
|---|---|---|
|
새 세션 시작 |
크게 줄어듦 |
세션 시작 훅이 저장소 지도를 미리 넣어 줌 |
|
클래스·파일 구조 파악 |
크게 줄어듦 |
|
|
작업 위치 찾기 |
줄어듦 |
|
|
호출 관계 추적 |
차이 적음 |
PHP는 이름 기반 엣지라 로케이터 호출이 끊김 |
공식 벤치마크의 42%를 제 환경에 그대로 기대하지는 않습니다. 언어 등급도 다르고 --deep도 없기 때문입니다. 숫자가 궁금하다면 같은 질문을 graft 유무로 나눠 돌려 보는 것이 가장 정확합니다.
# 같은 커밋으로 클론 두 개: A는 graft init, B는 그대로
Q="회원 탈퇴 시 토큰을 무효화하는 코드는 어디 있고, 무엇이 그걸 호출하나?"
for d in repo-graft repo-cold; do
(cd $d && claude -p "$Q" --output-format json \
| jq '{dir: "'$d'", turns: .num_turns, cost: .total_cost_usd, usage: .usage}')
done
질문은 "위치 찾기", "구조 파악", "호출 추적" 유형을 골고루 섞어 대여섯 개 준비합니다. 같은 질문을 두세 번씩 돌려 평균을 보면 편차가 줄어듭니다. 지난 글의 세션 로그 집계 스크립트로 평소 작업 세션을 도입 전후로 비교하는 방법도 있습니다.
돌아보며
- 탐색은 반복되는 비용이다. 세션을 짧게 끊는 것만으로는 부족했습니다. 끊을 때마다 버려지는 "저장소 이해"를 어딘가에 남겨야 했고, graft는 그것을 파일로 남깁니다.
- 비싼 층 없이도 쓸 만하다.
--deep을 포기했지만 tree-sitter 구조 그래프만으로map,skeleton,ask가 제 몫을 했습니다. 무료로 시작하고, 필요할 때 의미 층을 얹으면 됩니다. - 언어 등급을 확인하고 기대치를 정한다. PHP는 이름 기반 엣지라 호출 추적은 반쯤만 믿습니다. 벤치마크 숫자보다 내 언어와 프레임워크에서 어디까지 되는지가 중요했습니다.
- 무료 티어는 배치 작업의 적이다. 파일 수만큼 호출하는 작업은 RPM 한도와 정면으로 부딪힙니다. 일회성 풀빌드라면 결제를 연결하고 끝내는 편이 시간 대비 쌉니다.
- 맥락은 대화가 아니라 저장소에 둔다. 지난 글의
CLAUDE.md와 메모 파일처럼, 코드 그래프도 같은 원칙입니다. 저장소에 남긴 맥락은 세션이 바뀌어도 사라지지 않습니다.
혼자 여러 제품을 오가며 개발하면 "이 프로젝트 구조가 어땠더라"를 에이전트도 매번 다시 묻습니다. 그 질문에 대한 답을 미리 써 두는 것, 그게 코드 그래프의 쓸모였습니다.
출처
- graft README (trailhq/Graft) — 설치, 명령, 언어 지원 등급, 공식 벤치마크 수치
댓글 0개