Zig 완벽 가이드 — C의 진정한 후계자를 노리는 시스템 프로그래밍 언어
이 글의 핵심
Zig는 "C의 단순성·성능·이식성을 유지하면서 현대적 결함을 고친다"는 목표로 Andrew Kelley가 2016년 시작한 시스템 프로그래밍 언어입니다. 숨겨진 제어 흐름 없음, 명시적 메모리 할당자, comptime 기반 제네릭, 최고 수준의 크로스컴파일 지원이 특징이며 Bun 런타임·TigerBeetle 데이터베이스·Uber 등이 프로덕션에서 채택했습니다. 이 글은 Zig의 핵심 개념·실전 예제·Rust와의 비교·현 시점의 채택 기준을 정리합니다.
Zig의 설계 철학
Andrew Kelley가 정리한 Zig의 5대 원칙:
- Robust: 버그를 재현 가능하게 (UB 최소화·디버그 검증)
- Optimal: 최고 성능을 얻을 수 있어야
- Reusable: 한 번 쓴 코드를 어디서나
- Maintainable: 쉽게 읽고 수정
- “No hidden control flow”: 숨겨진 함수 호출·할당·예외 없음
숨겨진 제어 흐름 없음이 Zig의 가장 독특한 특징입니다.
- 연산자 오버로딩 없음 (
a + b는 진짜 덧셈) - 예외(exceptions) 없음 → 오류는 값으로
- 숨은 heap allocation 없음 →
allocator를 인자로 명시 - 생성자/소멸자 없음 (defer로 정리)
설치
# macOS
brew install zig
# Linux (공식 바이너리)
wget https://ziglang.org/download/0.14.0/zig-linux-x86_64-0.14.0.tar.xz
tar -xf zig-linux-x86_64-0.14.0.tar.xz
export PATH=$PWD/zig-linux-x86_64-0.14.0:$PATH
# Windows
winget install zig.zig
zig version
Hello, World
const std = @import("std");
pub fn main() !void {
const stdout = std.io.getStdOut().writer();
try stdout.print("Hello, {s}!\n", .{"Zig"});
}
!void= 오류를 반환할 수 있는 voidtry는 오류를 상위로 전파 (Rust의?와 유사).{"Zig"}는 익명 구조체 — 포맷 인자 튜플
컴파일·실행:
zig run hello.zig
zig build-exe hello.zig -O ReleaseFast
기본 문법
변수
const x: i32 = 10; // 불변
var y: u64 = 100; // 가변
y += 1;
const arr = [_]u8{ 1, 2, 3 }; // 길이 추론
const slice: []const u8 = "hello";
제어 흐름
const n: i32 = 5;
if (n > 0) {
std.debug.print("positive\n", .{});
} else {
std.debug.print("non-positive\n", .{});
}
for (arr) |item, i| {
std.debug.print("{d}: {d}\n", .{ i, item });
}
var i: usize = 0;
while (i < 10) : (i += 1) {
// ...
}
const msg = switch (n) {
0 => "zero",
1...9 => "single digit",
else => "large",
};
Optional·Error Union
// null 가능한 타입
var maybe: ?i32 = null;
maybe = 42;
if (maybe) |v| {
std.debug.print("value: {d}\n", .{v});
} else {
std.debug.print("nothing\n", .{});
}
// 오류 유니언
const ParseError = error{ Invalid, TooLong };
fn parseNumber(s: []const u8) ParseError!i32 {
if (s.len > 10) return ParseError.TooLong;
return std.fmt.parseInt(i32, s, 10) catch ParseError.Invalid;
}
pub fn main() !void {
const n = try parseNumber("42");
_ = n;
}
Allocator: 메모리의 명시성
Zig의 가장 독특한 설계. 표준 라이브러리의 어떤 함수도 할당자 없이 힙을 쓰지 않습니다.
const std = @import("std");
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
var list = std.ArrayList(i32).init(allocator);
defer list.deinit();
try list.append(1);
try list.append(2);
try list.append(3);
std.debug.print("{any}\n", .{list.items});
}
GeneralPurposeAllocator: 누수·UAF 탐지 기능이 있는 범용 할당자FixedBufferAllocator: 스택 또는 정적 버퍼 위에서 할당ArenaAllocator: 라이프타임 동안만 살아있고 한 번에 해제page_allocator: 시스템 페이지 단위
사용자가 어디서 메모리가 할당되는지 항상 아는 것이 Zig의 원칙입니다. 라이브러리가 “마음대로 heap을 쓰는” 일이 없습니다.
defer
fn writeFile(allocator: std.mem.Allocator, path: []const u8, data: []const u8) !void {
const file = try std.fs.cwd().createFile(path, .{});
defer file.close();
try file.writeAll(data);
const backup = try allocator.alloc(u8, data.len);
defer allocator.free(backup);
@memcpy(backup, data);
}
defer는 스코프 종료 시 LIFO로 실행되어 소멸자 없이도 안전한 정리가 가능합니다.
comptime: 컴파일 타임 실행
Zig의 제네릭·메타프로그래밍은 comptime으로 이뤄집니다.
fn max(comptime T: type, a: T, b: T) T {
return if (a > b) a else b;
}
const big = max(i32, 10, 20);
const bigF = max(f64, 1.5, 2.5);
// 컴파일 타임 반복
fn tupleToArray(comptime tuple: anytype) [tuple.len]@TypeOf(tuple[0]) {
var result: [tuple.len]@TypeOf(tuple[0]) = undefined;
inline for (tuple, 0..) |item, i| {
result[i] = item;
}
return result;
}
컴파일 타임에 타입을 생성·검사하고, 런타임에는 완전히 특수화된 코드만 남습니다. C++ 템플릿·Rust 제네릭의 장점을 더 직관적인 문법으로 제공.
구조체와 메소드
const Point = struct {
x: f64,
y: f64,
pub fn init(x: f64, y: f64) Point {
return .{ .x = x, .y = y };
}
pub fn distance(self: Point, other: Point) f64 {
const dx = self.x - other.x;
const dy = self.y - other.y;
return std.math.sqrt(dx * dx + dy * dy);
}
};
const p1 = Point.init(0, 0);
const p2 = Point.init(3, 4);
std.debug.print("{d}\n", .{p1.distance(p2)}); // 5
- 상속 없음 → 합성 선호
- 인터페이스는
fn(ptr: *anyopaque, ...)스타일의 vtable로 표현 가능
테스트
test "addition" {
try std.testing.expectEqual(@as(i32, 3), 1 + 2);
}
test "allocator no leak" {
const allocator = std.testing.allocator;
const buf = try allocator.alloc(u8, 10);
defer allocator.free(buf);
@memset(buf, 0);
}
zig test src/main.zig
std.testing.allocator는 누수가 있으면 테스트 실패시키는 특수 할당자.
빌드 시스템 (build.zig)
const std = @import("std");
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const exe = b.addExecutable(.{
.name = "myapp",
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
});
// C 라이브러리 링크
exe.linkLibC();
exe.linkSystemLibrary("sqlite3");
b.installArtifact(exe);
const run_cmd = b.addRunArtifact(exe);
const run_step = b.step("run", "Run the app");
run_step.dependOn(&run_cmd.step);
const unit_tests = b.addTest(.{
.root_source_file = b.path("src/main.zig"),
});
const test_step = b.step("test", "Run tests");
test_step.dependOn(&b.addRunArtifact(unit_tests).step);
}
zig build # 기본
zig build -Doptimize=ReleaseFast
zig build run
zig build test
CMake·Meson·Makefile 같은 별도 도구 없이 Zig 코드로 빌드 그래프를 선언합니다.
크로스컴파일 (Zig의 슈퍼파워)
# macOS에서 Windows x64 바이너리 빌드
zig build-exe main.zig -target x86_64-windows-gnu
# Linux arm64
zig build-exe main.zig -target aarch64-linux-gnu
# macOS arm64
zig build-exe main.zig -target aarch64-macos
# WASI
zig build-exe main.zig -target wasm32-wasi
단일 호스트에서 모든 OS/아키텍처 바이너리를 만들 수 있습니다. 이 기능만으로도 많은 프로젝트가 Zig를 빌드 도구로 채택합니다.
zig cc — C 컴파일러로서의 Zig
# Zig가 Clang을 감싼 C 컴파일러로 동작
zig cc -O2 -o hello hello.c
# 크로스컴파일도 지원
zig cc -target aarch64-linux-musl -o hello_arm hello.c
Go 빌드 시스템에서 CC="zig cc"로 쓰면 크로스컴파일이 간소화되는 사례가 유명합니다(Uber의 Go 빌드 파이프라인이 대표적).
C 인터옵
Zig에서 C 헤더 import
const c = @cImport({
@cInclude("stdio.h");
@cInclude("math.h");
});
pub fn main() !void {
_ = c.printf("sqrt(2) = %f\n", c.sqrt(2.0));
}
C 함수·타입·매크로를 자연스럽게 사용. 바인딩 생성 단계 없음.
C에서 Zig 호출
export fn add(a: c_int, b: c_int) c_int {
return a + b;
}
zig build-lib add.zig -dynamic
생성된 .so/.dll/.dylib를 C 프로그램이 링크.
실전 예제: 간단한 HTTP 서버
const std = @import("std");
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
const addr = try std.net.Address.parseIp("0.0.0.0", 8080);
var server = try addr.listen(.{ .reuse_address = true });
defer server.deinit();
std.debug.print("listening on :8080\n", .{});
while (true) {
const conn = try server.accept();
handleConn(allocator, conn) catch |err| {
std.debug.print("error: {s}\n", .{@errorName(err)});
};
}
}
fn handleConn(allocator: std.mem.Allocator, conn: std.net.Server.Connection) !void {
defer conn.stream.close();
var buf: [4096]u8 = undefined;
const n = try conn.stream.read(&buf);
_ = n;
_ = allocator;
const response =
"HTTP/1.1 200 OK\r\n" ++
"Content-Type: text/plain\r\n" ++
"Content-Length: 13\r\n\r\n" ++
"Hello, Zig!\r\n";
try conn.stream.writeAll(response);
}
사용 사례
| 프로젝트 | 사용처 |
|---|---|
| Bun | JavaScript 런타임 전체 |
| TigerBeetle | 금융 트랜잭션 DB (100% Zig) |
| Uber | Go 크로스 빌드 툴체인 |
| RoadRunner | C 라이브러리 빌드 파이프라인 |
| 게임 엔진 | 수 개의 상용 게임 |
| 임베디드 | 베어메탈·RTOS 타깃 |
언제 Zig를 선택할지
- 단일 바이너리 + 크로스컴파일이 핵심 요구사항
- C 라이브러리와 깊이 통합하는 시스템 프로그래밍
- C의 단순성은 좋지만 안전성을 조금 더 원할 때
- 컴파일 타임 메타프로그래밍을 C++ 템플릿보다 깔끔하게
- 언어 변경을 감당할 수 있는 팀 (아직 1.0 전)
언제 Zig를 피할지
- 정적 메모리 안전이 필수 → Rust
- 방대한 C++ 라이브러리 생태계 필요 → C++
- 생산성 중심 앱 → Go, Rust, TypeScript
- JVM·에코시스템 의존 → Kotlin, Java
- 장기 안정적 ABI 필요 → 아직 Zig는 1.0 전
Rust와의 비교 요약
| 관점 | Zig | Rust |
|---|---|---|
| 메모리 안전 | 런타임 + 명시적 allocator | 컴파일 타임 borrow checker |
| 학습 곡선 | 중간 | 높음 |
| 컴파일 속도 | 빠름 | 보통 |
| C 인터옵 | 최고 수준(헤더 직접 import) | bindgen 필요 |
| 생태계 | 초기 | 성숙 |
| 크로스컴파일 | 최고 | 좋음 |
| 안정성 | pre-1.0 | 안정 |
| 표현력 | 의도적으로 작음 | 풍부함 |
트러블슈팅
error: expected type 'u32', found 'comptime_int'
Zig는 매우 엄격한 타입 검사. @as(u32, 10)·@intCast로 명시 캐스팅.
메모리 누수
GeneralPurposeAllocator의 deinit() 반환값이 .leak 이면 누수. defer _ = gpa.deinit()으로 체크.
빌드 속도 저하
-O Debug 기본은 빠르지만 최적화는 느림. 반복 개발에서는 -Doptimize=Debug 유지.
업그레이드 깨짐
Zig는 pre-1.0이라 마이너 릴리스가 코드 변경을 요구할 수 있음. 릴리스 노트·zig fmt·커뮤니티의 upgrade script 참고.
라이브러리 부재
표준 라이브러리 외에는 아직 제한적. zig fetch로 크레이트처럼 dependency 설치 가능하지만 생태계 크기는 Rust·Go 대비 작음.
체크리스트
- Zig 안정 버전 고정 (0.14.x 등)
-
GeneralPurposeAllocator로 기본 개발, 누수 체크 -
zig fmtCI 통과 -
build.zig로 명확한 빌드 그래프 - 크로스컴파일 타깃 자동화
-
std.testing.allocator로 leak-safe 테스트 - C 라이브러리 필요 시
@cImport로 직접 바인딩 - 언어/표준 라이브러리 변경에 대응하는 팀 규약
마무리
Zig는 “C의 영혼을 유지한 채 단점을 고친다”는 야심 찬 목표를 꾸준히 실현해나가는 언어입니다. 정적 메모리 안전에서는 Rust에 못 미치지만, 명시성·단순성·크로스컴파일·comptime이라는 독자적 강점으로 고유한 자리를 확보했습니다. Bun·TigerBeetle 같은 성공적 프로덕션 사례가 Zig의 성능·실용성을 증명하고 있고, zig cc는 이미 많은 C/C++ 프로젝트의 빌드 인프라를 단순화하고 있습니다. 2026년 현재 1.0 전 단계이므로 신중한 선택이 필요하지만, 시스템 프로그래밍에 관심 있는 개발자라면 반드시 한 번은 다뤄볼 가치가 있는 언어입니다.
관련 글
- Rust 완벽 가이드
- C/C++ 완벽 가이드
- Bun 완벽 가이드
- 시스템 프로그래밍 언어 비교
자주 묻는 질문 (FAQ)
Q. 이 내용을 실무에서 언제 쓰나요?
A. Zig 언어 완벽 가이드. 숨겨진 제어 흐름 없음·comptime·allocator 명시성·크로스컴파일 일급 지원으로 C/C++의 단점을 해결하는 시스템 언어. Bun/TigerBeetle이 선택한 이유, Rust … 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.
Q. 선행으로 읽으면 좋은 글은?
A. 각 글 하단의 이전 글 또는 관련 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.
Q. 더 깊이 공부하려면?
A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.
이 글에서 다루는 키워드 (관련 검색어)
Zig, Systems Programming, C, C++, Rust, comptime, Cross Compilation 등으로 검색하시면 이 글이 도움이 됩니다.