엔지니어링 즉시 반복: Zig의 새로운 ELF 링커와 빌드 시스템 재구성 내부

시스템 프로그래머에게 '편집-컴파일-테스트' 사이클은 종종 생산성의 가장 큰 병목 현상입니다. JavaScript나 Python과 같은 고수준 언어가 거의 즉각적인 피드백을 제공하는 반면, 시스템 언어는 전통적으로 반복 속도를 런타임 성능과 맞바꾸어 왔습니다.

Zig 프로그래밍 언어에 대한 최근 업데이트는 2026년 devlog에 자세히 소개되었으며, 이 격차를 메우기 위한 집중적인 노력을 보여줍니다. ELF 링커를 재구상하고, 빌드 시스템을 분리하며, 컴파일러 내부 타입 해석을 최적화함으로써 Zig는 복잡한 프로젝트에서도 재빌드가 밀리초 단위로 이루어지는 개발 경험을 향해 나아가고 있습니다.

새로운 ELF 링커: 밀리초 재빌드

가장 영향력 있는 업데이트 중 하나는 새로운 ELF 링커의 도입(-fnew-linker 로 활성화)입니다. 초기에는 Zig 전용 코드에만 제한되었지만, 이제는 LLVM 및 LLD 라이브러리를 포함한 자체 호스팅 Zig 컴파일러 자체를 링크할 수 있는 강력한 도구로 발전했습니다.

빠른 증분 컴파일

새로운 링커의 핵심 기능은 x86_64 Linux에서 빠른 증분 컴파일을 지원한다는 점입니다. 기존 링크 방식에서는 작은 변경조차도 비용이 많이 드는 재링크 과정을 유발할 수 있습니다. 새로운 구현은 외부 라이브러리와 C 소스를 링크하면서도 추가적인 성능 오버헤드 없이 증분 재빌드를 가능하게 합니다.

실제로는, Tetris 클론과 같은 프로젝트의 변경 사항을 약 30ms 안에 재빌드할 수 있음을 의미합니다. Zig 컴파일러 자체도 증분 빌드 시간이 약 200~300ms로 감소했습니다. 이 기능은 개발자가 상태를 관찰하기 위해 자주 작은 변경을 가하는 '프린트 디버깅'에 특히 혁신적입니다.

현재 제한 사항

진전에도 불구하고, 링커는 아직 기능이 완전하지 않습니다. 가장 큰 누락 요소는 Zig 코드에 대한 DWARF 디버그 정보 생성이며, 이는 핵심 팀의 우선 순위로 남아 있습니다. 따라서 현재 링커는 최종 프로덕션 도구라기보다 빠른 반복을 위한 생산성 도구에 가깝습니다.

빌드 시스템 분리

링커 개선과 병행하여, Andrew Kelley는 zig build 프로세스의 주요 아키텍처 재구성을 도입했습니다. 이전에는 build.zig 파일과 빌드 시스템 구현이 디버그 모드에서 하나의 거대한 프로세스로 컴파일되었습니다.

Configurer와 Maker

새로운 아키텍처는 프로세스를 두 개의 뚜렷한 역할로 분리합니다:

  1. Configurer(구성자): build.zig 로직이 메모리 내에 빌드 그래프를 구성하는 작은 프로세스로 컴파일됩니다. 이 그래프는 바이너리 설정 파일로 직렬화되어 캐시됩니다.
  2. Maker(제작자): 릴리스 모드로 컴파일된 별도 프로세스가 직렬화된 설정 파일을 기반으로 빌드 그래프를 실행합니다.

성능 향상

이 분리는 세 가지 주요 이점을 제공합니다:

  • 컴파일 감소: 변경 시 전체 빌드 시스템이 아니라 사용자의 build.zig만 컴파일됩니다.
  • 중복 로직 건너뛰기: 빌드 설정이 변경되지 않은 경우(예: -freference-trace 플래그 추가), Zig는 build.zig 실행을 완전히 건너뛰고 캐시된 바이너리 설정을 사용할 수 있습니다.
  • 실행 최적화: 실제 빌드 그래프를 실행하는 프로세스는 이제 최적화가 활성화된 상태로 사전 컴파일됩니다.

