Bun 창업자 Jarred Sumner가 자사 JavaScript 런타임 Bun의 코어를 Zig에서 Rust로 다시 짜는 작업을 11일 만에 끝냈다. 대상은 주석을 뺀 기준 53만 5,496줄, 1,448개 파일 규모의 코드베이스 전체이고, 작업은 Anthropic의 신규 모델(사전 공개판 Claude Fable 5, Mythos 계열)을 이용한 코딩 에이전트가 담당했다. Bun은 월간 다운로드 2,200만 건에 달하고 Claude Code, OpenCode 같은 도구가 런타임으로 쓰는 프로젝트라, 이 정도 규모의 언어 전환을 사람이 아니라 에이전트가 짧은 기간에 해냈다는 점이 화제가 됐다.
왜 Zig에서 Rust로 옮겼나
Bun은 애초에 Sumner가 2021년 esbuild의 Go 코드를 Zig로 이식하며 시작한 프로젝트다. Zig는 가비지 컬렉션이 없는 저수준 언어라 성능은 확보했지만, JavaScript 엔진(JavaScriptCore)이 다루는 가비지 컬렉션 값과 Zig가 직접 관리하는 메모리가 뒤섞이는 지점에서 메모리 버그가 계속 나왔다. Bun v1.3.14에서 고친 버그 목록만 봐도 heap-use-after-free, 이중 해제, 메모리 누수 등이 반복적으로 등장한다. Sumner는 "모든 메모리 할당을 일일이 검토해야 했다"고 설명했다. Rust를 택한 이유는 단순하다. use-after-free, double-free, 해제 누락 같은 오류 상당수가 안전한 Rust에서는 컴파일 시점에 걸러지기 때문이다.
기존에는 왜 재작성이 답이 아니었나
이 정도 규모의 재작성은 그동안 현실적인 선택지가 아니었다. Sumner는 사람이 손으로 했다면 코드베이스 전체를 파악한 엔지니어 3명이 붙어도 1년은 걸렸을 일이라고 말한다. 그 1년 동안은 신규 기능 개발과 버그 수정, 보안 패치를 사실상 멈춰야 하는데, 그런 결정은 내릴 수 없었다. 그래서 대안으로 Zig 코드에 Rust식 스마트 포인터를 얹어 스타일 가이드로 안전성을 강제하는 방향을 준비하고 있었는데, "직접 하고 싶지는 않았다"는 게 본인의 표현이다. 대신 Anthropic의 새 모델로 일주일간 Rust 재작성이 가능한지 시험해 보기로 했고, 며칠 만에 테스트 스위트 통과율이 올라가는 것을 보고 방향을 바꿨다.

실제로 어떻게 진행했나
Sumner가 처음 한 일은 코드를 짜는 게 아니라 규칙을 만드는 것이었다. 코드를 쓰기 전 약 3시간 동안 Claude와 대화하며 Zig 코드베이스의 패턴을 Rust로 어떻게 대응시킬지 정리했고, 이 대화 내용을 Claude가 600줄짜리 PORTING.md 문서로 정리했다. 이 문서는 이후 Hacker News에도 올라와 Zig에서 Rust로 가는 포팅 가이드로 화제가 됐다. 문서에는 "tokio, rayon, hyper 금지. Bun이 자체 이벤트 루프와 시스템 콜을 소유한다", "async fn 금지, 전부 콜백과 상태 머신으로" 같은 구체적인 규칙이 담겼다. 구조체 필드마다 적절한 Rust 라이프타임을 제안하는 LIFETIMES.tsv도 별도로 만들었다.
본격적인 이식에 앞서 1,448개 파일 중 3개만 골라 시범 삼아 옮겨보고, 이 결과를 서로 다른 세션의 Claude 두 개가 적대적으로 검토하게 했다. 그 다음 작업을 64개의 Claude 에이전트로 쪼개 파일 단위로 병렬 처리했는데, 처음에는 여러 에이전트가 같은 git 저장소에서 서로의 변경을 git stash나 git reset --hard로 지워버리는 문제가 생겼다. Sumner는 "저장소가 너무 커서 에이전트마다 별도 워크트리를 주면 디스크 공간이 부족했다"고 설명했다. 이 문제는 워크플로 자체를 수정해 해결했다. 특정 파일만 커밋하는 git 명령 외에는 금지하고, 4개의 워크트리에 16개씩 총 64개의 에이전트를 배치하는 구조로 안정화했다.

