Claude가 작성한 패키지 매니저를 믿기까지 필요했던 것

이 글은 AI로 영어에서 번역했습니다. 영어 원문 읽기

jpm을 만들면서 나는 코드를 한 줄도 쓰지 않았고, 읽어 보지도 않았다. jpm은 npm, pnpm, yarn, bun 프로젝트에서 쓸 수 있는 JavaScript 패키지 매니저다. 크기를 줄이려고 AI에게 TLS, 암호화, http 모듈까지 직접 짜게 한 것도 포함해서다. 처음에는 말 그대로 바이브 코딩 프로젝트였다. 게다가 오후 한나절 만에 끝났다. 그 상태로는 나도, 다른 누구도 믿으면 안 됐다. 하지만 그다음 27년 넘게 전문 소프트웨어 엔지니어로 일하며 배운 걸 가져와서, 기존 테스트 스위트로 검증하게 하고, 5일 동안 스스로 코드를 짜고 철저하게 테스트하게 해서 단단하게 만들었다.

내 배경

나는 그동안 여러 프로그래밍 언어를 써 왔지만, 대부분 C#, Elixir, JavaScript 같은 고수준 언어였다. 어릴 때 C를 배우면서 여름마다 아버지 회사에서 저수준 디스크 컨트롤러 일을 도운 이후로는, 저수준 언어로 함수 몇 개 이상 짜 본 적이 없다. 그러니 Rust를 익혀서 60k+ 줄짜리 프로젝트를 만드는 건 짧은 시간에 할 수 있는 일이 아니었다. 내가 아는 건 npm, pnpm, bun이 어떻게 동작하는지다. Node.js 0.8(2012)부터 일하면서 Node.js를 써 왔기 때문이다. 그리고 소프트웨어에 실제로 얼마나 많은 테스트가 필요한지, 한 번 하고 끝내는 게 아니라 자동화하는 게 얼마나 중요한지도 안다. 테스트부터 먼저 짜라던 애자일 하는 사람들 말을 들은 보람이 있었다!

왜 jpm인가

YouTube의 Syntax라는 팟캐스트를 보고 있었다. 진행자는 Wes, Scott, CJ였고, 그 에피소드는 upm 이야기로 시작했다. 빠르고 정말 작은 패키지 매니저인데, JavaScript로 작성된 것이었다. 먼저 든 생각은, 작다는 말은 틀렸다는 것이었다. Node.js 런타임이든 다른 런타임이든 같이 포함해야 하니까. upm은 Pooya가 만들었고, CJ는 프로젝트가 제공한 벤치마크로 빠르다는 걸 보여 주고 있었는데, 나는 그냥 우리가 더 빠르게 만들 수 있을지 보고 싶었다. Pooya는 JavaScript 생태계를 위해 좋은 도구를 많이 만든 실력 있는 엔지니어라서, 쉽지 않으리라는 건 알고 있었다. 마지막으로, 그때 나는 Opus 5.5 모델을 쓰고 있었는데 정말 감탄하고 있었다. 그래서 생각했다. 이미 수많은 도구가 Rust로 만들어졌으니(팟캐스트에서도 그걸로 농담을 했다), 나도 Rust로 정말 작고 빠른 패키지 매니저를 만들 수 있지 않을까.

팟캐스트에서 진행자들은 청취자들이 보낸 AI 관련 질문에 답하고 있었다. 요즘 모두가 묻는 질문들이다. AI가 진짜 소프트웨어를 만들 수 있을까, 아니면 데모뿐일까? 엔지니어는 걱정해야 할까, 아니면 기대해야 할까? 대부분은 AI가 뭐든 한 번에 만들어 낼 수 있다고 믿는다. 스포일러: 할 수 있다. 다만 누군가 이끌어 주고 소프트웨어 개발을 이해하지 않으면 결과가 좋지 않을 뿐이다.

그날 오후

그래서 그날 오후, 본업을 마무리하고 나서 터미널에 탭을 하나 열고 Claude에게 upm을 Rust CLI로 다시 작성하려면 무엇이 필요한지 물었다. 요약을 훑어보고 OK를 하자 작업을 시작했다. 아마 45분 정도 걸렸던 것 같은데, 동작하는 버전이 나왔다. 다만 서드파티 Rust 크레이트를 잔뜩 써서 훨씬 커졌고, 작은 package.json 파일 하나로만 테스트한 상태였다. 크기를 줄이려고 무엇 때문에 이렇게 커졌는지 분석하게 했고, 그날 저녁이 끝날 무렵에는 직접 만든 HTTP, JSON, 암호화, TLS 모듈과 훨씬 작은 바이너리가 생겼다. 필요한 부분만 작성하라고 지시했지만, 동시에 가능하면 다른 프로젝트의 테스트로, 안 되면 우리 자체 테스트와 퍼즈 테스트로 철저하게 테스트하라고도 했다. 확실히 탄탄한지 확인하고 싶었다.