벤치마크 결과 간단한 명령에 대한 실제 시간(wall-clock time)이 크게 감소한 것을 보여줍니다. 예를 들어, zig build -h가 150ms에서 14.3ms로 감소했으며, 이는 90.4%의 개선률에 해당합니다.

컴파일러 내부: 타입 해석과 LLVM

링커와 빌드 시스템을 넘어, 컴파일러 내부가 대대적인 개편을 겪었습니다. 30,000줄에 달하는 PR이 타입 해석 로직을 더 게으르고 정밀하게 재설계했습니다.

지연 분석 및 향상된 오류

Zig는 이제 타입 필드 분석을 더 게을러졌습니다. 타입이 단순히 네임스페이스로만 사용되는 경우( Zig에서 흔한 패턴), 해당 타입 내부의 필드는 실제로 초기화되지 않는 한 분석되지 않습니다. 이는 포함된 타입이 상수만을 위해 사용될 때 불필요한 @compileError 호출이 발생하는 것을 방지합니다.

또한, "dependency loop" 경험이 개선되었습니다. 도움이 되지 않는 오류 대신, 컴파일러는 루프가 정확히 어디서 발생하는지 상세한 노트를 제공하며(예: type 'repro.Foo' depends on type 'repro.Bar').

LLVM 증분 지원

증분 컴파일은 LLVM 코드 생성 백엔드에도 확장되었습니다. 이는 "LLVM Emit Object" 단계 자체를 가속화하지는 않지만(LLVM이 자체적으로 처리), Zig 자체 컴파일러 코드에서 소요되는 시간을 최소화합니다. 이를 통해 LLVM 백엔드를 사용하더라도 컴파일 오류가 밀리초 단위로 보고됩니다.

생태계 확장: I/O와 libc

Zig는 레거시 C 추상화로부터 더 깊은 통합과 독립성을 지속적으로 추구하고 있습니다.

실험적 이벤트 기반 I/O

Zig는 io_uring(Linux)과 Grand Central Dispatch(macOS)를 위한 실험적인 std.Io.Evented 구현을 도입했습니다. 이들은 "fibers"(스택 기반 코루틴)를 사용하여 애플리케이션 로직을 변경하지 않고도 I/O 구현을 손쉽게 교체할 수 있게 합니다. 이를 통해 개발자는 초기화 코드를 변경하는 것만으로 스레드 기반 I/O 모델에서 이벤트 기반 모델로 전환할 수 있습니다.

zig libc 프로젝트

중복을 줄이고 바이너리 크기를 개선하기 위해, zig libc 프로젝트는 벤더링된 C 소스 파일을 Zig 표준 라이브러리 래퍼로 점진적으로 교체하고 있습니다. Zig Compilation Unit(ZCU)을 공유함으로써 이러한 함수들은 함께 최적화될 수 있으며, 이는 libc 경계를 넘어선 링크 타임 최적화(LTO)를 효과적으로 제공합니다.

커뮤니티 관점

이러한 변화에 대한 커뮤니티의 반응은 Zig가 개발자 속도를 희생하지 않는 실용적이고 고성능의 C 대안으로 자리매김하고 있다는 믿음이 커지고 있음을 보여줍니다. 한 사용자는 다음과 같이 언급했습니다:

"이 링커와 다른 타깃에 대한 증분 컴파일이 도입되면, Zig는 궁극적인 C 대체제가 될 것이며, 이는 C나 Rust의 성능과 함께 JS나 Python 수준의 반복 속도를 제공할 것입니다."

일부는 C의 고유한 단순성 때문에 어떤 언어도 C를 완전히 대체할 수 없다고 주장하지만, Win32 API의 "불필요함"을 없애고 빌드 루프를 최적화하며 툴체인을 정제하는 데 초점을 맞춘 Zig의 궤적은 현대 스크립트 언어의 사용성을 갖춘 시스템 언어의 힘을 제공하는 목표를 시사합니다.

Sources