프로젝트 구조
빌드 진입점, 기능 UI, 스타일, 에셋을 확장하기 좋은 위치에 두고, 각 메모리를 누가 소유하는지 파악합니다.
권장 구조
zenit은 디렉터리 구조를 강제하지 않습니다. 빌드 파일은 안정적으로 유지하고, 기능별 트리·상태·스타일을 한 디렉터리에 나란히 두면 기능 하나를 고칠 때 저장소 안을 오가지 않아도 됩니다.
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 같은 빌더가 마운트 시 평가합니다. 테마가 바뀌면 프레임워크가 새 토큰으로 다시 적용하지만, 인라인 리터럴 스타일은 그렇지 않습니다.
// 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 등)에 있습니다. 이 두 계층 밖의 모든 것은 내부 구현입니다.
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는 부모와 함께 해제됩니다.
bindState한 struct는 해제되지 않으며, 창이 닫힐 때까지 살아 있습니다. Cx.deinit의 순서는 루트 Scope dispose → 노드 해제 → state 해제입니다.바인딩된 상태: pub deinit만 선언
struct가 pub deinit(*T)를 선언하면 프레임워크가 Cx.deinit에서 struct를 해제하기 전에 자동으로 호출합니다. 정리에 allocator가 필요하면 필드로 저장하십시오.
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를 거치지 않고 직접 할당합니다.
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빌더로 사용하면 테마 전환이 자동으로 적용됩니다.