클라우드플레어가 워커스 런타임(workerd)의 모듈 레지스트리를 다시 짜, 지정자를 URL로 다루고 지연 컴파일과 캐시 공유를 설계에 넣었다. new_module_registry 플래그를 켜면 Node.js 모듈 해석 규칙에 훨씬 가까워진다.
클라우드플레어 워커스에 올리는 코드는 원래 여러 파일과 의존성으로 나뉘어 있지만, 랭글러가 배포 전에 esbuild로 이걸 통째로 이어붙인다. 그렇게 만들어진 번들 하나가 수십만 줄까지 불어난 사례도 있었다. 클라우드플레어는 이 방식의 밑바탕이었던 워커 런타임(workerd)의 모듈 레지스트리 자체를 다시 짰다.
왜 다시 짜야 했나
기존 레지스트리는 모듈 지정자를 URL이 아니라 파일시스템 경로처럼 해석했다. 그래서 import.meta.url을 깔끔하게 구현할 방법이 없었고, 상대 경로 임포트는 new URL()과 다른 규칙을 따랐으며, node: 와 cloudflare: 같은 프로토콜도 진짜 프로토콜이 아니라 문자열 접두사로 특별 취급됐다. 여기에 더해 워커 번들 전체를 실제로 임포트되는지와 상관없이 미리 다 컴파일했고, V8 아이솔레이트마다 그 코드를 따로 한 벌씩 갖고 있었다. 클라우드플레어는 CPU 코어에 부하를 나누려고 같은 워커를 여러 V8 아이솔레이트 복제본으로 띄우는데, 그 결과 같은 소스를 몇 번이고 다시 컴파일하고 메모리에도 여러 벌 올려두는 셈이었다.
지금 뭐가 달라지나
new_module_registry 호환 플래그를 켜면 import.meta.url, import.meta.main, import.meta.resolve()가 모두 동작한다. node: 내장 모듈은 어떤 경로로 불러오든 같은 모듈 인스턴스로 해석되고, with { type: 'json' } 같은 임포트 속성도 제대로 검증된다. require()로 ES 모듈을 불러올 때는 Node.js의 require(esm) 규칙을 그대로 따르고, 에러는 어느 경로로 실패했든 같은 클래스와 메시지를 쓴다. 모듈은 처음 임포트될 때 지연 컴파일되고, WebAssembly 모듈은 소스 페이즈 임포트도 지원한다. 덕분에 클라우드플레어 Vite 플러그인처럼 Rolldown으로 번들링하는 도구가 변형을 덜 하고 모듈 해석을 런타임에 더 맡길 길이 열렸다. --no-bundle로 배포하거나 여러 모듈을 그대로 올리면 작성한 그대로의 그래프가 런타임에 나타난다.
기존 레지스트리 구현이 당장 없어지는 건 아니다. 다만 이번 변화로 워커스는 Node.js가 모듈을 찾고 불러오고 캐싱하는 방식에 한층 가까워졌고, 랭글러나 Vite 같은 번들러가 배포 전에 코드를 미리 변형해야 할 이유도 그만큼 줄었다.