Rust 개발자가 Zig로 돌아와 4개월을 써 보니: 명시적 할당자와 빠른 빌드, 그리고 1.0 전에 남은 숙제

한눈에 보는 핵심 요약
몇 년 동안 Zig를 떠나 Rust를 주로 쓰던 한 개발자가 다시 Zig로 돌아와 약 4개월 동안 라이브러리와 프로그램을 만들었습니다. 결론은 이렇습니다. 아직 불안정한 부분이 있지만, 이제 대부분의 프로젝트에서 Zig를 먼저 고르겠다는 것입니다.
- 좋았던 점: 할당자와 IO를 인자로 넘기는 명시적 자원 관리, comptime, 다른 도구와 잘 맞물리는 빌드 시스템, 의존성을 적게 쓰는 문화,
try를 이용한 간결한 오류 전파, 빠른 증분 컴파일 - 아쉬웠던 점: 표준 라이브러리의 잦은 변경, 부족한 수명 검사와 문서, 컴파일 타임 할당자 부재, 바이너리 블롭에 기대는 부트스트랩
- 제약 사항: 증분 컴파일은 현재 Linux와 macOS의 x86_64에서만 지원됩니다
아래에서 항목별로 자세히 살펴보겠습니다.
복귀 후 만든 프로젝트들
필자는 주로 Rust와 비교하면서 Zig를 다시 써 봤습니다.
- snurl: Gemini, Finger, Gopher, Spartan 같은 smallnet 프로토콜을 다루는 라이브러리입니다. curl보다 작은 규모로, 그에 버금가는 테스트·퍼징 수준을 갖춘 smallnet용 대안을 목표로 합니다. 프로젝트 규모에 맞는 범위에서 TigerStyle도 따르려 했다고 합니다.
- TUI 타이핑 경주 게임: 최소 기능 버전은 예상보다 쉽게 만들었다고 합니다.
- n체 시뮬레이터: raylib을 쓰고 항성 진화 기능까지 넣었습니다. (두 프로그램 모두 아직 공개되지 않았습니다.)
처음부터 걸림돌도 있었습니다. zig init이 만들어 주는 main.zig에 퍼징 예제가 들어 있어 기대했지만, Zig 내장 퍼저가 아직 완성되지 않아 실제로는 쓸 수 없었다고 합니다.
Seattle Zig Day: 커뮤니티의 힘
Zig Day는 지역 사용자들이 모여 함께 프로젝트를 진행하고 Zig 이야기를 나누는 모임입니다.
- 필자는 8월 20일 Lobsters에서 Seattle Zig Day 소식을 봤고, Zig를 만든 Andrew Kelley가 온다는 걸 알고 더 기대하게 됐다고 합니다.
- 신청자가 수용 인원을 넘어서 장소가 Bellevue 도서관에서 Bothell 도서관으로 바뀌었습니다.
- 친구 Evin Yulo와 snurl 작업을 함께 했고, Evin은 Zig 표준 라이브러리의 TLS 구현을 대체 백엔드로 붙이는 작업을 시작했습니다.
- 다른 개발자들과 프로젝트 이야기를 나눴고 Andrew Kelley와도 짧게 대화했습니다.
지역 그룹이 직접 운영하는 형식이라 커뮤니티와 연결된 느낌이 강했다고 합니다. 필자는 앞으로도 Seattle 행사에 참석할 계획이고, Portland 모임에 가는 것도 생각하고 있습니다.
anyzig로 여러 Zig 버전 오가기
Zig는 버전마다 변화가 크기 때문에 버전 관리가 중요합니다. 필자는 anyzig로 프로젝트에 따라 Zig master와 0.16.0을 오가며 썼습니다.
다만 build.zig.zon이 없는 디렉터리에서는 anyzig가 작동하지 않습니다. 그래서 프로젝트 밖에서는 zig 명령에 의존하는 도구를 쓸 수 없고, 일회성 Zig 스크립트의 자동 포맷도 깨졌습니다. 이런 상황에서 쓸 기본 Zig 버전을 지정하는 기능이 있었으면 좋겠다고 합니다.
Zig의 가장 큰 무기: 명시적 할당자와 IO
할당자를 인자로 넘기면 생기는 일
Zig에서는 메모리를 할당하는 함수가 할당자를 인자로 받습니다. 덕분에 코드 부분마다 다른 할당 전략을 분명하게 고를 수 있습니다.
- 상황에 따라 아레나 할당자, 범용 할당자,
FixedBufferAllocator를 골라 쓸 수 있습니다. - 전역 기본 할당자나 Odin의 암묵적인
context와 비교하면, 함수가 메모리를 할당하는지가 잘 드러나고 엉뚱한 할당자를 넘기기도 어렵습니다. - 필자 경험으로는 Rust보다 Zig에서 메모리 할당을 추론하기가 쉬웠다고 합니다. Rust의 숨겨진 할당은 메모리가 부족하면 패닉을 일으키고, 명시적 할당 지원은 처음부터 설계된 게 아니라 나중에 추가되고 있는 기능입니다.
IO도 인자로 넘긴다
운영체제와 상호작용하는 Zig 함수는 일부 예외를 빼면 IO 인자를 요구합니다. 이렇게 하면 다음과 같은 장점이 있습니다.
- 새 운영체제를 지원하는 작업이 대부분 IO 구현을 옮기는 일로 정리됩니다.
- 테스트용 IO를 주입해 입력 스트림을 바꾸거나 쓰기 실패를 일부러 일으킬 수 있습니다.
- 할당자와 마찬가지로, 어떤 코드가 외부 상태와 상호작용하는지 쉽게 알 수 있습니다.
IO를 도입하면서 생긴 호환성 파손은 감수할 만해 보인다고 합니다. 다만 필자는 0.15.0에서 0.16.0으로 직접 전환하는 과정은 겪지 않았다고 밝힙니다. 비슷한 기능을 여럿 추가하는 대신 강력한 기능 하나로 여러 사용 사례를 해결하는 설계를 Zig의 매력으로 꼽습니다.
comptime과 빌드 시스템
- comptime은 메타프로그래밍과 사전 계산을 한 가지 자연스러운 방식으로 처리합니다.
- Zig 빌드 시스템은 다른 도구와 통합하기 좋고, 코드 생성이 필요한 프로젝트에서 특히 쓸모가 있었습니다. Cargo에서는 제약이 있던 도구 통합도 매끄럽게 할 수 있었다고 합니다.
- Uber처럼 다른 언어로 작성한 소프트웨어에 Zig 빌드 시스템을 쓰는 기업도 있습니다.
의존성을 줄이는 문화와 간결한 오류 처리
의존성이 적다는 것
Zig 생태계에는 불필요한 의존성을 넣지 않는 문화가 있습니다.
- 의존성이 필요하면 검증된 C 라이브러리를
translate-c로 가져올 수 있고, 외부 코드를 프로젝트 안에 넣어 직접 감사하기도 쉽습니다. - 필자가 만든 Zig 프로그램은 대부분 직접 의존성 1~2개, 간접 의존성 0개였습니다. 반면 작은 Rust 프로젝트에서도 직접 의존성 몇 개에 간접 의존성 수십 개가 흔했습니다.
- 리플렉션, 난수 생성, 오류 처리, JSON 파싱이 언어나 표준 라이브러리에 들어 있어서
serde나syn같은 라이브러리를 매번 쓸 필요가 줄어듭니다.
오류 처리
Zig에서는 try와 반환 타입의 !만으로 오류를 전파할 수 있습니다. 가장 간단하게 짠 코드가 곧 제대로 된 오류 처리가 되는 셈입니다.
Rust에서는 프로토타입 단계에서 넣은 패닉이 운영 코드에 그대로 남는 일을 겪었다고 합니다. ?는 unwrap보다 짧지만, 범용 오류를 반환하려면 Result<Type, Box<dyn std::error::Error>> 같은 타입을 써야 합니다. anyhow::Result를 쓰면 편해지지만 그만큼 의존성이 늘어납니다. Zig처럼 조합 가능한 오류를 별도 라이브러리로 만드는 방법도 있습니다.
증분 컴파일: 저장하자마자 결과 보기
zig build --watch -fincremental은 파일 변경을 감시하다가 링크 단계까지 바뀐 부분만 다시 빌드합니다.
- Zig는 처음부터 이런 동작을 염두에 두고 설계됐습니다.
- 큰 코드베이스에서도 사람이 지연을 거의 느끼지 못할 만큼 빠른 피드백을 목표로 합니다. 필자는 편집기 옆에 띄워 두고 저장할 때마다 바로 결과를 확인했다고 합니다.
- 예전 Rust 프로젝트에서는 작은 프로젝트도 증분 컴파일에 10초 넘게 걸리는 일이 흔했습니다. 다른 일을 하기엔 짧고 집중이 흐트러지기엔 충분한 시간인데, Zig에서는 이 공백을 줄일 수 있었습니다.
하위 호환성을 지켜야 하는 Rust와 달리, Zig 유지관리자들은 언어를 개선하기 위해 큰 호환성 파손도 감수합니다.
⚠️ 현재 증분 컴파일은 Linux와 macOS의 x86_64에서만 지원됩니다. 다른 OS나 아키텍처에서는 아직 쓸 수 없습니다.
아쉬운 점 1: 표준 라이브러리 안정성과 수명 검사
자주 바뀌는 표준 라이브러리
- 표준 라이브러리가 자주 바뀌어서 생태계 전반이 불편을 겪습니다. 안정화는 1.0 전에 남은 마지막 과제 중 하나입니다.
- 필자는 0.17.0 출시가 오래 걸린 이유도 생태계가 반복해서 깨지는 문제와 관련 있다고 이해하고 있습니다.
- Andrew Kelley는 Zig Day에서 누구보다 1.0을 원하는 사람은 자신이라고 말했습니다.
- 필자는 안정적인 언어와 생태계가 꼭 필요한 프로젝트에는 여전히 Rust를 씁니다. 하지만 다른 프로젝트에서는 Zig의 장점이 불안정성을 상쇄하고, Zig로 만들어지는 소프트웨어들을 보면 안정성이 전부는 아니라고 말합니다.
수명(lifetime) 검사
컴파일러가 지역 변수의 수명 오류를 더 잘 잡아 주면 좋겠다는 바람도 있습니다.
- 예를 들어 함수의 지역 메모리를 반환하는 실수를 디버그 모드의 내장 린트로 잡거나, 함수가 반환된 뒤 그 메모리를 무효화하는 방식을 원합니다.
- 지금 컴파일러는 일부 변수를 블록보다 넓은 범위까지 살려 두기 때문에, 블록 안의 스택 버퍼
buf로 만든buf[0..n]슬라이스를 블록 밖으로 반환해도 현재는 동작합니다. - 필자는 이런 코드가 미래 버전에서 아무 경고 없이 깨질 수 있다고 이해하고 있습니다. 수명을 잘 안다고 생각해도 실수하기 쉽다는 얘기입니다.
아쉬운 점 2: 문서 부족과 커뮤니티의 역할
- Zig 문서는 입문에는 충분합니다. 하지만 그다음 단계부터는 문서 주석이 드물고 예제가 부족해서 의도를 추측해야 하는 경우가 많습니다.
- 실제로 snurl의 표준 라이브러리 TLS 통합은 두 사람이 버퍼 설정 방법을 알아내지 못해 아직 작동하지 않습니다.
- 실용적인 지식이 웹 여기저기 흩어져 있어서, 문서·발표·소스 코드·커뮤니티에 익숙한 사람이 유리합니다. 커뮤니티와 연결이 약하면 효과적으로 작업하기 어렵습니다.
필자는 문서 부족이 커뮤니티 참여를 이끄는 면도 있지만, 사람 사이의 협력과 좋은 문서는 함께 갖출 수 있다고 강조합니다. LLM이 인간 협업을 약화시킬 수 있다는 우려가 나오는 지금이야말로 사람끼리의 협력이 더 필요하다는 것입니다. 문서가 좋아도 질문은 계속 나오기 마련이고, 좋은 커뮤니티는 'RTFM'으로 끝내지 않고 알맞은 자료로 안내해 준다고 말합니다.
덧붙여 필자는 **문학적 프로그래밍(literate programming)**과 Zig를 결합하는 데도 관심이 있습니다. 관련 도구는 지금 Rust로 작성돼 있지만 언어를 바꿀 수도 있다고 합니다.
아쉬운 점 3: 컴파일 타임 할당자의 부재
한 프로젝트에서는 바이너리에 정적 JSON 파일을 넣고, 이를 컴파일 타임에 구조체 집합으로 파싱하고 싶었습니다.
- 그런데 표준 라이브러리 JSON 파서는 할당자를 요구하고, 지금은 컴파일 타임에 작동하는 할당자가 없어서 결국 런타임에 파싱했습니다.
- 할당자가 필요 없는 JSON 파서를 추가하려는 움직임이 있는 것으로 알려져 있고, 데이터가 이미 JSON 문자열 안에 있으니 이런 파서라면 당장의 문제는 풀립니다.
- 다만 더 일반적인 경우에는 여전히 컴파일 타임 할당자가 필요할 수 있습니다. 필자는 예전에
FixedBufferAllocator가 컴파일 타임에 작동했던 것으로 기억하며 다시 지원되기를 바랍니다.
아쉬운 점 4: 부트스트랩과 배포판 패키징
Zig의 부트스트랩 방식
- Zig는 C++ 컴파일러 대신 작은 WASM 블롭과 WASM 인터프리터를 씁니다.
- 축소된 WASM 컴파일러가 전체 Zig 컴파일러를 C로 변환합니다.
- 시스템 C 컴파일러가 그 C 코드를 컴파일합니다.
Rust처럼 이전 버전 컴파일러 전체가 필요한 방식보다는 낫습니다. 하지만 소스 저장소에 미리 컴파일된 바이너리 블롭이 있어야 하므로 모든 단계를 소스에서부터 쌓을 수는 없고, 블롭에 악성 코드가 들어갈 가능성도 남습니다.
실제로 생기는 문제
- Debian 등 여러 배포판은 자신들의 패키징 지침에 맞게 Zig를 패키징하지 못하고 있습니다.
- 원래는 C 백엔드로 생성한 C 버전 컴파일러를 따로 유지해 소스 구현을 두 개 갖추려 했지만, 관리가 너무 어려워 포기했습니다.
- 완전한 소스 기반 부트스트랩을 하려면 다른 언어(아마도 C)로 작성한 대체 Zig 컴파일러가 필요합니다. 하지만 Zig Software Foundation은 지금 이를 직접 만들 생각이 없고, 빠르게 바뀌는 언어를 따라가야 하니 유지 부담도 큽니다.
- 실제로 필자는 한 프로젝트를 더 널리 쓰이게 하려고 Debian 패키징 문제 때문에 다른 언어를 선택하기도 했습니다.
Zig는 누구나 대체 구현을 만들 수 있을 만큼 단순한 언어를 지향하므로, 안정화 뒤에는 대체 컴파일러를 만들기가 수월해질 것으로 기대됩니다. 다만 이 문제도 1.0까지 기다려야 할 수 있습니다.
마치며: 그래도 Zig를 고르는 이유
여러 단점에도 필자는 지금까지 써 본 언어 중 Zig를 가장 좋아하고, 앞으로도 대부분의 프로젝트에서 Zig를 먼저 고르겠다고 말합니다.
정리하면 이렇습니다.
- 이런 분께 추천합니다: 메모리 할당과 IO를 명확하게 통제하고 싶은 분, 의존성이 적은 코드베이스를 원하는 분, 빠른 빌드 피드백을 중요하게 여기는 분
- 이런 경우엔 신중하세요: 장기적인 API 안정성이 필수인 프로젝트, 배포판 공식 패키징이 중요한 프로젝트, x86_64 외 환경에서 증분 컴파일이 꼭 필요한 경우
Rust를 쓰다가 Zig가 궁금해진 분이라면 이 회고가 좋은 출발점이 될 것입니다. 1.0을 향해 가는 Zig가 앞으로 어떤 모습이 될지 지켜볼 만합니다.