Claude Code에 LSP를 붙여 본 실험

세션들은 “이 함수는 누가 호출하나?”라고 물었을 때는 사용했지만, 작업 도중에는 건너뛰었다. 넛지는 아무 변화도 없었고, 288개 세션에서 grep은 호출자를 한 번도 놓치지 않았으며, 진짜 성과는 매 편집 뒤에 보고되는 타입 오류였다. Claude Code용 LSP와 codegraph를 설정하면서 내가 측정한 것, 그리고 rtk를 쓰지 않은 이유.

내 Claude Code 설정은 여러 저장소에 걸쳐 있다. 그중 몇 개는 TypeScript이고, 몇 개는 TypeScript와 C#이 섞여 있으며, 그곳들은 풀 리퀘스트도 리뷰된다. 어느 날 나는 Claude에게 아주 단순한 질문을 했다. “당신이 더 효율적으로 일할 수 있도록, 내 쪽에서 설정해 둘 수 있는 게 있을까?” 나는 “이 함수는 누가 호출하지?”라는 질문에 답할 때 세션이 grep을 하고, 몇 파일을 읽고, 다른 철자를 찾으려고 다시 grep을 하고, 또 몇 개를 더 읽는 모습을 보곤 했다.
Claude는 LSP를 가리켰다. 나는 내 편집기에서 언어 서버를 알고 있었다. “정의로 이동”과 “모든 참조 찾기”를 해 주는 것들이다. 하지만 에이전트에게 그걸 제공한다는 생각은 해 본 적이 없었다. 대화를 이어 가다 보니 목적이 분명해졌다. 언어 서버는 텍스트 검색이 아니라 컴파일러가 답해 주는 것이다. 제대로 설정하면 grep을 수없이 돌리는 일을 덜어 줄 뿐만 아니라, 이름이 바뀐 import나 재수출(re-export)을 거친 호출처럼 grep이 놓치는 사용처도 찾아낸다. 또한 주석 안의 같은 이름이나 동일한 이름의 다른 함수처럼 grep이 잘못 집어내는 일치 항목은 건너뛴다.
그래서 TypeScript와 C#용 공식 Claude Code LSP 플러그인을 설치했다. 문제는 해결됐다고 생각했다.
아니었다. 이 글은 그 이유에 대한 이야기다. 여러 저장소에서 Claude Code를 쓴다면, 당신도 같은 벽에 부딪힐 수 있다.
세 가지 도구, 두 가지 다른 문제
그 뒤로 나는 codegraph가, 다른 프로젝트에서 쓰이는 것을 봤던 그 도구가, 같은 일을 하는지 궁금해지기 시작했다. 그리고 내가 눈여겨보던 세 번째 도구도 있었다. rtk다.
- codegraph는 미리 코드의 지도를 만든다(tree-sitter로 파일을 파싱한다). 그리고 세션이 “체크아웃 흐름은 어떻게 동작하지” 같은 질문을 자연어로 던지면, 관련 함수들, 그 코드, 그리고 누가 누구를 호출하는지 한 번에 돌려준다. MCP 서버로 실행된다.
- rtk는 세션과 셸 사이에 끼어들어 명령 출력물을 줄여 준다. 테스트 실행 결과는 실패만 남기고,
git status는 더 간결한 형태로 보여 주는 식이다.
나는 한동안 이 둘이 같은 문제를 푸는 게 아니라는 사실을 알아차리지 못했다.

