AI 에이전트가 grep으로 코드를 더듬는 비용, CodeGraph로 걷어내기
AI 코딩 에이전트가 텍스트 검색에 의존하며 겪는 비효율을 해결하는 CodeGraph를 소개합니다. 코드베이스를 로컬 SQLite 기반 지식 그래프로 미리 인덱싱하여, 에이전트가 호출 관계와 영향 범위를 한 번의 호출로 파악하게 함으로써 탐색 시간을 획기적으로 줄이고 보안까지 챙기는 최적의 코딩 환경을 구축해 보세요

AI 코딩 에이전트를 며칠만 써 보면 한 가지 패턴이 눈에 들어온다. 무언가를 시키면 본격적인 작업에 들어가기 전에 한참을 "탐색"한다. grep 한 번, 파일 하나 읽고, 거기서 나온 심볼을 다시 grep, 또 파일을 읽는다. 정작 코드를 고치기도 전에 컨텍스트 창의 절반이 파일 덤프로 차 있다. 사람이 IDE에서 "정의로 이동" 한 번이면 끝낼 일을, 에이전트는 텍스트 검색으로 더듬더듬 재구성하고 있는 셈이다.
CodeGraph는 이 탐색 단계를 통째로 들어내려는 도구다. 코드베이스를 미리 한 번 파싱해서 심볼과 그 관계를 그래프로 만들어 두고, 에이전트가 물어보면 "필요한 소스 + 그게 누구를 부르고 누가 그걸 부르는지"를 한 번의 호출로 돌려준다. grep/read 루프 수십 번이 explore 한두 번으로 줄어든다. 아래 내용은 v1.1.0을 한 프로젝트에 붙여서 한동안 써 본 기록이다.
grep으로 코드를 "읽는" 게 왜 비싼가
LLM 에이전트가 코드를 이해하는 기본 동작은 결국 텍스트 검색이다. requirePermission이 어디서 쓰이는지 알고 싶으면 그 문자열을 grep하고, 매칭된 파일들을 열어서 읽고, 그중 또 호출 관계를 따라가려면 다시 grep을 돈다. 문제가 몇 가지 겹친다.
- 호출 관계를 텍스트로는 못 따라간다. 콜백, 동적 디스패치, JSX 자식으로 넘어가는 흐름은 문자열 매칭으로 안 잡힌다. 에이전트는 추측하거나, 더 많은 파일을 읽거나, 그냥 놓친다.
- 토큰이 샌다. 함수 하나만 보면 되는데 매칭된 파일 전체를 읽어 들인다. 컨텍스트는 한정 자원인데 관련 없는 코드로 채워진다.
- 느리다. grep → read → grep 은 본질적으로 직렬이다. 매 라운드가 왕복 한 번이고, 실제 작업은 그 뒤에 시작된다.
핵심은, 이 "구조 파악"이 매번 새로 할 필요가 없는 일이라는 점이다. 호출 그래프는 코드가 바뀔 때만 바뀐다. 그렇다면 한 번 계산해서 저장해 두고 재사용하면 된다. 그게 CodeGraph가 하는 일이다.
CodeGraph가 하는 일
한 줄로 줄이면, 코드베이스의 지식 그래프를 로컬 SQLite에 미리 만들어 두고 에이전트에 MCP 도구로 물려주는 인덱서다. README의 표현을 빌리면 "surgical context · fewer tool calls · faster answers · 100% local"인데, 마지막 항목이 개인적으로 가장 컸다. 데이터가 기계 밖으로 안 나간다. API 키도, 외부 서비스도 없고 SQLite 파일 하나가 전부다. 회사 코드를 다루는 입장에선 이게 채택 가능 여부를 가른다.
인덱싱 파이프라인 자체는 단순하다.