5일간

그리고 여기가 “바이브 코딩”으로 프로젝트를 만드는 사람들이 다 빼먹는 부분이다. 물론 Windows WSL2의 Linux에서 작은 package.json 파일로는 동작했다. 하지만 Mac이나 Windows에서는 테스트하지 않았고, 정말 큰 프로젝트도 전혀 돌려 보지 않았다. 그래서 단단하게 만들기 위한 스크립트를 작성하게 했다. 대형 공개 프로젝트 100개, 나중에는 166개를 가져와서 설치하고 빌드해 본다. 그런 다음 그 프로젝트가 쓰는 패키지 매니저로 설치하고 빌드해서 비교한다. 가능한 곳에서는 테스트도 돌린다. 어떤 테스트는 시크릿이나 다른 설정이 필요했기 때문이다. Windows 명령줄과 PowerShell에서도 똑같이 하게 했고, Mac에서도 에이전트를 하나 띄워서 같은 일을 시켰다. 그리고 5일 뒤, 무엇을 어떻게 지원할지 많은 결정을 내리면서 머지된 PR이 261개가 되었고, 단단하고 테스트가 잘 된 패키지 매니저가 나왔다.

이끈다는 건 어떤 모습이었나

그러니까 코드는 쓰지 않았지만, 방향은 내가 잡았고 작은 결정을 아주 많이 내렸다. 예를 들어 Claude는 더 빠른 락파일 형식으로 바꿔 봐야 몇 ms밖에 줄지 않는다고 했지만, 나는 그래도 바꿔 달라고 했다. Claude는 전역 스토어를 옵트인 기능으로 만들고 싶어 했지만, 패키지의 99%가 쓸 수 있다면 왜 그래야 하느냐고 짚었고, 결국 기본값이 되었다. 또 의존성의 설치 스크립트를 포함해서, 괜찮다고 명시적으로 적어 두지 않은 스크립트는 실행하지 않길 원했다. 요즘은 AI가 일 대부분을 하니까, 패키지의 스크립트를 승인하기 전에 먼저 AI에게 문제가 없는지 검토해 달라고 하면 된다는 생각이었다.

Claude가 실제로 반대한 적도 두 번 있었는데, 두 번 다 내가 밀어붙였다.

첫 번째는 TLS였다. 바이너리를 줄이고 싶었는데, TLS와 암호화 크레이트가 그중 큰 부분을 차지했다. Claude는 그건 절대 직접 작성하지 않는 코드라고 했다. 나는 그건 옛날 엔지니어링 사고방식이라고 말했다. 우리는 Rust를 쓰고 있으니 Heartbleed 같은 사고의 원인이 된 메모리 버그는 애초에 논외이고, 우리 버전도 똑같이, 아니 더 철저하게 테스트할 수 있다고. Claude는 위험을 과장했다고 인정했고, 그걸 증명할 테스트 스위트(Wycheproof, NIST 테스트 벡터, x509-limbo, BoGo)를 정리해 냈다. 나는 지금도 이게 좋은 생각이었는지 의문이 들고, Claude는 듣기 좋은 말을 해 주는 걸 좋아하니까 위험을 과장했다고 한 것도 그냥 비위를 맞춰 준 것일 수 있다. 다른 프로젝트의 실제 버그로 만든 테스트를 포함해 수많은 테스트를 통과한다. 누가 작성했든 상관없이, 지금은 훨씬 더 믿을 수 있다는 건 안다.

두 번째는 HTTP/2였다. 속도를 더 내고 싶어서 물어봤다. Claude는 먼저 curl로 테스트해 보더니 만들 가치가 없다고 했다. HTTP/1.1로 하는 jpm의 전체 설치가 이미 curl의 HTTP/2 다운로드 하나보다 빨랐기 때문이다. 그래도 나는 밀어붙였다. curl은 모든 경우를 처리해야 하지만, 우리는 레지스트리 하나에 GET을 많이 보내기만 하면 된다. bun을 이기려면 틀을 벗어나서 생각해야 한다. Claude는 동의했고, 두 시간쯤 뒤에 동작하는 클라이언트가 나왔다.

그다음 측정했다. 우리 집 회선에서는 HTTP/2가 약 17% 빨랐다. GitHub 러너에서는 비기거나 졌고, nuxt 콜드 설치에서는 최대 2배 느렸으며, 큰 tarball에서는 CPU를 8~15% 더 썼다. 설치가 실제로 시간과 돈을 잡아먹는 곳은 CI이고, 바이너리도 60 KB 늘었다. 그래서 3,671줄을 전부 삭제하게 했다. 그건 내가 틀렸고, 숫자가 그걸 알려 줬다.