무언가를 찾으려면 여러 번 왕복해야 한다. LSP와 codegraph는 왕복 횟수를 줄여 주고, rtk는 왕복은 그대로 두되 각 출력만 짧게 만든다.
설치되어 있다고 해서 쓰는 것은 아니다
첫 번째 놀라움이었다. LSP를 켠 뒤 처음 며칠 동안, 한 저장소에서는 9개의 세션, 메인 세션 기준 271회의 검색(에이전트 제외), 그리고 9번의 LSP 호출이 있었다. 그중 8번은 내가 문자 그대로 “LSP를 사용할 수 있나요?”라고 물었던 두 세션에서 나왔다.
내 첫 생각은 메모였다. “LSP가 설치되어 있습니다. 우선 사용하세요.” 한 세션이 괜찮은 메모를 남겼다. 그런데 그 저장소의 다음 세 세션은 어떤 LSP 호출도 하지 않은 채, 어떤 함수의 호출자를 찾으려고 grep만 했다. 세션 시작 시 읽는 메모는, 세션이 grep을 찾는 그 순간과는 너무나 멀다.
이유의 일부는 기계적인 것이었다. Claude Code는 모든 도구를 모델에 미리 보여 주지 않는다. 많은 도구가 지연 로드(deferred) 되어 있어서, 모델은 이름만 보고 검색으로 하나를 불러와야만 호출할 수 있다. LSP도 그중 하나였다. 그래서 세션은 도구가 존재한다는 사실을 기억하고, 그것을 불러온 다음에야 사용할 수 있었다. 대부분은 그러지 못했다.
Claude Code에는 mods라는 것(자기 동작에 훅을 거는 작은 플러그인)이 있고, 그 훅 중 하나는 도구를 지연 로드하지 않음으로 표시할 수 있다:
import type { Register } from 'claude-code';export const register: Register = (on) => { on('tool.describe', { tool: 'LSP' }, async ($, e, next) => ({ ...(await next(e)), isDeferred: false }))}같은 프롬프트를 두 번 실행해 봤다. mod가 없을 때는 세션이 도구를 검색해서 불러온 뒤 호출했다. mod가 있을 때는 LSP를 바로 호출했다. 이 mod는 ~/.claude/mods/lsp-front에 있고, ~/.claude/settings.json의 CLAUDE_CODE_PLUGIN_DIRS를 통해 모든 세션에 로드된다. 대가로는 매 세션마다 도구 설명 몇백 토큰이 든다. 그래도 끝난 뒤에도 하나 이상한 점은 남는다. 먼저 불러올 도구 목록이 계속 LSP를 가리키기 때문에, 세션이 가끔 호출 전에 그것을 검색한다는 점이다. 그렇다고 해서 mod가 망가졌다는 뜻은 아니다.
Whether it mattered is at the end.
codegraph: 내 진짜 질문은 지도가 어떻게 최신 상태를 유지하느냐였다
codegraph에서 내가 걱정했던 것은 인덱스였다. 체크아웃은 하나의 브랜치에만 머무르지 않는다. main은 fetch되고, pull request는 다른 브랜치에 머무르며, 개발은 그 사이를 오간다. 이 지도가 지금 세션 바로 앞에 있는 코드만을 대상으로 하는 걸까?
내가 알아낸 것:
- 인덱스는 체크아웃 안의
.codegraph/폴더에 저장되며, 체크아웃마다 하나씩 있다. 500~1,200개 파일이 있는 저장소에서는 빌드에 1~2초와 20~60MB가 들었고, 약 5,000개 파일이 있는 저장소 하나에서는 21초와 470MB가 들었다. - 서버가 시작되면 디스크에 있는 내용과 지도를 맞춰 동기화하고, 파일 감시자가 저장 후 약 2초 뒤에 이를 갱신한다. git 훅은 필요 없다.
- 다른 체크아웃(예를 들어 git worktree)은 거기서 직접 빌드하기 전까지는 지도가 없다. 메인 폴더에서 시작한 서버는 메인 폴더에 대해서만 응답한다.
- 텔레메트리는 기본적으로 켜져 있다. 나는
CODEGRAPH_TELEMETRY=0과DO_NOT_TRACK=1로 껐다.
정확성도 중요하다. codegraph는 코드의 형태를 읽지, 타입을 읽지는 않는다. C# 인터페이스 메서드 하나에서 LSP와 비교해 보았더니, LSP는 모든 21개 호출 지점을 찾아냈지만 codegraph는 애플리케이션 코드에서의 2개 호출자만 찾았고 테스트에서의 19개 호출은 하나도 찾지 못했다. 다른 메서드에서는 9개 중 1개만 찾았다. 그래서 나는 이것을 잘하는 용도, 즉 “이 영역은 어떻게 동작하지?”라는 질문에 사용하고, LSP는 “누가 이것을 사용하지?”라는 질문에 사용한다.
rtk: 설치하기 전에 측정했다
rtk는 토큰을 60~90% 줄인다고 홍보한다. 모든 세션에서 모든 셸 명령을 다시 쓰는 전역 훅을 설치하기 전에, 그것이 내 세션 출력의 어느 정도를 실제로 건드리는지 알고 싶었다. Claude Code는 세션 대화 기록을 유지하므로, 3일 동안 두 저장소의 세션이 셸에서 다시 읽어온 내용을 세고, rtk의 문서가 각 명령에 대해 무엇을 한다고 설명하는지에 따라 정리했다.

