---
title: "빠른 시작 — zenit Zig UI 문서"
description: "zenit 저장소 밖에 두고 독립적으로 빌드·실행되는 네이티브 macOS 앱을 만듭니다."
url: https://zenit.z.express/ko/docs/guide/getting-started
language: ko
alternate_en: https://zenit.z.express/docs/guide/getting-started.md
alternate_zh: https://zenit.z.express/zh/docs/guide/getting-started.md
alternate_es: https://zenit.z.express/es/docs/guide/getting-started.md
alternate_ja: https://zenit.z.express/ja/docs/guide/getting-started.md
alternate_fr: https://zenit.z.express/fr/docs/guide/getting-started.md
alternate_de: https://zenit.z.express/de/docs/guide/getting-started.md
project: zenit v0.1.0-alpha (Zig 0.15.2, macOS)
source: https://github.com/version-next/zenit
---

# 빠른 시작

zenit 저장소 밖에 두고 독립적으로 빌드·실행되는 네이티브 macOS 앱을 만듭니다.

Claude Code나 다른 에이전트로 개발하나요? 먼저 zenit UI dev Skill을 설치하세요.

이 매뉴얼, 컴포넌트 선택표, 실제 프로젝트에서 겪은 함정을 에이전트가 필요할 때 불러오는 지침으로 묶었습니다. 그래서 작성되는 코드가 zenit 규칙을 따릅니다.

