문제 해결
빌드 오류부터 상태 수명, 렌더링 이상까지, 가장 빠른 경로로 원인을 찾습니다.
빌드 오류
invalid fingerprint
템플릿의 fingerprint는 재사용할 수 없습니다. build.zig.zon에서 .fingerprint 줄을 삭제하고 zig build를 한 번 실행한 뒤, Zig가 출력한 고유 값을 다시 붙여 넣으십시오.
import of file outside module path
프레임워크 내부 파일에 zig test를 직접 실행하지 마십시오. zig build test-ui 같은 저장소의 테스트 step을 사용하세요(명령어 참고).
@import("ui") / @import("zenit_app") not found
build.zig.zon에 .zenit 의존성이 선언되어 있는지, executable을 만든 뒤 zenit.attach(zenit_dep, exe)를 호출하는지 확인하십시오. 이 호출이 ui / zenit_app 모듈을 추가하고, macOS 브리지를 컴파일하고, 시스템 프레임워크를 링크합니다.
build API field mismatch
먼저 zig version을 실행하십시오. zenit은 Zig 0.15.2가 필요하며(build.zig.zon의 minimum_zig_version), 다른 버전의 build API는 지원하지 않습니다.
-Dtest-mode=true has no effect
Zig는 의존성 옵션을 격리합니다. templates/minimal-app/build.zig처럼 b.dependency("zenit", ...)에서 .@"test-mode"와 .@"e2e-port"를 명시적으로 전달하십시오.
런타임과 상태
error.StateNotFound
명시적 id를 쓰는 cx.handler(State, id, method)가 해당 state가 생기기 전에 실행되었습니다. cx.bindState + cx.on을 우선 사용하십시오. 상태 포인터를 직접 전달하므로 관리할 id가 없습니다.
leaked ArrayList / HashMap at exit
bindState의 상태는 Cx가 소유하며 Cx가 해제될 때 함께 해제됩니다. T가 pub deinit(self: *T)를 선언하면 프레임워크가 자동으로 호출합니다. 누수는 보통 deinit이 pub이 아니거나, 리소스가 페이지 수명을 따라야 했다는 뜻입니다. 다음 섹션을 참고하세요.
crash after switching pages
전역 상태에 이전 페이지의 *Node나 *Scope를 보관하고 있지 않은지 확인하십시오. 페이지의 Scope가 dispose되면 이 포인터들은 모두 무효가 되므로 함께 지워야 합니다.
Invalid free after updating text
노드의 현재 텍스트는 노드가 소유한 복사본일 수 있습니다. getText()로 얻은 props의 content를 수정해 setText로 되돌려 넣지 마십시오. 문자열을 복사하고 소유권을 올바르게 표시하는 try node.setTextContent(cx.allocator, "Updated")를 사용하세요.
상태를 올바르게 정리하기
상태에 allocator 필드를 두고 pub deinit이 정리를 맡게 하십시오. 초기값은 아직 리소스를 소유해서는 안 되며, bindState 이후에 할당합니다.
const Editor = struct {
allocator: std.mem.Allocator,
lines: std.ArrayList([]const u8) = .empty,
// pub: the Cx calls this automatically when it frees the state.
pub fn deinit(self: *Editor) void {
self.lines.deinit(self.allocator);
}
};
// The initial value must not own resources yet; allocate after binding.
const editor = try cx.bindState(Editor, .{ .allocator = cx.allocator });scope.onCleanup은 두 경우에만 사용합니다. 정리 함수가 pub deinit이 아닐 때, 또는 리소스가 Cx 전체가 아니라 특정 Scope(페이지)를 따라야 할 때입니다.
// Resources that must follow a page (Scope), not the whole window (Cx).
const PageCache = struct {
allocator: std.mem.Allocator,
map: std.StringHashMapUnmanaged(u32) = .empty,
// Not named `pub fn deinit`: the Cx would call it again and double-free.
fn release(self: *PageCache) void {
self.map.deinit(self.allocator);
}
};
const cache = try cx.bindState(PageCache, .{ .allocator = cx.allocator });
try scope.onCleanup(PageCache, cache, PageCache.release);레이아웃과 그리기
state changed but the text did not
setText는 이미 이전/새 시그니처를 비교하고 필요에 따라 sizing / render dirty를 표시하므로 수동 markRenderDirty가 필요 없습니다. 텍스트가 갱신되지 않는다면 보통 TextProps 복사본을 고치고 setText를 호출하지 않았거나, 표시 값이 해당 Signal을 구독하지 않은 경우입니다. ui.textFmt(cx, scope, fmt, .{ signals }, props)로 Signal / Memo를 직접 구독하는 방식을 권장합니다.
some colors ignore a theme switch
boxStyled / textStyled 등의 생성자는 on_theme hook을 붙이며, cx.setTheme는 cx.root 서브트리에서만 이를 다시 실행합니다. 일반 노드와 컴포넌트 스타일은 mount 시점의 스냅샷입니다. 테마가 바뀔 때 해당 트리를 다시 만들거나, Effect에서 cx.themeSignal(scope)를 구독해 스타일을 갱신하십시오. cx.root 아래에 붙지 않은 노드도 갱신되지 않습니다.
clicks fall through an overlay’s empty area
상호작용이 없는 순수 시각 컨테이너는 기본적으로 pass_through입니다. 오버레이 루트의 스타일 확장에 hit_behavior = .@"opaque"를 설정하거나, 자체 barrier를 갖춘 Modal / Sheet를 사용하십시오.
a huge invisible hit area in the window
ui.devtools.overlay를 붙여 히트되는 노드를 검사하고, node.globalRect()로 화면상의 rect를 출력하십시오. 흔한 원인은 부모의 fill / grow 크기 설정이나 지나치게 큰 절대 위치입니다. 숨긴 오버레이라면 히트 테스트에서 빠졌는지 확인하세요. node.setDisplay(.none)로 숨긴 서브트리는 레이아웃, 그리기, 히트 테스트, Tab 순회에서 통째로 빠집니다.
// Plain visual containers are pass-through by default.
// Make an overlay root swallow clicks on its empty area:
(try panel.style.ensureExtFallible(cx.allocator)).hit_behavior = .@"opaque";그래도 해결되지 않을 때
이슈를 등록할 때는 최소한 다음을 포함하십시오.
- ✓
버전: zenit revision,
zig version, macOS 버전. - ✓
재현: 최소 재현 코드와 실행한 build step.
- ✓
출력: 마지막 줄만이 아닌 전체 오류 출력.
- ✓
렌더링 문제: 스크린샷과 DevTools에서 확인한 관련 노드의 크기.