rtk가 의미를 바꾸지 않고 줄여 주는 부분은 내 셸 출력의 1.7%와 3.1%였다. rtk가 안전하게 줄여 주는 부분, 즉 테스트 실행, 빌드, git status는 두 저장소에서 내 세션이 셸에서 다시 읽어온 내용의 1.7%와 3.1%였다. 큰 절감은 파일 읽기와 diff를 줄이는 데서 나오는데(27~39%), 바로 그것들이 코드 리뷰에서는 한 글자도 빠뜨릴 수 없는 부분이다. 이런 필터를 꺼도 최선은 셸 출력의 5분의 1에서 4분의 1 수준이다. 그리고 셸 출력은 세션이 읽는 것의 일부일 뿐이고, 세션이 읽는 것은 곧 청구서의 일부일 뿐이다.
이것이 암달의 법칙이다: 속도 향상은 그것이 닿는 부분에만 도움이 된다. 그래서 rtk는 일단 보류다. 세션이 긴 테스트 스위트나 빌드를 실행하기 시작하면, 그때 다시 살펴볼 것이다.
어떤 질문에 어떤 도구를 쓸까
내가 내린 결론은 이렇다:

- 시그니처를 바꾸기 전이나 아무도 그것을 쓰지 않는다고 주장하기 전에, 함수·클래스·상수의 모든 사용처가 필요하다면? LSP(
findReferences). 작업 도중 빠르게 훑어보려는 용도라면 grep도 괜찮다. - 어디에 정의되어 있는지, 어떤 타입인지, 무엇을 구현하는지 알고 싶다면? LSP, 한 번씩 호출하면 된다.
- 이 영역은 어떻게 동작하는가, 이 코드 주변에는 무엇이 있는가? codegraph(
explore). - 이 텍스트, ID, 문자열, 설정은 어디에 나타나는가? grep. 나머지 둘은 코드를 보지, 텍스트를 보지 않는다.
함정
이것들 하나하나가 모두 “LSP가 작동하지 않는다”처럼 보였다.
어디에서 묻느냐에 따라 달라지는 “참조 찾기”
각각 자체 tsconfig.json을 가진 열두 개의 패키지가 있는 한 모노레포에서, 프로젝트 참조가 없고 루트 설정도 없는 상태로 공유 패키지에서 공유 함수의 참조를 요청하면 1개 파일에서 2개 결과만 나왔다. 소비자 패키지에서 요청하면 5개 파일에서 10개가 나왔다. TypeScript 서버는 tsconfig마다 하나의 프로그램을 만들고, 자신이 요청받은 파일에 대해서만 그 프로그램을 구성한다. 해결책은 루트 tsconfig.json에 "files": []를 두고, 모든 프로젝트에 대한 references 항목을 추가하는 것이었다(그리고 새 패키지가 그 목록에서 빠지면 실패하는 테스트도 함께).
저장소 루트에 있지 않은 TypeScript
제가 확인한 대부분의 저장소는 TypeScript를 하위 폴더에 두고 있었고, 앱마다 하나씩(ClientApp, E2E 프로젝트) 배치해 두었습니다. 루트에 둔 곳은 두어 개뿐이었습니다. 나머지 저장소에서는 TypeScript 언어 서버가 아예 시작되지 않았습니다. “유효한 TypeScript 설치를 찾을 수 없습니다”라는 메시지가 떴습니다. 최신 TypeScript를 전역으로 설치해도 소용이 없었습니다. TypeScript 7은 네이티브 재작성 버전이며, 해당 패키지에는 이제 더 이상 language server가 구동하는 프로그램인 tsserver가 포함되지 않습니다. 해결책은 공식 TypeScript LSP 진입점을 복사하고 폴백을 추가하는 작은 로컬 플러그인이었습니다:
{ "name": "ts-lsp-fallback", "lspServers": { "typescript": { "command": "typescript-language-server", "args": ["--stdio"], "extensionToLanguage": { ".ts": "typescript", ".tsx": "typescriptreact" }, "initializationOptions": { "tsserver": { "fallbackPath": "/Users/me/.claude/tools/typescript/node_modules/typescript/lib" } } } }}그 폴더에는 TypeScript 6.0.3가 설치되어 있고, 동일한 파일에 대해 서버가 둘씩 응답하지 않도록 공식 플러그인은 꺼 두었습니다. 이 폴백은 저장소의 루트에 자체 TypeScript가 없는 경우에만 사용됩니다.
폴백 버전은 앱의 버전과 꼭 일치할 필요가 없습니다
처음에는 “하지만 이 앱들 대부분은 5.9인데”라는 생각이 들었습니다. 앱을 빌드하는 컴파일러와 “이걸 누가 사용하나요?”에 답하는 언어 서비스는 서로 다른 작업입니다. 더 최신 TypeScript는 오래된 프로젝트도 탐색 용도로는 잘 읽습니다. 빌드와 tsc가 여전히 무엇이 오류인지 결정합니다. 저는 baseUrl 별칭을 통해 import하는 5.9 앱에서 이것을 확인했습니다. 17개 파일에서 36개의 참조가 나왔고, 함수를 import하는 모든 파일을 찾아냈습니다.
막 시작한 서버는 부분적인 답을 줍니다
세션이 막 시작되자마자 find-references를 요청하니 결과가 하나만 돌아왔습니다. 선언부뿐이었습니다. 20초 뒤 같은 호출을 하니 8개가 나왔습니다. 첫 번째 답변이 부분 결과임을 표시하는 표시는 없으므로, 시작 직후 “선언부만 나옴”은 “다시 물어보라”는 뜻이지 “이것을 사용하는 곳이 없다”는 뜻이 아닙니다.
경로로 잡는 테스트 모크는 참조가 아닙니다
vi.mock('../hooks/useThing', () => ({ useThing: ... }))는 이름을 객체 키로 보관합니다. find-references는 이것을 나열하지 않지만, 시그니처가 바뀌면 깨집니다. 모듈 경로도 함께 grep 하세요.
올바른 SDK가 설치되어 있었지만, 잘못된 것이 사용되고 있었습니다
한 저장소에서는 C# LSP가 시작되지 않았습니다. global.json에는 전날 배포된 SDK 패치 버전이 고정되어 있었고, rollForward: latestFeature는 더 오래된 패치로는 절대 내려가지 않습니다. 더 최신 SDK는 이미 제 기계에 설치되어 있었습니다(Homebrew의 cask를 통해서였습니다). 하지만 환경 변수와 PATH가 Homebrew의 formula에서 온 더 오래된 복사본을 가리키고 있었고, 이것은 뒤처져 있을 뿐 아니라 PowerShell도 여기에 의존하고 있었기 때문에 그냥 지울 수 없었습니다. DOTNET_ROOT와 PATH를 올바른 복사본으로 가리키게 하자 해결됐습니다. 교훈: 무엇이든 설치하기 전에 which -a dotnet을 먼저 확인하세요.
제 리뷰 흐름도 영향을 받았지만, 해결할 수 있었습니다
저는 claude-delivery-skills의 review-pr 스킬로 PR을 리뷰합니다. 이 스킬은 제가 유지보수하는 Claude Code 플러그인입니다. PR을 별도의 git worktree에 체크아웃하고, 그곳을 읽도록 reviewer 에이전트들을 보냅니다. 저는 worktree 방식이 좋습니다. 여러 PR을 병렬로 리뷰할 수 있고, 각각이 자기 커밋에 고정되며, 메인 폴더의 제 작업은 전혀 건드리지 않기 때문입니다.
하지만 바로 그 지점에서 도구들이 부족했습니다:
- 언어 서버는
claude가 시작된 폴더, 즉 제 메인 폴더와 PR 이전의 코드를 기준으로 응답합니다. 리뷰 worktree 안의 파일에 대해서는 C# 서버가 아무 답도 하지 않았습니다. - reviewer 에이전트들은 고정된 도구 목록(Read, Grep, Glob, Bash, Write)만 가지고 있고 LSP가 없으며, 검색의 대부분을 그들이 수행합니다. 제 한 저장소 리뷰에서는 308번 중 235번이었습니다.
- codegraph의 맵은 만들어진 위치에 머물러 있고, worktree에는 그것이 없었습니다.
Claude의 첫 제안은 각 리뷰 전에 메인 폴더를 PR 브랜치로 바꾸라는 것이었습니다. 그러면 병렬 리뷰가 불가능해지고, 같은 저장소의 메인 폴더에서는 개발이 계속될 수도 있으므로 저는 거절했습니다. 그 반박이 더 나은 답으로 이어졌습니다. Claude Code에는 기본 제공 EnterWorktree 도구가 있고, 세션이 리뷰 worktree로 들어가면 언어 서버도 함께 이동합니다.