1만 6천 건의 컴파일 오류는 어떻게 처리했나
이식이 끝난 뒤에도 코드는 컴파일되지 않았다. 기존 Zig 코드베이스는 사실상 하나의 컴파일 단위였는데, Rust에서는 빌드 속도를 위해 약 100개의 크레이트로 쪼개야 했고 이 과정에서 순환 의존성이 대거 드러났다. 순환 의존성을 정리하는 과정에서만 약 1만 6천 건의 컴파일 오류가 나왔다. Sumner는 이를 "크레이트를 작업 큐로 삼아" 처리했다. 각 크레이트마다 cargo check를 돌려 오류를 파일별로 묶고, 64개의 Claude가 크레이트 단위로 나눠 고치는 방식이다. 구현자 1명이 수정하면 적대적 리뷰어 2명이 검토하고, 다시 반영자 1명이 최종 적용하는 루프를 반복했다. 이 단계는 팀이 잠든 자정부터 오전 11시 30분까지 에이전트가 스스로 돌아가며 진행됐다.
컴파일이 끝난 뒤에는 Bun의 대규모 테스트 스위트를 로컬에서 통과시키는 데 이틀, 이를 3개 플랫폼(Debian, macOS, Windows) CI에서 모두 통과시키는 데 사흘이 더 걸렸다. 최종적으로 Debian 13 x64 기준 138만 6,826개, macOS 14 arm64 기준 125만 9,953개, Windows 2019 x64 기준 100만 7,544개의 테스트 assertion이 모두 통과했고, 테스트를 건너뛰거나 삭제한 사례는 0건이었다. 전체 작업은 5월 3일 시작해 5월 14일 병합까지 11일, 커밋 수는 6,778건이었다.
비용은 16만 5천 달러
이만한 작업에는 대가가 따릅니다. 병합 전까지 캐시되지 않은 입력 토큰 59억 개, 출력 토큰 6억 9천만 개, 캐시된 입력 토큰 읽기 720억 개를 소비했고, API 가격 기준 약 16만 5천 달러가 들었다. Sumner는 이 비용을 두고 "손으로 했다면 코드베이스에 완전히 익숙한 엔지니어 3명이 1년 동안 매달려야 했을 일"이라며, 그 기간 동안 Node.js 호환성 개선이나 버그 수정, 보안 패치, 신규 기능 개발은 전부 멈춰야 했을 것이라고 밝혔다. Zig 러스트 이식과는 별개로 최근 Anthropic이 공개한 사례에서, 같은 회사의 Mike Krieger가 진행한 16만 5천 줄 규모 Python→TypeScript 포팅은 토큰 사용량이 약 2,700만 개로 Bun 사례보다 훨씬 적었는데, 이는 코드 규모 자체가 Bun의 3분의 1 이하였기 때문이다. 두 사례를 겹쳐 보면 토큰 비용은 대체로 코드 규모에 비례해 늘어난다는 것을 확인할 수 있다.

재작성 이후 남은 문제와 반복 가능성
재작성이 끝났다고 버그가 사라진 건 아니다. Bun v1.4.0은 병합 이후 19건의 회귀를 발견해 모두 고쳤는데, 원인은 대부분 Zig와 Rust에서 문법은 비슷하지만 동작이 다른 지점이었다. 예를 들어 Zig의 assert는 함수라 릴리스 빌드에서도 항상 실행되지만, Rust의 debug_assert!는 매크로라서 릴리스 빌드에서는 통째로 사라집니다. 이 차이 때문에 부수 효과가 있는 코드 한 줄이 릴리스 빌드에서만 빠지면서 핫 리로드가 깨지는 문제가 실제로 발생했다. 반대로 좋아진 부분도 있다. Rust의 Drop은 값이 스코프를 벗어나면 자동으로 정리 코드를 실행해, Zig에서 호출부마다 일일이 defer를 챙겨야 했던 메모리 누수 상당수를 없앴다. 한 가지 사례로 Bun.build()를 반복 호출할 때마다 빌드당 약 3MB씩 새던 메모리 누수가 v1.4.0에서 사라졌다.
Sumner는 이 방식이 아무 프로젝트에나 통하는 건 아니라고 선을 긋는다. 코드베이스를 속속들이 아는, 매우 의욕적인 엔지니어가 필요하고, 통과하면 정말로 동작을 신뢰할 수 있는 탄탄한 테스트 스위트가 있어야 하며, 결과를 장담할 수 없는 상태에서 상당한 토큰 비용을 투입할 각오도 필요하다. 이 세 조건 중 테스트 스위트와 의욕적인 엔지니어가 갖춰지면 토큰 비용은 상대적으로 덜 위험한 변수라는 게 이번 사례가 보여주는 지점이다. 1년 걸릴 재작성을 11일로 줄일 수 있다면 시도할 프로젝트가 나올지, 아니면 여전히 예외적인 사례로 남을지는 이후 다른 팀들이 비슷한 시도를 얼마나 반복하느냐에 달려 있다.
출처: The Pragmatic Engineer, "The Pulse: What can we learn from Bun's rapid Rust rewrite with AI?", Bun Blog, "Rewriting Bun in Rust"
