본문으로 바로가기
COMMUNITY모든 이야기

Zig는 무슨 언어일까? C와 Rust 사이에서 자꾸 이름이 보이는 이유

빗방울 2026.11.01 14:55 8분 읽기 조회 117 · 추천 2

프로그래밍 언어 소개를 보다 보면 자꾸 ‘C를 대체할 언어’가 나온다. Rust도 자주 그 자리에 등장하고, 오늘 이야기할 Zig도 있다.

그런데 둘은 문제를 푸는 방향이 꽤 다르다. Zig는 메모리를 자동으로 관리해주는 언어도 아니고, Rust의 소유권 검사를 간단히 만든 언어도 아니다.

오히려 ‘컴퓨터에서 무슨 일이 일어나는지 코드에 잘 보이게 하자’는 쪽으로 접근하면 이해하기 쉽다.

Zig는 어디에 쓰는 언어인가?

Zig는 범용 프로그래밍 언어이자 도구 모음이다. 네이티브 프로그램, 시스템 소프트웨어, 라이브러리, 임베디드 같은 저수준 제어가 필요한 영역을 다룰 수 있다.

파이썬처럼 인터프리터에서 한 줄씩 시험하는 감각보다는, 코드를 컴파일해 실행 파일을 만드는 쪽이다. 메모리 배치와 자원 사용을 직접 다루는 일이 중요해진다.

그렇다고 ‘C보다 무조건 빠르다’거나 ‘Rust보다 무조건 쉽다’고 단정할 수는 없다. 언어의 방향과 실제 프로그램의 성능은 따로 봐야 한다.

일단 코드는 이렇게 생겼다

const std = @import("std");

pub fn main() void {
    std.debug.print("Hello, Zig!\n", .{});
}

공식 개요에 나오는 기본 출력 예제와 같은 형태다. hello.zig로 저장했다면 아래 명령으로 컴파일하고 실행할 수 있다.

zig run hello.zig

const는 재할당하지 않는 선언, fn은 함수 선언, void는 반환값이 없다는 뜻이다. std.debug.print는 디버그 출력 함수라 일반적인 표준출력 예제와는 구분해 보면 좋다.

포인트 1. 메모리를 누가 할당하는지 드러낸다

Zig에는 자동 가비지 컬렉터가 없다. 필요한 메모리의 수명을 설계하고 해제하는 책임이 프로그래머에게 있다.

특히 표준 라이브러리에서 동적 메모리가 필요한 기능은 allocator, 즉 메모리 할당자를 인자로 받는 방식을 사용한다. 호출하는 쪽에서 어떤 할당 전략을 사용할지 선택하기 좋다.

짧은 작업에 필요한 메모리를 모았다가 한 번에 정리하거나, 정해진 버퍼 안에서만 할당하게 만드는 식의 설계와 잘 맞는다.

다만 ‘숨은 할당이 없다’는 철학이 ‘Zig로 작성된 모든 함수는 내부에서 절대 할당하지 않는다’는 뜻은 아니다. 사용자 코드와 외부 라이브러리의 계약도 확인해야 한다.

포인트 2. defer로 뒷정리를 가까이 둔다

자원을 얻은 곳 근처에 정리 코드를 적어두고, 현재 스코프를 빠져나갈 때 실행하게 만드는 기능이 defer다.

파일을 열었다면 닫기, 메모리를 얻었다면 해제하기 같은 처리를 멀리 떨어진 함수 끝까지 미루지 않아도 된다. 오류로 빠져나갈 때만 실행하는 errdefer도 있다.

이 기능이 메모리 관리를 자동으로 해결해주는 것은 아니다. 정리 코드를 언제 실행할지 표현하기 편하게 해주는 도구에 가깝다.

포인트 3. 오류도 값으로 다룬다

Zig에서는 함수가 성공값 또는 오류를 반환하도록 타입에 표현할 수 있다. 오류를 전달할 때는 try, 처리할 때는 catch 같은 문법을 쓴다.

여기서 try는 다른 언어의 try 블록과 똑같이 생각하면 헷갈린다. ‘이 호출이 실패하면 현재 함수에서도 그 오류를 반환한다’는 동작을 간결하게 표현한다.

fn divide(a: u32, b: u32) error{ZeroDivisor}!u32 {
    if (b == 0) return error.ZeroDivisor;
    return a / b;
}

fn example() !u32 {
    const result = try divide(12, 3);
    return result;
}

성공하면 u32 값이 나오고, 0으로 나누려고 하면 ZeroDivisor 오류가 나온다. 위 코드는 문법 설명용 함수 조각이며 단독 실행 프로그램은 아니다.

포인트 4. comptime — 실행 전에 계산하기