worktree는 그대로 유지됩니다. 세션과 그 도구들이 리뷰가 진행되는 동안 그 안으로 이동합니다. 측정 결과: 새 리뷰 worktree 안에서 C# 서버는 메인 폴더에서 찾는 24개의 참조 중 18개를 찾았습니다. 그곳에서 dotnet restore를 한 뒤에는(로컬 패키지 캐시에서 4초 걸림) 24개 모두를 찾았습니다. reviewer의 도구 목록에 LSP를 더한 에이전트가 직접 호출했을 때도 같은 24개가 나왔습니다. 그리고 기계에 없는 도구 이름은 에이전트 목록에서 그냥 빠지므로, LSP를 추가해도 LSP 플러그인이 없는 사람에게는 아무 문제도 생기지 않습니다.
두 가지 조건이 붙었습니다. 워크트리에 들어가면 그 폴더의 CLAUDE.md와 설정, 훅까지 함께 불러오므로, PR이 .claude/, CLAUDE.md 또는 .mcp.json을 바꾸는 경우에는 세션이 끼어들면 안 됩니다. 그리고 자동 모드에서도 매번 승인을 묻습니다. 저는 이 설정을 유지하고 있습니다. 지금으로서는 이것이 시험용일 뿐이며, 아직 review-pr의 일부는 아닙니다.
설치됐다고 쓰이는 건 아니다, 2부
다시 모드로 돌아가 보겠습니다. LSP를 앞에 두고 codegraph를 설정한 뒤, 한 저장소에서 다섯 개의 세션을 세어 보았습니다. 첫 번째 세션은 기능 하나를 작성했고, 프롬프트에는 어떤 질문에 어떤 도구를 쓸지 적혀 있었습니다. 나머지 네 세션은 평범한 프롬프트였고 도구에 대한 힌트가 없었습니다. 그중 둘은 짧은 조사 세션이었고, 하나는 여러 변경에 걸쳐 코드를 작성한 긴 세션이었으며(에이전트 14개 사용), 또 하나는 조사와 코드를 섞은 세션이었습니다.