tree-sitter가 소스를 AST로 파싱하고, 언어별 쿼리로 노드(함수·클래스·메서드·라우트)와 엣지(호출·import·상속)를 뽑아 로컬 SQLite에 넣는다. 검색은 FTS5(전문 검색)로 깔린다. 파일 하나에 갇히지 않도록 참조를 교차 해석하는 게 포인트다. 호출은 정의로, import는 원본으로, 상속 체인을 파일 경계 너머로 잇는다. 그 뒤로는 OS 네이티브 파일 워처(macOS면 FSEvents)가 변경을 감지해 디바운스 후 자동으로 sync하므로, 한 번 init하면 코딩하는 동안 그래프가 알아서 따라온다.
지원 언어도 넉넉하다. TypeScript/Python/Go/Rust/Java/C#/Swift/Kotlin 등 20개 이상이고, Django·Flask·Express·Rails 같은 웹 프레임워크 17종의 라우팅을 인식한다. 라우트가 노드 종류로 따로 잡히는 게 백엔드 코드에선 은근히 유용했다(뒤에 status 출력의 route 항목으로 다시 나온다).
실제로 붙여 보기
설치는 세 단계다. 스크립트 한 줄로 깔고, 에이전트에 등록하고, 프로젝트마다 인덱스를 만든다.
curl -fsSL https://raw.githubusercontent.com/colbymchenry/codegraph/main/install.sh | sh
codegraph install # Claude Code, Cursor, Codex 등에 MCP 서버 등록
codegraph init # 현재 프로젝트 인덱싱init이 끝나면 .codegraph/codegraph.db가 생긴다. 상태를 찍어 보면 그래프 규모가 한눈에 보인다. 아래는 내가 인덱싱한 중간 규모 TS/JS 프로젝트의 실제 수치다.
CodeGraph Status
Index Statistics:
Files: 586
Nodes: 6,625
Edges: 21,989
DB Size: 20.19 MB
Backend: node:sqlite — built-in (full WAL)
Nodes by Kind:
import 2,637
function 1,596
constant 768
file 582
interface 494
route 151
...586개 파일에서 심볼 6,625개와 엣지 21,989개를 뽑아 20MB짜리 SQLite 하나에 담았다. 이 20MB가 에이전트가 grep으로 매번 재구성하던 구조 정보 전부다.
explore — 보통 이거 하나면 끝난다
가장 자주 쓰는 명령이다(MCP에선 codegraph_explore 도구로 노출된다). 자연어 질문이나 심볼 이름을 던지면 세 덩어리를 돌려준다. 연관 심볼의 verbatim 소스, 호출 관계, 그리고 블라스트 반경이다. 출력은 대략 이렇게 생겼다(내용은 설명용으로 단순화했다).
$ codegraph explore "how does the permission check work"
Found 23 symbols across 9 files.
Blast radius — what depends on these (update/verify before editing)
- permissionToApi (.../mappers.ts:84) — 7 callers in permissions.service,
files.service, folders.service; tests: core-extended.test
- insertOwnerPermission (.../permissions.repo.ts:75) — 2 callers; ⚠️ no covering tests
Relationships
calls: GET /:id/permissions → listPermissions
addPermission → permissionToApi
Source Code (줄번호 붙은 실제 on-disk 소스 — Read 한 것과 byte-for-byte 동일)
server/auth/permission.ts — requirePermission (function)
131 export function requirePermission(role: Role = "viewer") {
132 return async (req, res, next) => {
133 const granted = await resolveRole(req.user, req.params.id)
134 if (!hasRole(granted, role)) return res.status(403).end()
135 next()
136 }
137 }세 가지가 한 응답에 같이 온다는 게 핵심이다. "이 코드"만 주는 게 아니라 이걸 고치면 누가 깨지는지(블라스트 반경), 그리고 테스트가 걸려 있는지까지 같이 알려준다. grep으로는 ⚠️ no covering tests 같은 건 절대 못 뽑는다. 게다가 Source Code가 줄번호 붙은 실제 디스크 내용이라, 받은 그대로 바로 편집에 쓸 수 있다. 에이전트 입장에선 "여러 파일을 읽어 보겠습니다" 단계가 통째로 사라지는 셈이다.
impact — 고치기 전에 폭을 본다
전체 그림이 아니라 심볼 하나의 영향 범위만 보고 싶을 때가 있다.
codegraph node requirePermission # 한 심볼의 소스 + 호출자/피호출자
codegraph callers makeId # 이 함수를 부르는 곳 전부
codegraph impact makeId # 이걸 바꾸면 영향받는 코드(테스트 포함)이 중 impact에 손이 제일 자주 갔다. 공용 ID 생성 헬퍼 하나를 손보려다 codegraph impact makeId를 쳤더니 "66 affected symbols"가 떴다. 업로드 서비스, 컨트롤러, 그리고 내가 깜빡했을 게 분명한 테스트 파일까지 파일별로 쭉 나왔다. 헬퍼 하나가 이렇게 멀리 뻗어 있는 줄은 그래프를 보기 전엔 몰랐다. 변경 폭을 눈에 넣고 편집을 시작하는 것과, 고친 다음 무엇이 깨졌는지 뒤늦게 아는 것은 차이가 크다.
에이전트에 물리기
MCP 등록은 codegraph install이 알아서 해 주지만, 프로젝트에 박아 두고 싶으면 설정 파일 한 조각이면 된다. Claude Code 기준 .mcp.json은 이렇게 생겼다.
{
"mcpServers": {
"codegraph": {
"type": "stdio",
"command": "codegraph",
"args": ["serve", "--mcp"]
}
}
}여기에 "코드 읽기 전에 codegraph부터 써라"는 한 줄을 에이전트 지침에 넣어 두니, 작업 전에 파일을 우르르 열어 보던 습관이 눈에 띄게 줄었다.
git worktree를 쓴다면 — 한 번 걸린 함정
.codegraph/는 기본적으로 gitignore된다(인덱스는 빌드 산출물이니 당연하다). 그런데 git worktree를 즐겨 쓴다면 여기서 발이 한 번 걸린다. worktree로 새 작업 디렉터리를 만들면 untracked인 .codegraph/가 따라오지 않는다. 정작 편집이 일어나는 워크트리에서 그래프가 깜깜이가 되는 것이다.
해결은 간단하다. MCP 설정(.mcp.json)은 추적 파일로 커밋해 모든 워크트리에 전파하고, 워크트리에 인덱스가 없으면 자동으로 만들게 훅을 건다. Claude Code라면 SessionStart 훅으로:
command -v codegraph >/dev/null 2>&1 \
&& [ ! -d ".codegraph" ] \
&& ( codegraph init . >/dev/null 2>&1 & ) ; true세션 시작 시 .codegraph/가 없으면(=새 워크트리) 백그라운드로 그 브랜치 기준 인덱스를 새로 뜬다. codegraph가 안 깔린 환경에선 조용히 no-op이라 안전하다. 워크트리당 20MB쯤 더 쓰지만 git worktree remove할 때 같이 정리된다. 막 작성한 코드까지 정확히 반영하려면 결국 워크트리별 인덱스가 맞다.
그래서, 쓸 만한가
README가 내건 벤치마크는 7개 코드베이스에서 "tool call 58% 감소 · 22% 빠름 · 파일 읽기 거의 0"이고, 절약 폭은 코드베이스 규모에 따라 다르다고 솔직하게 적어 두었다. 나는 이 수치를 재현해 보진 않았다. 어디까지나 정성적 인상이지만, 에이전트가 작업 전에 파일을 끌어모으던 습관이 줄고 호출 관계 질문에 답이 한 번에 나온다는 점은 분명했다.
한계도 있다.
- 인덱스는 쓰기보다 약 1초 늦다. 파일 워처가 디바운스 후 sync하므로, 방금 저장한 코드를 곧바로 explore하면 직전 상태가 잡힐 수 있다. 보통은 무시할 만하다.
- 그래프가 답하는 건 "구조"지 "의도"가 아니다. 누가 누구를 부르는지는 정확하지만, 왜 그렇게 짰는지는 여전히 코드와 주석을 읽어야 한다.
- 동적 디스패치는 정적 호출만큼 깔끔하진 않다. 다만 콜백·런타임 디스패치 일부를 따라가 주므로, 적어도 grep보다는 확실히 더 잡는다.
붙이고 나서 달라진 건 단순하다. 에이전트한테 뭘 시키면 더는 파일부터 열지 않는다. 호출 관계를 물으면 여러 파일을 읽어 보겠다는 말 없이 답이 바로 나오고, 헬퍼 하나를 고치기 전에 영향 범위가 먼저 손에 잡힌다. 회사 코드라 데이터가 기계 밖으로 안 나가는 것도 마음에 들었다. codegraph init 한 번 치는 값이 이 정도면 싸다.