[Skill 다운로드.zip · 54 KB](https://zenit.z.express/downloads/zenit-ui-dev.zip)[설치 가이드](https://zenit.z.express/ko/docs/guide/ai-skill)

## 사전 준비

이번 릴리스는 macOS를 대상으로 하며 Apple Silicon에서 지속적으로 검증합니다. Zig 0.15.2와 Xcode Command Line Tools가 있는지 확인하십시오. Bun은 엔드투엔드 테스트에만 필요합니다.

```sh
zig version
xcode-select -p
```

> WARNING
> 
> **버전이 일치해야 합니다.** 템플릿은 `build.zig.zon`에 `minimum_zig_version = "0.15.2"`를 선언합니다. 다른 버전은 빌드 API나 패키지 지문에서 이해하기 어려운 오류를 낼 수 있습니다.

## 프로젝트 만들기

저장소의 `templates/minimal-app`에서 시작합니다. 완전한 다운스트림 프로젝트입니다:

`templates/minimal-app`

```
minimal-app/
├── README.md
├── build.zig          # zenit.attach() + run / app steps
├── build.zig.zon      # declares the zenit dependency
├── e2e/
│   └── record-demo.ts # drives and records the app through the Harness
└── src/
    └── main.zig       # counter-button app
```

1.  **템플릿 복사**
    
    templates/minimal-app을 zenit 저장소 밖의 새 디렉터리로 복사합니다.
    
2.  **의존성 경로 수정**
    
    템플릿 기본값 `.path = "../../"`는 저장소 안에서만 해석됩니다. 복사한 뒤 build.zig.zon 기준 상대 경로로 zenit checkout을 가리키게 하거나, `zig fetch --save=zenit <url>`로 공개된 리비전에 고정하십시오.
    
3.  **지문 생성**
    
    템플릿의 .fingerprint를 그대로 쓰지 마십시오. 이를 공유하는 두 프로젝트가 Zig 패키지 캐시에서 충돌합니다. 그 줄을 지우고 zig build를 한 번 실행한 뒤 오류에 나온 값을 다시 붙여 넣으십시오. 이참에 .name = .myapp도 바꾸십시오.
    

```sh
git clone https://github.com/version-next/zenit.git
cp -R zenit/templates/minimal-app ./myapp
cd myapp   # then set .zenit = .{ .path = "../zenit" } in build.zig.zon
zig build
```

## zenit 연결

`build.zig.zon`이 의존성을 선언하고, `build.zig`는 `@import("zenit")`로 zenit 자체 빌드 API를 가져옵니다.

`build.zig.zon`

```zig
.{
    .name = .myapp,
    .version = "0.1.0",
    .fingerprint = 0x8798022a7f8e220e, // replace: delete this line, run zig build once
    .minimum_zig_version = "0.15.2",

    .dependencies = .{
        // Relative to this file. Absolute paths are rejected by Zig 0.15.2.
        .zenit = .{ .path = "../zenit" },
    },

    .paths = .{
        "README.md",
        "build.zig",
        "build.zig.zon",
        "e2e",
        "src",
    },
}
```

다음은 템플릿의 build.zig 원문입니다. attach 호출 한 줄만이 아니라 테스트 스위치 전달, Harness 클라이언트 설치, run 단계와 .app 패키징까지 포함합니다:

`build.zig`

```zig
const std = @import("std");
const zenit = @import("zenit");

pub fn build(b: *std.Build) void {
    const target = b.standardTargetOptions(.{});
    const optimize = b.standardOptimizeOption(.{});
    const test_mode = b.option(bool, "test-mode", "Enable the Zenit automation harness") orelse false;
    const e2e_port = b.option(u16, "e2e-port", "Zenit Harness RPC directory suffix") orelse 19816;

    const zenit_dep = b.dependency("zenit", .{
        .target = target,
        .optimize = optimize,
        // Dependency build options are isolated in Zig. Forward these
        // explicitly so `zig build -Dtest-mode=true` reaches Zenit.
        .@"test-mode" = test_mode,
        .@"e2e-port" = e2e_port,
    });

    const exe = b.addExecutable(.{
        .name = "myapp",
        .root_module = b.createModule(.{
            .root_source_file = b.path("src/main.zig"),
            .target = target,
            .optimize = optimize,
        }),
    });

    zenit.attach(zenit_dep, exe);
    zenit.installHarnessClient(b, zenit_dep);

    b.installArtifact(exe);

    const run_cmd = b.addRunArtifact(exe);
    run_cmd.step.dependOn(b.getInstallStep());
    const run_step = b.step("run", "Run the app");
    run_step.dependOn(&run_cmd.step);

    // Optional: `zig build app` packages a double-clickable .app bundle.
    if (target.result.os.tag == .macos) {
        const bundled = zenit.bundleApp(b, .{
            .exe = exe,
            .display_name = "My App",
            .bundle_id = "com.example.myapp",
            .version = "0.1.0",
            .signing = .ad_hoc,
        });
        const app_step = b.step("app", "Build the .app bundle");
        app_step.dependOn(bundled.final_step);
    }
}
```

| 호출 | 역할 |
| --- | --- |
| `zenit.attach(zenit_dep, exe)` | exe에 ui와 zenit\_app 모듈 import를 추가하고, macOS ObjC 브리지 5개를 컴파일하며, Cocoa, Metal, CoreText 등 프레임워크를 링크 |
| `.@"test-mode" / .@"e2e-port"` | Zig의 의존성 옵션은 격리되어 있으므로 명시적으로 전달해야 zig build -Dtest-mode=true가 zenit에 도달 |
| `zenit.installHarnessClient(b, zenit_dep)` | 타입이 있는 Harness 클라이언트를 zig-out/share/zenit/harness/client.ts에 설치해 e2e/record-demo.ts에서 사용 |
| `zenit.bundleApp(b, .{ ... })` | 더블클릭으로 실행되는 zig-out/<display\_name>.app 생성, 여기서는 ad-hoc 서명 |

## 첫 UI 트리 마운트

`App.runWith`는 루트 Scope를 만들고 마운트 함수를 한 번 호출한 뒤 이벤트 루프를 넘겨받습니다. 마운트 함수의 시그니처는 반드시 `fn (cx: *ui.Cx, scope: *ui.Scope) anyerror!*ui.Node`여야 하며, 앱은 루트 노드만 반환합니다.

`src/main.zig`

```zig
const std = @import("std");
const ui = @import("ui");
const App = @import("zenit_app").App;

const Counter = struct {
    n: u32 = 0,
    label: ?*ui.Node = null,
    buf: [32]u8 = undefined,

    pub fn increment(self: *Counter) void {
        self.n += 1;
        const node = self.label orelse return;
        const content = std.fmt.bufPrint(&self.buf, "Clicked {d} times", .{self.n}) catch return;
        if (node.getText()) |old| {
            var t = old;
            t.content = content;
            node.setText(t);
        }
        node.markRenderDirty();
    }
};

fn mountUi(cx: *ui.Cx, scope: *ui.Scope) anyerror!*ui.Node {
    const allocator = cx.allocator;

    const counter = try cx.bindState(Counter, .{});
    const click = cx.on(Counter, counter, Counter.increment);

    const root = try ui.box(cx, .{
        .width = .fill(),
        .height = .fill(),
        .direction = .column,
        .gap = 16,
        .padding = ui.Padding.all(40),
        .background = cx.tokens.color.bg_primary,
        .align_items = .center,
        .justify = .center,
    }, .{});

    try root.appendChild(allocator, try ui.text(cx, "Hello, zenit!", .{
        .font_size = 24,
        .font_weight = 600,
        .color = cx.tokens.color.fg_primary,
    }));

    const button = try ui.widgets.Button(.{
        .label = "Click me",
        .variant = .primary,
        .on_click = click,
    }).mount(scope, cx);
    button.meta.ownership.meta.test_id = "counter.increment";
    try root.appendChild(allocator, button);

    const label = try ui.text(cx, "Clicked 0 times", .{
        .font_size = 14,
        .color = cx.tokens.color.fg_secondary,
    });
    try root.appendChild(allocator, label);
    counter.label = label;

    return root;
}

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();

    const app = try App.init(gpa.allocator(), .{
        .window = .{ .width = 640, .height = 480, .title = "Hello, zenit" },
    });
    defer app.deinit();

    try app.runWith(mountUi);
}
```

`cx.bindState`는 `Counter`를 Cx에 맡기고, `cx.on`은 그 메서드를 버튼 콜백으로 바꿉니다. `test_id` 덕분에 E2E 스크립트가 의미로 버튼을 찾을 수 있습니다.

> NOTE
> 
> **템플릿은 최소 구성을 위해 텍스트를 직접 갱신합니다.** 버퍼는 `Counter` 안의 안정된 주소에 있으므로 `setText`에 빌려줘도 안전합니다. 실제 앱에서는 카운트를 Signal에 두고 `ui.textFmt`가 레이블을 갱신하게 하는 방식이 더 일반적입니다. [반응성](https://zenit.z.express/ko/docs/guide/reactivity)을 참고하십시오.

## 빌드와 실행

```sh
zig build run   # build and run the unbundled executable
zig build app   # package zig-out/My App.app (macOS)
```

[Video](https://zenit.z.express/media/hello-button.mp4?v=b4d2b8f8a0)

빌드 산출물에서 실행한 실제 Hello Button.app(examples/hello\_button, 템플릿과 같은 카운터 버튼): harness가 hand 커서로 세 번 클릭하면 레이블이 “Clicked 3 times”가 됩니다. 창, 위젯, 응답 모두 위의 공개 API에서 나옵니다.

템플릿에는 녹화 스크립트도 들어 있습니다. `zig build -Dtest-mode=true`로 빌드하고 앱을 실행한 뒤 `e2e/record-demo.ts`를 실행하면 같은 종류의 영상을 얻습니다. 자세한 내용은 [E2E harness](https://zenit.z.express/ko/docs/advanced/e2e)를 참고하십시오.

> TIP
> 
> **다음 단계.** 창이 뜨면 다음으로 프로젝트 구조와 UI 트리를 읽으십시오. 자체 컴포넌트 계층을 서둘러 만들 필요는 없습니다. zenit은 이미 위젯, 테마, 반응형 프리미티브를 제공합니다.
