Coldstart의 초기 사용자들이 실제로 망가뜨린 것들
요약
Coldstart 개발자가 초기 사용자들의 실제 사용 환경에서 발생한 설치 오류와 타임아웃 문제를 해결해 나간 과정을 다룹니다. 의존성 충돌과 ESM/CommonJS 불일치 문제를 WASM 도입으로 해결하고, npx의 오버헤드로 인한 타임아웃 문제를 분석한 기술 회고입니다.
핵심 포인트
- 의존성 충돌 문제를 해결하기 위해 파싱 엔진을 WASM으로 전환하여 설치 복잡성 제거
- ESM과 CommonJS 간의 모듈 불일치로 인한 런타임 오류 사례 공유
- npx의 무결성 검사 과정이 MCP 서버 타임아웃을 유발하는 레이스 컨디션 분석
- 개발자 환경과 실제 사용자 환경 사이의 간극을 메우는 디버깅의 중요성
자신의 도구를 직접 테스트하는 것은 그것이 작동한다는 것을 알려줍니다. 실제 사용은 그것이 어떻게 실패하는지를 알려줍니다 — 그리고 그 실패 목록은 거의 완전히 달랐습니다.
저는 몇 달 동안 coldstart를 공개적으로 배포해 왔으며, "내가 사용할 때는 작동한다"와 "낯선 사람이 내가 본 적 없는 기기에서 밤 11시에 설치했을 때 작동한다" 사이의 간극이 제가 배운 가장 유용한 사실임이 드러났습니다. 다음에 이어지는 내용은 모두 가설이 아닙니다 — 릴리스 기록에서 그대로 가져온 것입니다. 실제 에러 메시지, 실제 근본 원인, 실제 수정 사항들입니다.
첫 번째 버그는 설치 그 자체였습니다
누군가 이 도구가 유용한지 유용하지 않은지 말해주기 전에, 대부분의 사람들은 npx coldstart-mcp init이 실제로 완료되는 단계를 통과해야 했습니다. 한동안은 완료되지 않았습니다.
증상은 CPU 점유율이 사실상 100%인 상태로 몇 분 동안, 때로는 무기한 동안 멈춰 있는 것이었습니다. 원인을 찾아냈을 때는 거의 코미디 같았습니다: 두 개의 tree-sitter 문법(grammar) 패키지가 충돌하는 peer-dependency 범위를 선언했고(^0.21.x vs ^0.22.x), npm의 resolver는 콜드 캐시(cold cache) 상태에서 기준이 될 lockfile이 없자 그냥... 계속 돌기만 했습니다. 디버그 로그에서 수렴하지 못한 채 반복되는 89,000개 이상의 placeDep ROOT 라인을 발견했습니다.
첫 번째 수정 방법은 --legacy-peer-deps 플래그를 사용하는 것과 더 명확한 문서였습니다. 그것은 한동안 유지되었으나, 이후 다른 방식으로 다시 문제가 발생했습니다: __filename is not defined. 이는 ESM/CommonJS 간의 직접적인 불일치로 인해 init이 아무것도 하기 전에 충돌을 일으켰습니다. 테스트가 모듈을 직접 실행하면서, 정작 문제가 발생한 실제 설치 경로(install-path)의 코드는 건드리지 않았기 때문입니다.
실제로 효과가 있었던 수정은 더 과감했습니다: v2.1.0에서 전체 파싱 엔진을 WASM으로 옮긴 것입니다. 네이티브 node-gyp 컴파일도 필요 없고, 조정해야 할 문법 peer-dependency 범위도 없으며, 설치 스크립트도 필요 없습니다. npm install -g coldstart는 완전히 평범한 설치가 되었습니다. 이것이 이 목록의 모든 곳에서 나타나는 패턴입니다 — 첫 번째 수정은 증상을 다루지만, 진짜 수정은 실패의 범주 자체를 제거합니다.
아무도 재현할 수 없었던 타임아웃
한동안 사람들은 MCP server "coldstart" connection timed out after 30000ms 오류를 보고했지만, 저는 무엇을 시도해도 로컬 환경에서 이를 재현할 수 없었습니다.
그 원인은 특별히 찾아 나서지 않는 한 보이지 않았습니다. .mcp.json이 npx -y coldstart-mcp를 통해 서버를 실행하고 있었는데, 매 실행 시마다 npx가 npm의 공급망 무결성 검사 (supply-chain integrity check)의 일환으로 전체 설치 트리(install tree)를 다시 해싱(re-hash)하고 있었습니다. 여기에는 15개의 tree-sitter 패키지가 포함되어 있었고, 그중 몇 개는 수 메가바이트 크기의 네이티브 바이너리 (native binaries)를 포함하고 있었습니다. 이 과정에 25~30초가 소요되었습니다. MCP 클라이언트의 타임아웃 (timeout) 설정은 30초였습니다. 이것은 간헐적인 오류 (flaky)가 아니라, 사용자의 디스크와 CPU 속도에 따라 결정되는 레이스 컨디션 (race)이었습니다. 그래서 제 컴퓨터에서는 재현되지 않았고, 다른 사용자들의 환경에서는 항상 발생했던 것입니다.
해결책은 플래그 (flag)를 추가하는 것이 아니라, "매 에디터 세션마다 실행"되는 패턴에 npx가 잘못된 도구임을 인식하는 것이었습니다. npx는 일회성 스캐폴더 (scaffolder)를 위해 만들어진 것이지, 장기 실행 서버 (long-running servers)를 위한 것이 아닙니다. 이제 init 명령어가 비용이 많이 드는 설치를 한 번 수행한 다음, node /path/to/index.js를 직접 호출하도록 작성합니다. 설정 이후에는 npm의 개입 없이 1초 미만의 스타트업 (startup) 속도를 보장합니다. 기존 사용자들은 v1.4.0을 처음 실행할 때 타임스탬프가 포함된 백업과 함께 설정 파일이 자동으로 재작성되는 자동 마이그레이션 (auto-migration)을 받았으며, 별도의 조치는 필요하지 않았습니다.
세션 도중 내용을 잊어버린 노트북
이 버그는 저의 긴 세션에서만 나타났는데, 이는 다른 방법으로는 정말 잡기 어려운 범주의 버그입니다.
캡처 파이프라인 (capture pipeline)은 마지막 캡처 이후 무엇이 새로 추가되었는지 알기 위해 저장된 라인 오프셋 (line offset)을 기준으로 에이전트의 트랜스크립트 (transcript)를 분할했습니다. Claude Code의 /compact 명령은 트랜스크립트를 훨씬 짧게 다시 작성하는데, 저장된 오프셋이 계속해서 새로 작성된 더 짧은 끝부분 너머를 가리키고 있었던 것입니다. 압축 (compaction) 이후의 모든 턴 (turn)은 해당 세션이 끝날 때까지 캡처 과정에서 소리 없이 보이지 않게 되었습니다. 에러도, 경고도 아니었습니다. 그저 아무 일도 일어나지 않았을 뿐이며, 하필이면 가장 기억할 가치가 있는 길고 복잡한 세션들에서 정확히 그런 일이 발생했습니다.
해결책은 shrink guard(축소 가드)였습니다. 트랜스크립트(transcript)가 저장된 오프셋(offset)보다 짧아지는 것을 감지하면, 이를 리셋하고 새로운 베이스라인(baseline)부터 다시 처리하는 방식입니다. 작은 수정이었지만, 이 기능이 존재할 수 있었던 이유는 제가 유닛 테스트(unit test)로는 절대 드러나지 않을 법한, 실제의 지저분하고 오래 지속되는 작업들에 이 도구를 사용했기 때문입니다. 유닛 테스트는 애초에 /compact에 도달할 만큼 충분히 오래 실행되지 않으니까요.
Recall(회상)은 대부분 노이즈였으며, 저는 정확히 그 양을 측정했습니다
이 부분은 제가 단순히 느낌(vibes)에 의존해 패치하지 않은 것이 가장 다행이라고 생각하는 지점입니다.
제 자신의 세션들을 관찰하며 느낀 불만 사항은, 관련이 있을 법할 때 과거의 노트를 자동으로 띄워주는 기능인 Recall(회상)이 대부분의 경우 잘못된 내용을 주입하는 것처럼 느껴진다는 것이었습니다. '느껴진다'는 것은 수정 기준이 될 수 없기에, 저는 140개의 실제 주입 사례를 대상으로 그것이 실제로 관련이 있는지 여부를 수동으로 라벨링(hand-labeled)했습니다. 기준 정밀도(Baseline precision)는 31%였습니다.
각각 독립적으로 측정 가능한 세 가지 별개의 원인이 있었습니다:
- 단일 일반 단어 매칭은 주제 매칭이 아니라 대부분 동음이의어였습니다. "Merge the PR"은 노트북 병합(notebook-merge) 노트를 불러왔습니다. "Status 403"은 단지 "status"라고 불리는 관련 없는 노트를 불러왔습니다. 이는 모든 주입의 61%를 차지했으며, 정밀도는 5%에 불과했습니다. 해결책은 점수 임계값(score threshold)이나 불용어 목록(stopword list)이 아니었습니다. 둘 다 시도해 보았지만, 홀드아웃 검증(held-out validation)에서 모두 실패했습니다. 해결책은 해당 용어가 코드 형태(code-shaped)이거나 명시적으로 이름이 지정되지 않은 한, 두 번째의 확증 신호(corroborating signal)를 요구하는 것이었습니다.
- 동일한 노트가 한 세션 내에서 반복적으로 다시 나타났습니다. 최악의 경우 12번이나 나타났는데, 이는 첫 번째 주입 때 이미 콘텐츠가 컨텍스트(context)에 놓여 있었기 때문에 순수한 비용(tax) 낭비였습니다.
- Harness 텔레메트리(telemetry)가 관련성 쿼리(relevance query)로 유출되었습니다. "while running local commands"와 같은 상용구 래퍼(boilerplate wrapper) 텍스트만으로도 관련 없는 노트를 두 단계나 끌어올리기에 충분했습니다. 이를 제거하는 stripper(스트리퍼)는 코드에 존재했지만, 실제로 배포된 적은 없었습니다.
세 가지 수정 사항을 모두 적용한 후: 정밀도(precision) 47%, 실제 유용했던 주입(injections)의 86%를 유지했습니다. 저는 이와 함께 솔직한 주의 사항을 함께 게시했습니다. 즉, 이 수치는 하나의 저장소(repo)에 있는 노트북과 한 사람의 프롬프트 스타일에서 나온 것이며, 홀드아웃 분할(held-out split) 데이터셋에서는 규칙이 유지되었지만, 이것이 모든 코드베이스에 일반화된다고 주장하는 것은 아니라는 점입니다. 이것은 마케팅용 숫자가 아니라, 측정된 실제적인 진전입니다.
버그 리포트가 전혀 아니었던 피드백
모든 것이 무언가 고장 나서 발생한 것은 아니었습니다. v2.0.0에서 coldstart-mcp를 coldstart로 이름을 변경한 것은 서서히 깨달아가는 과정에서 나온 결과였습니다. 기존 패키지 메타데이터(package metadata) — 설명(description), 키워드(keywords) — 에
패턴이 있다면 다음과 같습니다. 실제로 효과가 있었던 거의 모든 수정 사항은 눈앞에 놓인 특정 사례를 패치하기보다는 실패의 범주(category of failure)를 제거하는 방식이었습니다. WASM 재작성(rewrite) 대신 플래그(flag)를 사용하는 것은 일주일 동안 증상만을 해결했습니다. 재작성은 이를 영구적으로 해결했습니다. 이는 이제 제가 변경 사항을 배포하는 방식에 있어 하나의 규칙에 가까운 것이 되었습니다. 즉, 국소적이고 제한된 수정은 임시방편(stopgap)으로서 종종 옳지만, 저는 단순히 특정 사례(instance)가 아니라 그 해결책이 어떤 문제 유형(class of problem)을 해결했는지 이름을 붙일 수 있을 때까지는 아무것도 실제로 완료되었다고 간주하지 않습니다.
coldstart는 오픈 소스이며 MIT 라이선스를 따릅니다. npm install -g @cstart/coldstart로 설치한 후, 어떤 저장소(repo)에서든 coldstart init을 실행하세요. 만약 이 목록에 없는 문제에 직면한다면, GitHub Discussions가 열려 있으며, 위에서 언급한 모든 내용의 편집되지 않은 전체 기록은 release notes에서 확인할 수 있습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기