반대의 경우도 있었다. jpm은 원하면 Node도 설치해 주는데, 나는 Node 서명 검사가 설치를 느리게 할 거라고 생각해서 플래그를 켜야만 동작하게 하고 싶었다. Claude는 측정해 보니 15~19 ms라고 했고, 플래그로 두면 결국 아무도 켜지 않는다고 했다. 지금은 기본으로 켜져 있고, 보안이 더해지니 다행이라고 생각한다.

아무도 타이핑하지 않은 코드를 믿는 이유

짧게 답하면 Wycheproof, node-semver, pnpm, yarn, bun 자체의 테스트, 그리고 테스트에 쓴 저장소 166개다. 그 과정에서 찾아낸 버그 몇 가지:

  • node-semver 자체 테스트에서 jpm의 판단이 다른 경우를 25건 찾아냈다. 예를 들어 ^9007199254740991.0.0을 범위로 받아들이는 식이었다.
  • npm-package-arg의 테스트는 처음에 154개 중 117개만 일치했다. a@a.tar.gz가 태그로 읽혔다.
  • 보안 스캔에서 클론한 저장소의 .npmrc가 여러분의 레지스트리 토큰을 자기 프록시를 통해 보내게 할 수 있다는 걸 찾아냈다. 개념 증명에서는 Bearer USER-SECRET-TOKEN이 탈취됐다. 이제 프로젝트의 .npmrc로는 보안을 낮출 수 없다.
  • 실제 저장소에서 jpm의 설치 결과가 pnpm과 달랐던 의존성 링크는 1,320개에서 183개로, npm과 달랐던 것은 614개에서 14개로 줄었다.
  • macOS에서 테스트하기 전까지는 macOS에서 아예 컴파일조차 되지 않았다.

이렇게 오랫동안 소프트웨어를 만들어 왔으니, 테스트를 얼마나 짜고 얼마나 돌리든 버그 없는 건 없다는 것도 안다. 그리고 신뢰는 쌓아 가는 것이니, 앞으로 내 모든 프로젝트에서 이걸 쓰면서 수정도 계속 받을 생각이다. AI가 보낸 수정이라도 말이다. 그렇다고 전부 받겠다는 건 아니다. 테스트로 무언가를 고친다는 걸 증명해야 한다. 요점은 PR을 닫아 둘 생각은 없지만, 그걸 처리하는 데 AI의 도움을 받겠다는 것이다.

현재 상황

실행 파일은 2MB까지 줄였고, 항상 가장 빠른 건 아니지만 최상위권에 있다. 승인 없이는 스크립트를 실행하지 않으니 가장 안전한 축에도 든다. 게시된 지 최소 하루가 지나지 않은 패키지는 최신 버전으로 업데이트하지 않는다. 그리고 .npmrc 관련 보안도 있다.

다음은 GitHub Actions 러너에서 다른 패키지 매니저와 비교해 우리가 얼마나 빠를 수 있는지 보여 주는 차트 중 하나다:

GitHub CI, Linux에서 nuxt(패키지 591개) 콜드 설치, 5회 실행 중앙값: jpm 1.22 s, bun 1.28 s, pnpm 1.72 s, aube 3.00 s, upm 3.52 s, deno 7.64 s, yarn 9.63 s, npm 18.54 s

모든 머신, 단계, 프로젝트 결과는 getjpm.sh에 있다.

가장 빠르지 않은 곳

솔직히 말하면 벤치마크 규모는 작다. 저장소 세 개뿐이다. 그리고 우리가 항상 가장 빠른 것도 아니다. 예를 들어 Windows의 nuxt에서는 Defender 검사가 속도를 다 잡아먹는다. macOS에서는 nuxt 웜 설치에서 bun과 aube가 이긴다. 그러니 속도가 최종 목표라면 개선할 여지가 있다. Linux, macOS, Windows에서 프로젝트 166개를 설치해 봤고, 버그를 찾으려고 Linux에서는 무작위 프로젝트를 더 많이 돌렸지만, 하나하나 CI와 각 머신에서 성능 테스트를 하지는 않았다.

jpm은 완벽할까? 아니다. 내가 27년 동안 손으로 작성한 그 무엇도, 어떤 사람이 작성한 그 무엇도 완벽하지 않다. 완벽이란 존재하지 않는다. 충분히 좋은 것이 있고, 훌륭한 것이 있고, 지금 가진 게 어느 쪽인지 알 만큼 충분히 테스트한 것이 있을 뿐이다.

macOS와 Linux에서는 curl -fsSL https://getjpm.sh | sh, Windows에서는 irm https://getjpm.sh/install.ps1 | iex로 써 보고, 아무 프로젝트에서나 jpm install을 실행해 보자. 버그를 찾으면 GitHub에 이슈를 열어 달라.