Zig의 대표 기능인 comptime은 컴파일 시점 계산이다. 일부 값을 프로그램 실행 전에 정하거나, 타입을 인자로 받아 그 타입에 맞는 코드를 구성할 수 있다.

fn square(comptime T: type, x: T) T {
    return x * x;
}

const answer = square(u32, 12);

여기서는 u32라는 타입을 컴파일 시점에 전달한다. 타입마다 비슷한 함수를 여러 개 복사하는 대신 하나의 함수로 표현할 수 있다.

중요한 점은 comptime 인자가 있다고 함수 안의 모든 계산이 반드시 컴파일 시점에 끝나는 것은 아니라는 것이다. 위의 T는 컴파일 시점에 정해지지만, 다른 호출에서 전달하는 x는 실행 시점 값일 수도 있다.

템플릿이나 매크로가 담당하던 문제 일부를 언어 자체의 컴파일 시점 실행으로 다룬다는 점이 흥미롭다.

C와 같이 쓰는 능력도 중요한 부분이다

Zig는 C 헤더를 가져오거나 C ABI를 통해 라이브러리를 연결하는 기능을 제공한다. 기존 C 코드를 한 번에 갈아엎기보다 일부를 연결해 쓰는 접근이 가능하다.

언어 자체 외에도 zig cc와 zig c++ 같은 도구가 있다. Zig 도구 모음을 C·C++ 컴파일에 활용할 수 있다는 뜻이다.

크로스 컴파일도 주요 특징이다. 다만 다른 운영체제의 모든 라이브러리·SDK 의존성까지 자동으로 해결해준다는 뜻은 아니다. 대상 플랫폼과 외부 의존성에 따른 조건은 여전히 남는다.

그럼 Rust보다 편하고 안전한가?

여기서 둘을 한 줄로 순위 매기면 이야기가 이상해진다.

관점ZigRust
메모리 관리명시적인 수동 관리와 allocator 선택소유권·대여 규칙과 타입 체계를 중심으로 관리
수명 오류 방지일반적으로 프로그래머가 수명 규칙을 지켜야 함안전한 Rust의 규칙 안에서 컴파일러가 많은 오류를 차단
접근 방식할당과 제어 흐름을 드러내고 직접 제어잘못된 사용을 타입·소유권 규칙으로 제한

Zig에도 선택적 타입, 오류 처리, 빌드 모드에 따른 런타임 안전 검사가 있다. 하지만 이것이 해제 후 사용이나 모든 잘못된 포인터 접근을 컴파일 단계에서 막아준다는 뜻은 아니다.

Debug와 ReleaseSafe는 안전 검사를 사용하는 대표 모드다. ReleaseFast·ReleaseSmall은 기본 안전 검사 정책이 다르므로, ‘디버그에서 잡혔으니 릴리스에서도 항상 잡힌다’고 생각하면 안 된다.

처음 배울 때 걸리는 부분도 있다

문법이 상대적으로 작다는 것과 시스템 프로그래밍이 쉽다는 것은 별개다. 포인터·메모리 수명·할당 실패를 이해해야 한다는 사실은 사라지지 않는다.

아직 1.0 이전의 언어라 표준 라이브러리와 빌드 API 변화에도 신경 써야 한다. 검색해서 찾은 예제가 안 돌아가면 우선 작성 당시 Zig 버전부터 확인하는 편이 좋다.

공식 문서도 개발 버전 문서와 특정 릴리스 문서를 구분해서 읽자. 설치된 버전은 아래 명령으로 확인한다.

zig version

이런 사람이 보면 재밌을 듯

C로 작은 도구를 만들다가 오류 처리와 뒷정리가 아쉬웠던 사람, 메모리 할당 방식을 직접 설계하고 싶은 사람, 컴파일 시점에 타입과 코드를 다루는 방식이 궁금한 사람.

반대로 웹 서비스 하나를 빨리 띄우는 것이 목표라면 언어 생태계와 필요한 라이브러리부터 비교하는 편이 낫다. 저수준 제어가 필요하지 않은 작업에서 그 책임까지 일부러 떠안을 필요는 없다.

Zig의 매력은 컴퓨터가 해주는 일을 더 많이 숨겨주는 데 있다기보다, 개발자가 통제할 부분을 더 또렷하게 보여주는 데 있다고 보면 좋겠다.


공식 자료

2026년 11월 공식 자료를 확인해 작성. 코드는 개념 소개용이며 이 글 작성 환경에서 직접 실행 검증하지는 않았다. 파일·입출력·빌드 관련 API는 사용하는 릴리스의 문서를 확인하는 편이 좋다.

모든 이야기1431

가벼운 안부부터 깊은 생각까지, 함께 나눠요.

글쓰기
총 1431개의 이야기
댓글