docs/guide/project-structure
시작하기 · 코드 구성

프로젝트 구조

빌드 진입점, 기능 UI, 스타일, 에셋을 확장하기 좋은 위치에 두고, 각 메모리를 누가 소유하는지 파악합니다.

약 6분 분량

권장 구조

zenit은 디렉터리 구조를 강제하지 않습니다. 빌드 파일은 안정적으로 유지하고, 기능별 트리·상태·스타일을 한 디렉터리에 나란히 두면 기능 하나를 고칠 때 저장소 안을 오가지 않아도 됩니다.

myapp/
myapp/
├── build.zig
├── build.zig.zon
└── src/
    ├── main.zig
    ├── app_state.zig
    ├── assets.zig
    └── features/
        └── dashboard/
            ├── view.zig
            ├── state.zig
            └── styles.zig

파일별 역할

파일담당피할 것
main.zigAllocator, App 초기화, 루트 마운트개별 페이지 UI를 쌓아 두기
view.zig노드 트리와 이벤트 연결하드코딩된 색상, 복잡한 데이터 변환
state.zig비즈니스 상태와 메서드제거되거나 다시 빌드될 수 있는 Node 포인터를 오래 보관하기
styles.zigToken 기반의 이름 있는 스타일 함수 fn (*const ui.ThemeTokens) ui.BoxStyleIO 수행이나 상태 변경

스타일 함수는 순수 함수이며, ui.boxStyled / ui.vstackStyled 같은 빌더가 마운트 시 평가합니다. 테마가 바뀌면 프레임워크가 새 토큰으로 다시 적용하지만, 인라인 리터럴 스타일은 그렇지 않습니다.

styles.zig + view.zig
// features/dashboard/styles.zig
const ui = @import("ui");

pub fn card(t: *const ui.ThemeTokens) ui.BoxStyle {
    return .{
        .padding = ui.Padding.all(t.space._6),
        .gap = t.space._4,
        .background = t.color.bg_secondary,
        .corner_radius = t.radius.xl,
    };
}

// features/dashboard/view.zig
const S = @import("styles.zig");
const panel = try ui.vstackStyled(cx, S.card, .{});

임포트 경계

공개 표면은 두 계층입니다. 자주 쓰는 타입과 빌더는 ui.X에, 고급 기능은 관심사별로 ui.<group>.X(ui.widgets, ui.fx, ui.events, ui.theme 등)에 있습니다. 이 두 계층 밖의 모든 것은 내부 구현입니다.

zig
const ui = @import("ui");

// Tier 1: what most UI code needs
const Node = ui.Node;
const Signal = ui.Signal;

// Tier 2: grouped by concern
const Router = ui.fx.Router;
const Button = ui.widgets.Button;
const Event = ui.events.Event;

이 경계는 빌드 시스템이 보장합니다. zenit.attach는 앱에 ui와 zenit_app 두 모듈만 추가하므로 render, gpu 같은 내부 모듈은 아예 임포트할 수 없습니다(아키텍처 다이어그램은 개요 참고).

소유권과 수명

소유자는 둘, 수명도 두 가지입니다. Cx는 창 단위로, 노드 트리와 cx.bindState가 할당한 모든 struct를 Cx.deinit까지 소유합니다. Scope는 반응형 리소스(Signal, Memo, Effect, onCleanup 콜백, 등록된 리소스)를 소유하고 dispose() 시 해제합니다. 자식 Scope는 부모와 함께 해제됩니다.

OWNERSHIP
페이지 Scope를 폐기해도 그 페이지에서 bindState한 struct는 해제되지 않으며, 창이 닫힐 때까지 살아 있습니다. Cx.deinit의 순서는 루트 Scope dispose → 노드 해제 → state 해제입니다.

바인딩된 상태: pub deinit만 선언

struct가 pub deinit(*T)를 선언하면 프레임워크가 Cx.deinit에서 struct를 해제하기 전에 자동으로 호출합니다. 정리에 allocator가 필요하면 필드로 저장하십시오.

state.zig
const Item = struct { title: []const u8 };

const Model = struct {
    allocator: std.mem.Allocator,
    items: std.ArrayList(Item) = .empty,

    // Must be pub: Cx.deinit detects it and calls it before freeing the struct.
    pub fn deinit(self: *Model) void {
        self.items.deinit(self.allocator);
    }
};

const model = try cx.bindState(Model, .{ .allocator = cx.allocator });
// Do NOT also call scope.onCleanup(Model, model, Model.deinit):
// that would run deinit twice.

페이지 수명 리소스: onCleanup

scope.onCleanup이 필요한 경우는 두 가지뿐입니다. 정리 함수가 pub이 아니거나, 리소스가 창 전체가 아니라 특정 Scope(예: 한 페이지)의 수명을 따라야 할 때입니다. 이런 객체는 bindState를 거치지 않고 직접 할당합니다.

features/dashboard/view.zig
const PageCache = struct {
    allocator: std.mem.Allocator,
    rows: std.ArrayList(Item) = .empty,

    fn release(self: *PageCache) void {
        self.rows.deinit(self.allocator);
        self.allocator.destroy(self);
    }
};

fn mountDashboard(cx: *ui.Cx, parent: *ui.Scope) !*ui.Node {
    const scope = try parent.childScope();

    // Page-lifetime resource: freed when this scope is disposed,
    // not when the window closes.
    const cache = try cx.allocator.create(PageCache);
    cache.* = .{ .allocator = cx.allocator };
    scope.onCleanup(PageCache, cache, PageCache.release) catch |err| {
        cx.allocator.destroy(cache);
        return err;
    };

    return ui.vstack(cx, .{ .gap = cx.tokens.space._4 }, .{});
}

체크리스트

  • ✓

    main.zig는 짧게 유지합니다. allocator, App.init, runWith만 둡니다.

  • ✓

    ui와 zenit_app만 임포트합니다. 필요한 기능이 두 공개 계층에 없다면 내부 구현을 건드리지 말고 issue를 등록하십시오.

  • ✓

    창 수명은 bindState, 페이지 수명은 Scope. 전자에는 pub deinit을, 후자에는 onCleanup을 쓰고 둘을 겹치지 마십시오.

  • ✓

    스타일은 이름 있는 함수로 작성합니다. styles.zig에 두고 *Styled 빌더로 사용하면 테마 전환이 자동으로 적용됩니다.

zenit · 이중 라이선스오픈 소스 프로젝트는 GPL-3.0-only로 무료 사용할 수 있으며, 비공개 소스나 상용 제품에는 상용 라이선스가 필요합니다.작성자 연락처: zongyi.xzy#gmail.com (#을 @로 바꾸세요)zenit 5f9add5+wip 2026-09-30