프롬프트에 도구가 명시된 세션만 LSP를 사용했습니다. 코드를 작성한 긴 세션은 336번의 검색을 했지만 LSP 호출은 없었습니다. 어떤 도구를 어떤 질문에 쓸지 적어 둔 프롬프트의 세션은 LSP를 10번 호출했습니다. 평범한 프롬프트의 네 세션은 도구가 바로 옆에 있는데도 모두 0번이었습니다. 처음에는 첫 번째 세션이 코드를 작성하는 세션이었기 때문에 그런 것이고, 조사 세션들은 필요가 없어서 그런 것 아닌가 생각했습니다. 세션 4가 그 의문에 답해 줍니다. 그 세션은 몇 시간 동안 코드를 작성했고, 336번 검색했으며, 그중 약 10번 중 7번은 코드 이름처럼 생긴 것(함수, 타입, 상수)을 찾는 검색이었습니다. 바로 그런 질문에 LSP가 답합니다. codegraph는 지시하지 않아도 네 개 중 두 세션에서 사용되었지만, LSP는 어느 세션에서도 그렇지 않았습니다.
모든 프롬프트에 도구 사용 지침을 붙이고 싶지는 않았습니다. 그래서 더 저렴한 두 가지 유도책을 만들고, 둘 다 아무것도 하지 않는 경우와 비교해 시험해 보았습니다.
첫 번째는 전역 지침 파일에 넣은 한 문장입니다. TypeScript나 C# 심볼을 누가 사용하는지 찾으려면 grep 대신 LSP의 find-references를 쓰고, 이미 로드되어 있으니 직접 호출하며, 첫 답변이 선언만 포함할 때는 다시 물어보라는 내용입니다. 두 번째는 훅입니다. Claude Code가 모든 검색 전에 실행하는 작은 스크립트입니다. 검색 대상이 코드처럼 생긴 것(getUserProfile, MAX_RETRIES)이면 모델이 읽는 한 줄을 추가합니다. LSP가 정확한 목록을 준다는 내용입니다. find-references가 선언만 돌려주면, 서버가 아직 로드 중일 수도 있다는 두 번째 줄이 붙습니다.
그다음 시험해 보았습니다. 헤드리스 세션은 아무도 키보드를 잡지 않은 상태에서 claude -p로 실행한 것이며, 그 전사본에는 모든 도구 호출이 기록됩니다. 그래서 같은 질문을 세 가지 방식(아무것도 없음, 문장 추가, 훅 추가)으로 실행해 횟수를 세었습니다. 세션과 질문 각각에 대해 한 번씩만 실행했고, 사용한 모델은 앞에서 세었던 세션들과 같은 Opus였습니다.
첫 번째 질문은 직접적으로 물었습니다. 널리 쓰이는 두 함수에 대해 모든 호출자와 모든 테스트를 나열하라는 것이었습니다. 세 세션 모두 LSP를 사용했는데, 아무것도 설치하지 않은 세션도 예외가 아니었습니다. 유도책은 경로만 바꿨을 뿐입니다. 둘 중 어느 것이든 세션은 네 번 검색한 뒤가 아니라 한 번 검색한 뒤 find-references로 갔고, 첫 답변이 선언만 포함할 때는 다시 물어봐서 전체 목록을 얻었습니다. 세 답변은 같았습니다. 호출자도 같고, 테스트도 같았습니다. 한 세션은 컴파일러의 목록이 grep 결과와 줄 단위로 일치한다고 적었습니다.
두 번째 질문은 그 세션들에서 실제로 나온 질문이었습니다. “누가 이걸 쓰는가”가 지나가듯 등장하는 설계 질문이었고, 제가 처음 물었던 커밋 지점에서 다시 재생했습니다. 세 세션 모두 LSP 호출은 0번이었습니다. 그 세션에서는 훅이 여섯 번 말을 걸었고, 그중 일부는 실제 사용처를 찾는 검색이었지만 세션은 매번 무시했습니다. 명령어들이 그 이유를 보여 줍니다. 세션은 사용처 검색을 하나의 더 큰 명령으로 접어 넣습니다. 이 파일을 읽고, 저 파일을 읽고, 이름을 grep하라는 식입니다. 안내 문구는 그 grep의 결과와 함께 도착하는데, 그때는 이미 세션이 답을 갖고 있습니다. 다음 사용처 검색도 같은 방식으로 또 다른 큰 명령에 접혀 들어갑니다.

