---
title: "프로젝트 구조 — zenit Zig UI 문서"
description: "빌드 진입점, 기능 UI, 스타일, 에셋을 확장하기 좋은 위치에 두고, 각 메모리를 누가 소유하는지 파악합니다."
url: https://zenit.z.express/ko/docs/guide/project-structure
language: ko
alternate_en: https://zenit.z.express/docs/guide/project-structure.md
alternate_zh: https://zenit.z.express/zh/docs/guide/project-structure.md
alternate_es: https://zenit.z.express/es/docs/guide/project-structure.md
alternate_ja: https://zenit.z.express/ja/docs/guide/project-structure.md
alternate_fr: https://zenit.z.express/fr/docs/guide/project-structure.md
alternate_de: https://zenit.z.express/de/docs/guide/project-structure.md
project: zenit v0.1.0-alpha (Zig 0.15.2, macOS)
source: https://github.com/version-next/zenit
---

# 프로젝트 구조

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

## 권장 구조

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.zig` | Allocator, App 초기화, 루트 마운트 | 개별 페이지 UI를 쌓아 두기 |
| `view.zig` | 노드 트리와 이벤트 연결 | 하드코딩된 색상, 복잡한 데이터 변환 |
| `state.zig` | 비즈니스 상태와 메서드 | 제거되거나 다시 빌드될 수 있는 Node 포인터를 오래 보관하기 |
| `styles.zig` | Token 기반의 이름 있는 스타일 함수 `fn (*const ui.ThemeTokens) ui.BoxStyle` | IO 수행이나 상태 변경 |

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

`styles.zig + view.zig`

```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` 같은 내부 모듈은 아예 임포트할 수 없습니다(아키텍처 다이어그램은 [개요](https://zenit.z.express/ko/docs#architecture) 참고).

> WARNING
> 
> **API는 아직 1.0 이전입니다.** zenit은 0.1.0부터 공개되어 시맨틱 버저닝을 따르지만, 1.0 이전에는 minor 버전에서도 호환성이 깨질 수 있습니다(모든 변경은 docs/MIGRATION.md에 기록). 브랜치 최신 커밋을 따라가지 말고 정확한 버전이나 revision에 고정하십시오.

## 소유권과 수명

소유자는 둘, 수명도 두 가지입니다. `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`

```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.
```

> WARNING
> 
> **두 번 정리하지 마십시오.** `bindState` struct에 pub `deinit`을 선언하고 *동시에* `scope.onCleanup(…, T.deinit)`을 등록하면 deinit이 scope dispose 때 한 번, `Cx.deinit` 때 한 번, 총 두 번 실행됩니다. pub이 아닌 `deinit`은 감지되지 않으며 자동으로 호출되지 않습니다.

### 페이지 수명 리소스: onCleanup

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

`features/dashboard/view.zig`

```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` 빌더로 사용하면 테마 전환이 자동으로 적용됩니다.
