Zig 언어 입문 — C의 대안이 될 수 있을까
Zig의 comptime, 메모리 안전성, C 호환성과 Bun/TigerBeetle 사례 분석.
한 줄 요약: Zig는 C의 복잡성(매크로, 암묵적 동작, 숨겨진 제어 흐름)을 제거하고 comptime, 명시적 메모리 관리, C 직접 호환으로 시스템 프로그래밍을 단순하게 만드는 언어다. Bun과 TigerBeetle이 프로덕션에서 증명한 실용성이 있지만, 아직 표준 라이브러리가 불안정하고 생태계가 작다.
- C/C++ 코드베이스를 유지보수하면서 대안 언어를 탐색 중인 시스템 프로그래머
- Rust의 borrow checker 학습 곡선 없이 메모리 안전 코드를 작성하고 싶은 경우
- WebAssembly 타겟이나 임베디드 환경에서 최소 런타임이 필요한 경우
- Bun, TigerBeetle 등 Zig 기반 프로젝트의 내부를 이해하고 싶은 경우
※ Zig 0.13.0 기준. 공식 사이트: ziglang.org · 공식 문서: ziglang.org/documentation
Zig의 핵심 특징 — C와 무엇이 다른가
Zig는 Andrew Kelley가 2015년 시작한 시스템 프로그래밍 언어다. Rust처럼 메모리 안전성을 목표로 하지만 접근 방식이 다르다. Rust는 borrow checker로 컴파일 타임에 메모리 오류를 막는다. Zig는 borrow checker 없이, 프로그래머가 명시적으로 모든 메모리 할당을 제어하고 에러를 처리하도록 강제한다.
C 대비 Zig의 주요 차이:
- 매크로 없음: 전처리기 매크로 대신
comptime으로 컴파일 타임 코드 실행 - 숨겨진 제어 흐름 없음: 연산자 오버로딩, 예외, 암묵적 형변환 없음
- 명시적 에러 처리:
try와 에러 유니온 타입으로 에러를 무시할 수 없음 - Allocator 명시: 모든 메모리 할당 함수가 allocator를 인자로 받음 — 전역 할당자 없음
- undefined 값: 초기화되지 않은 변수를 명시적으로
undefined로 표시 - C 직접 호환: C 헤더를 @cImport로 직접 사용, FFI 레이어 없음
Zig 기본 문법 — Hello World와 기본 구조// hello.zig const std = @import("std"); pub fn main() !void { // !void = 에러가 발생할 수 있는 void 반환 타입 const stdout = std.io.getStdOut().writer(); try stdout.print("Hello, {s}!\n", .{"Zig"}); // try = 에러 발생 시 즉시 반환 (에러 무시 불가) } // 빌드 및 실행 // zig build-exe hello.zig // zig run hello.zig // 기본 변수와 타입 const x: i32 = 42; // 상수 (불변) var y: f64 = 3.14; // 변수 (가변) const name: []const u8 = "Zig"; // 문자열 슬라이스 // 배열과 슬라이스 const arr = [_]i32{ 1, 2, 3, 4, 5 }; // 크기 자동 추론 const slice: []const i32 = arr[1..4]; // [2, 3, 4] // 구조체 const Point = struct { x: f32, y: f32, pub fn distance(self: Point, other: Point) f32 { const dx = self.x - other.x; const dy = self.y - other.y; return @sqrt(dx * dx + dy * dy); } }; const p1 = Point{ .x = 0, .y = 0 }; const p2 = Point{ .x = 3, .y = 4 }; const dist = p1.distance(p2); // 5.0
comptime — 매크로 없이 컴파일 타임 프로그래밍
comptime은 Zig의 가장 독특한 기능이다. C의 매크로, C++의 템플릿, 또는 별도의 메타프로그래밍 언어 없이 일반 Zig 코드를 컴파일 타임에 실행할 수 있다. 제네릭, 조건부 컴파일, 타입 계산이 모두 comptime 하나로 처리된다.
comptime이 강력한 이유는 타입 자체가 comptime 값이라는 점이다. 함수가 타입을 인자로 받고 새 타입을 반환할 수 있다. 이것이 Zig의 제네릭 구현 방식이다.
comptime — 제네릭과 컴파일 타임 계산const std = @import("std"); // comptime 제네릭 함수 — 타입을 인자로 받음 fn max(comptime T: type, a: T, b: T) T { return if (a > b) a else b; } // 사용 — 런타임 오버헤드 없음, 각 타입별 코드 생성 const m1 = max(i32, 10, 20); // 20 const m2 = max(f64, 3.14, 2.7); // 3.14 // comptime 스택 — 컴파일 타임에 크기가 결정되는 배열 fn makeArray(comptime size: usize) [size]i32 { var result: [size]i32 = undefined; for (&result, 0..) |*item, i| { item.* = @intCast(i * i); } return result; } const squares = makeArray(5); // [0, 1, 4, 9, 16] — 컴파일 타임 계산 // comptime 조건부 컴파일 (C의 #ifdef 대체) const builtin = @import("builtin"); fn platformSpecific() void { if (comptime builtin.target.os.tag == .linux) { // Linux 전용 코드 } else if (comptime builtin.target.os.tag == .macos) { // macOS 전용 코드 } // 해당 없는 분기는 컴파일에서 완전히 제거됨 }
메모리 안전성 — Zig의 접근 방식
Zig는 Rust와 달리 borrow checker가 없다. 대신 다음 메커니즘으로 메모리 안전성을 높인다.
1. 명시적 Allocator: 모든 힙 할당은 allocator를 통해 이뤄진다. 함수가 메모리를 할당할 경우 반드시 allocator를 인자로 받아야 한다. 이를 통해 어디서 메모리가 할당되는지 코드 읽기만으로 파악 가능하다.
2. 에러 유니온과 강제 처리: 에러를 반환하는 함수는 반환 타입이 !T다. 이 값을 그냥 쓰면 컴파일 오류가 발생한다. try로 전파하거나 catch로 처리해야 한다.
3. 안전 모드 런타임 검사: Debug/ReleaseSafe 빌드에서 배열 범위 초과, 정수 오버플로우, null 역참조를 런타임에 탐지하고 명확한 패닉 메시지를 출력한다. ReleaseFast에서는 성능을 위해 검사를 제거한다.
4. 옵셔널 타입: null이 될 수 있는 값은 ?T로 명시한다. null 역참조를 컴파일 타임에 방지한다.
Zig 메모리 관리 — Allocator와 에러 처리const std = @import("std"); // Allocator를 인자로 받는 함수 패턴 fn readFileContents(allocator: std.mem.Allocator, path: []const u8) ![]u8 { const file = try std.fs.cwd().openFile(path, .{}); defer file.close(); // defer: 스코프 종료 시 실행 보장 const size = try file.getEndPos(); const buffer = try allocator.alloc(u8, size); errdefer allocator.free(buffer); // 에러 시에만 해제 _ = try file.readAll(buffer); return buffer; // 호출자가 해제 책임 } pub fn main() !void { // GeneralPurposeAllocator — 디버그 빌드에서 메모리 누수 탐지 var gpa = std.heap.GeneralPurposeAllocator(.{}){}; defer _ = gpa.deinit(); // 종료 시 누수 검사 const allocator = gpa.allocator(); const contents = try readFileContents(allocator, "README.md"); defer allocator.free(contents); // 명시적 해제 std.debug.print("{s}\n", .{contents}); } // 옵셔널 타입 — null 안전성 fn findUser(id: u32) ?User { if (id == 0) return null; return User{ .id = id, .name = "Alice" }; } const user = findUser(1) orelse return error.UserNotFound; // orelse로 null 케이스 명시적 처리
C 호환성 — FFI 없이 C 라이브러리 사용
Zig의 C 호환성은 단순한 FFI(외부 함수 인터페이스)를 넘어선다. @cImport로 C 헤더를 직접 import하면 별도 바인딩 코드 없이 C 함수를 호출할 수 있다. Zig 컴파일러 자체에 C 컴파일러(Clang)가 내장돼 있어 C 파일을 직접 컴파일하고 링크한다.
Zig를 C 프로젝트의 빌드 시스템으로 사용: zig cc는 clang 호환 C 컴파일러로 동작한다. 기존 Makefile이나 CMake를 Zig 빌드 시스템으로 점진적으로 전환할 수 있다. 크로스 컴파일이 기본 지원이라 타겟별 툴체인 설정이 불필요하다.
C 헤더 직접 사용 및 Zig를 C 컴파일러로 활용// C 라이브러리 직접 사용 — bindings 불필요 const c = @cImport({ @cInclude("stdio.h"); @cInclude("sqlite3.h"); }); pub fn main() void { _ = c.printf("Hello from C printf!\n"); var db: ?*c.sqlite3 = null; const rc = c.sqlite3_open(":memory:", &db); if (rc != c.SQLITE_OK) { c.sqlite3_close(db); return; } defer _ = c.sqlite3_close(db); // C API를 그대로 Zig에서 호출 } // zig cc — 기존 C 프로젝트 크로스 컴파일 // $ zig cc -target x86_64-linux-musl main.c -o main-linux // $ zig cc -target aarch64-macos main.c -o main-macos // $ zig cc -target wasm32-freestanding main.c -o main.wasm // build.zig — C 소스 포함 빌드 const std = @import("std"); pub fn build(b: *std.Build) void { const exe = b.addExecutable(.{ .name = "myapp", .root_source_file = b.path("src/main.zig"), .target = b.standardTargetOptions(.{}), }); // C 소스 직접 포함 exe.addCSourceFile(.{ .file = b.path("vendor/lib.c") }); exe.linkLibC(); b.installArtifact(exe); }
실제 사용 사례 — Bun과 TigerBeetle
Bun — JavaScript 런타임: Bun은 Node.js 대체를 목표로 Zig로 작성됐다. HTTP 서버, 파일 I/O, 번들러 핵심 부분이 Zig다. JavaScriptCore(WebKit) 엔진을 C API로 직접 연동한 것도 Zig의 C 호환성 덕분에 가능했다. Bun이 Node.js 대비 I/O 성능이 높은 이유 중 하나는 Zig의 낮은 오버헤드다.
TigerBeetle — 금융 데이터베이스: TigerBeetle은 계좌 원장(ledger) 전용 데이터베이스로, 전체가 Zig로 작성됐다. 초당 수백만 건의 금융 트랜잭션을 처리하며, 메모리 할당을 완전히 제어해 지연 스파이크(latency spike)를 최소화한다. GC가 없고 모든 메모리를 사전 할당하는 설계가 Zig의 명시적 allocator 시스템으로 자연스럽게 구현됐다.
그 외 주목할 프로젝트:
- Mach: Zig 기반 게임 엔진 / 그래픽스 프레임워크
- Ghostty: Mitchell Hashimoto가 Zig로 작성한 고성능 터미널 에뮬레이터
- zig-sqlite: SQLite Zig 바인딩 — C 헤더 직접 활용 예시
현재 상태 — 도입 전 알아야 할 것
Zig는 아직 1.0 릴리스 전이다(2026년 3월 기준 최신 안정 버전 0.13.0). 이는 실무 도입 시 중요한 고려 사항이다.
현재 한계:
- 표준 라이브러리 불안정: 버전 간 API가 깨질 수 있다. 0.12 → 0.13에서도 다수의 breaking change 발생.
- 작은 에코시스템: npm, crates.io와 같은 큰 패키지 생태계가 없다. 서드파티 라이브러리 수가 적어 직접 구현하는 경우가 많다.
- IDE 지원: ZLS(Zig Language Server)가 있지만 Rust Analyzer나 TypeScript LSP 수준에는 미치지 못한다.
- 문서: 공식 문서가 완전하지 않으며, 커뮤니티 예시로 학습하는 비중이 크다.
도입이 합리적인 케이스: 시스템 레벨 도구, CLI 도구, 웹어셈블리 모듈, 기존 C 라이브러리를 Zig로 래핑하는 경우. 애플리케이션 레이어(웹 서버, 비즈니스 로직)는 아직 Go나 Rust가 더 안정적인 선택이다.