설정별, 질문별로 헤드리스 세션을 하나씩 돌렸습니다. 직접 물으면 모든 설정이 LSP를 사용했고, 지나가듯 물으면 아무도 사용하지 않았습니다. 그러니 제가 문제를 잘못 본 것이었습니다. LSP는 질문이 질문답게 던져질 때 사용되며, 다른 작업 중에 잠깐 훑어보는 용도로는 쓰이지 않았고, 두 유도책도 그 점을 바꾸지 못했습니다. 그런 잠깐의 확인에는 grep이면 충분했습니다.
멈춘 지점
결국 나는 어느 쪽의 넛지도 채택하지 않았다. 훅은 잘못된 순간에 말을 거는 코드다. 이 문장은 이미 LSP에 닿는 질문에 대해 몇 번의 검색을 줄이려고 모든 저장소의 모든 세션에 들어가게 될 것이다. LSP는 남는다. 세션은 질문이 명시적일 때 그것을 호출하고, 바로 그때 정확한 목록이 가장 중요하기 때문이다. LSP를 남겨둘 또 다른 이유도 있는데, 그건 나중에 찾았다(아래). 내 마음을 바꾸려면 grep으로 만든 호출자 목록이 한 호출자를 놓쳤는데 LSP는 바로 거기에 있었던, 실제 세션이 하나 있어야 한다.
그래서 나는 그런 사례를 찾으러 갔다. 변경으로 인해 남겨진 파일에서 발생한 타입 오류를 찾기 위해, 과거 세션 288개를 훑었다. 함수에 매개변수가 하나 추가되었는데 어떤 호출자가 여전히 예전 인자를 넘기는, 그런 종류의 오류다. 그런 사례는 9개 세션에서 16건 있었다. 어느 경우든 이전 검색이 이미 그 파일을 나열하고 있었다. 세션은 호출자를 찾아냈지만 업데이트를 잊었고, 커밋 전에 타입 검사기가 그것을 잡아냈다. 그다음에는 과거 코드 리뷰에서 나온 “이것을 사용하는 건 다른 데 없다”는 주장 6개를 골랐고, 합쳐서 이름이 20개였다. 그리고 리뷰가 읽었던 커밋 시점의 컴파일러 find-references로 각각을 돌렸다. 6개 모두 맞았다. 타입이 있는 코드에서는 grep의 실수는 너무 많이 나열하는 것이고, 나머지는 컴파일러가 걸러낸다.
내가 이 지점에 처음 도달한 사람은 아니다. Scott Spence는 자신의 세션을 직접 세어 보았다 : CLAUDE.md 규칙 하나 덕분에 그의 헤드리스 테스트에서 LSP가 사용되었고, 실제 세션에서는 계속 grep을 쓰고 있었으며, 그래서 지금 그 규칙은 정의와 사용처를 찾을 때 grep을 쓰라고 말한다. AgentConnect의 작은 연구는 그 이유의 일부를 설명한다. 각 이름이 하나의 의미만 갖는 저장소에서는 grep이 이미 모든 참조를 찾아내고 LSP는 아무것도 더하지 못한다. 같은 이름이 여러 곳에 등장하는 저장소에서는 LSP가 도움이 된다. 그리고 LSP는 위치만 돌려주므로 에이전트가 각 파일을 열어 봐야 하지만, grep은 그 줄을 보여 준다.
Spence의 결론은 진단 메시지가 LSP를 둘 이유라는 것이고, 내 기록도 그와 같았다. 다만 내 카운터가 보지 않았던 곳에서였다. 편집 후 언어 서버는 묻지 않아도 세션으로 타입 오류를 돌려보낸다. 타입에 더 이상 존재하지 않는 속성, 잘못된 인자로 호출한 경우 같은 것들이다. 내가 설정한 뒤 며칠 동안 그런 일이 다섯 세션에서 21번 있었다. 나중에 타입 검사가 잡아낼 것과 같은 종류의 실수인데, 편집 직후에 바로 보고된 것이다.
그 과정에서 수행한 검사들(내가 집계한 세션과는 별도의 짧은 헤드리스 세션들)은 Sonnet 기준으로 각각 $0.03에서 $0.50 사이의 비용이 들었다. 위의 6개 테스트 세션은 Opus에서 합계 $7.08, 즉 세션당 약 $1.20이 들었고, 이는 대충 짐작하는 것보다 훨씬 싸다.
요약하면: 에이전트를 위해 도구를 설치하는 것은 시작일 뿐이다. 그것이 실제로 도움이 되는지는 에이전트가 무엇을 하는지 세어 보면 알 수 있다.
치트 시트
- LSP를 앞에 나열하는 mod: 위의 다섯 줄,
CLAUDE_CODE_PLUGIN_DIRS에 이름이 들어간 폴더 안에 있는 것. - TypeScript 대체 경로: 위의 플러그인, 그 플러그인이 가리키는 폴더의 TypeScript 6.0.3, 그리고 공식
typescript-lsp플러그인을 끈 상태. codegraph:npm i -g @colbymchenry/codegraph@1.6.0(Homebrew formula 없음), 저장소에서codegraph init -y를 실행한 뒤claude mcp add-json --scope local codegraph '{..., "alwaysLoad": true}'.alwaysLoad는 LSP에 대해 mod가 하는 것처럼 도구를 앞에 나열한다.- 환경 변수(
CLAUDE_CODE_PLUGIN_DIRS,DOTNET_ROOT,CODEGRAPH_TELEMETRY=0,DO_NOT_TRACK=1)는 모두~/.claude/settings.json의env블록에 넣는다. - 집계 방법:
~/.claude/projects/ / .jsonl의 도구 호출, 그리고 에이전트용으로/subagents/agent-*.jsonl. 검색은Grep/Glob호출이거나 grep 또는 rg를 실행하는 Bash 명령이고, codegraph는mcp__codegraph__*호출이거나codegraph로 시작하는 Bash 명령이다.
행복한(덜한) grepping😎
출처
- 원문: https://medium.com/@montanaqw5809/i-gave-claude-code-an-lsp-then-i-counted-whether-it-mattered-026a4aa764fa?source=rss------claude_code-5 (2026-10-18 · Viktor Soroka)