---
title: "컴포넌트 — zenit Zig UI 문서"
description: "zenit 컴포넌트가 무엇을 약속하는지, 언제 하위 레이어로 내려가야 하는지, 그리고 마운트·오버레이·테스트라는 세 가지 계약을 설명합니다."
url: https://zenit.z.express/ko/docs/guide/components
language: ko
alternate_en: https://zenit.z.express/docs/guide/components.md
alternate_zh: https://zenit.z.express/zh/docs/guide/components.md
alternate_es: https://zenit.z.express/es/docs/guide/components.md
alternate_ja: https://zenit.z.express/ja/docs/guide/components.md
alternate_fr: https://zenit.z.express/fr/docs/guide/components.md
alternate_de: https://zenit.z.express/de/docs/guide/components.md
project: zenit v0.1.0-alpha (Zig 0.15.2, macOS)
source: https://github.com/version-next/zenit
---

# 내장 컴포넌트

zenit 컴포넌트가 무엇을 약속하는지, 언제 하위 레이어로 내려가야 하는지, 그리고 마운트·오버레이·테스트라는 세 가지 계약을 설명합니다. 컴포넌트별 녹화, props, 결과 타입은 [컴포넌트 사이트](https://zenit.z.express/ko/components)에 있습니다.

## 여기서 말하는 ‘컴포넌트’란

zenit 컴포넌트는 색상과 모서리 반경을 이어 붙이는 helper가 아닙니다. 공개 컴포넌트는 보통 노드 구조, 영속 상태, 이벤트 라우팅, 포커스와 키보드 시맨틱, 접근성 속성, 테마 token, 오버레이 수명, 그리고 harness가 찾아내고 다시 읽을 수 있는 테스트 경계를 함께 캡슐화합니다. 모두 `ui.widgets`에서 export됩니다.

**51**컴포넌트 사이트의 Story

**6**사용 사례 분류 (Labs 별도)

**1:1**Story 대 E2E test\_id

ANATOMY

mount는 먼저 자식 Scope를 만듭니다. 자식 Scope가 소유한 모든 것은 부모 Scope와 함께 해제됩니다.

## 먼저 올바른 레이어 선택하기

| 요구 | 레이어 | 여전히 직접 맡는 것 |
| --- | --- | --- |
| 표준 제품 컨트롤 | `ui.widgets.*` | 비즈니스 상태, 문구, 콜백, 배치 |
| 맞춤 시각, 검증된 인터랙션 | `ui.hooks` · `ui.interaction` · `ui.select_headless` | 그리기, token, a11y 레이블, 합성 경계 |
| 새로운 인터랙션 패턴 | `ui.Node` + `ui.events` | 히트 테스트, 키보드, 포커스, IME, a11y, 테스트 계약 |

> TIP
> 
> **widgets부터 시작하세요.** 기존 컴포넌트의 동작 모델이 맞지 않을 때만 하위 레이어로 내려가세요. 모양을 바꾸려고 포커스, IME, 오버레이 닫기, 접근성을 다시 구현할 가치는 거의 없습니다.

## 컴포넌트 카탈로그

모든 컴포넌트는 [컴포넌트 사이트](https://zenit.z.express/ko/components)에 전용 페이지가 있습니다. 실제 `zenit Storybook.app`의 녹화, 소스에서 생성한 props와 결과 타입, 그리고 녹화가 증명하는 동작입니다. harness는 `test_id`로 각 story를 찾고, 가상 커서나 입력을 구동하며, Metal drawable을 직접 녹화합니다.

| 분류 | 개수 | 대표 예 |
| --- | --- | --- |
| 액션과 선택 | 7 | [Button](https://zenit.z.express/ko/components/button) · [Checkbox](https://zenit.z.express/ko/components/checkbox) · [Switch](https://zenit.z.express/ko/components/switch) · [RadioGroup](https://zenit.z.express/ko/components/radio) · [Slider](https://zenit.z.express/ko/components/slider) … |
| 입력과 폼 | 10 | [Input](https://zenit.z.express/ko/components/input) · [Textarea](https://zenit.z.express/ko/components/textarea) · [Select](https://zenit.z.express/ko/components/select) · [Input · Select · Button](https://zenit.z.express/ko/components/formcompose) · [ComboBox](https://zenit.z.express/ko/components/combobox) … |
| 데이터 표시 | 17 | [Badge](https://zenit.z.express/ko/components/badge) · [Tag](https://zenit.z.express/ko/components/tag) · [Chip](https://zenit.z.express/ko/components/chip) · [Card](https://zenit.z.express/ko/components/card) · [Alert](https://zenit.z.express/ko/components/alert) … |
| 내비게이션 | 4 | [Tabs](https://zenit.z.express/ko/components/tabs) · [Accordion](https://zenit.z.express/ko/components/accordion) · [Menu](https://zenit.z.express/ko/components/menu) · [DropdownMenu](https://zenit.z.express/ko/components/dropdown) |
| 오버레이와 피드백 | 5 | [Notifier](https://zenit.z.express/ko/components/notification) · [Tooltip](https://zenit.z.express/ko/components/tooltip) · [Popover](https://zenit.z.express/ko/components/popover) · [Modal](https://zenit.z.express/ko/components/modal) · [Sheet](https://zenit.z.express/ko/components/sheet) |
| 레이아웃과 대용량 데이터 | 8 | [GlassBox](https://zenit.z.express/ko/components/glassbox) · [Divider](https://zenit.z.express/ko/components/divider) · [HStack / VStack](https://zenit.z.express/ko/components/stack) · [ui.box](https://zenit.z.express/ko/components/layoutbox) · [VirtualList](https://zenit.z.express/ko/components/virtuallist) … |
| 실험과 기능 검증 | 13 | [glasslab](https://zenit.z.express/ko/components/glasslab) · [glassislands](https://zenit.z.express/ko/components/glassislands) · [glassmotion](https://zenit.z.express/ko/components/glassmotion) · [glasschrome](https://zenit.z.express/ko/components/glasschrome) · [canvasevents](https://zenit.z.express/ko/components/canvasevents) … |

[Video](https://zenit.z.express/media/stories/button.mp4?v=6203a01415)

Button story: variant × size 매트릭스를 훑은 뒤 Primary에서 hover와 press를 보여 줍니다. 자세한 내용은 [/components/button](https://zenit.z.express/ko/components/button).

## Mount, 상태, 정리

대부분의 시각 컴포넌트는 builder 형식 `Config.mount(scope, cx)`를 사용합니다. `ui.widgets.Button(props)`는 builder를 반환하고, 실제로 트리를 만드는 것은 `mount`입니다. 반환값은 루트 `*Node`이거나 wrapper, trigger, body, panel, state 같은 핸들을 담은 결과 구조체입니다.

`mount_form.zig`

```zig
const EditorActions = struct {
    document: *Document,

    fn save(self: *EditorActions) void {
        self.document.save();
    }
};

const bindings = try cx.bindState(EditorActions, .{ .document = document });

const save = try ui.widgets.Button(.{
    .label = "Save",
    .variant = .primary,
    .on_click = cx.on(EditorActions, bindings, EditorActions.save),
}).mount(scope, cx);

const name = try ui.widgets.Input(.{
    .label_text = "Project name",
    .placeholder = "Untitled",
    .required = true,
    .width = 320,
}).mount(scope, cx);

try form.appendChild(cx.allocator, name);
try form.appendChild(cx.allocator, save);
```

일부 컴포넌트는 함수 형식으로, props, scope, cx를 한 번에 전달합니다. `mountScrollArea`, `mountGrid`, 그리고 `Select`, `ComboBox`, `TagsInput`, `NumberStepper`, `FileUpload`, `DataTable`입니다(이들은 `ui.widgets`에서 `mountX` 함수의 별칭이며 `ui.widgets.Select(props, scope, cx)`처럼 호출합니다).

`scroll_area.zig`

```zig
// Function form: props, scope and cx in one call; returns a handle struct.
const area = try ui.widgets.mountScrollArea(.{ .height = 320 }, scope, cx);
try area.content.appendChild(cx.allocator, list);
try root.appendChild(cx.allocator, area.container);
```

| 컴포넌트 | 반환값 |
| --- | --- |
| `Button` · `Input` | `*Node` |
| `Modal` | `ModalResult{ overlay, dialog, body, portaled }` |
| `Tooltip` | `TooltipResult{ wrapper, trigger, content }` |
| `Select` | `SelectMount{ wrapper, trigger, panel, state, is_open }` |
| `mountScrollArea` | `ScrollAreaResult{ container, content, state }` |
| `mountGrid` | `GridResult{ root, state }` |

config를 나중에 쓸 수 있는 live props로 취급하지 마세요. 컴포넌트는 mount 시점에 config를 읽어 노드와 상태를 만듭니다. config 안의 `signal.get()`은 스냅숏으로 한 번만 읽힙니다. 계속 바뀌는 데이터는 반환된 state, Signal(예: Modal의 `.visible(sig)`) 또는 컴포넌트의 공개 메서드를 통해 전달하세요. 임시 config에 대한 포인터를 프레임 너머로 보관해서도 안 됩니다. 컴포넌트의 내부 상태는 mount가 만든 자식 Scope에 있으며, 부모 Scope가 dispose될 때 함께 해제됩니다.

## 오버레이가 컴포넌트여야 하는 이유

Tooltip, Popover, Menu, Modal, Sheet, 그리고 앱 내 알림 [Notifier](https://zenit.z.express/ko/components/notification)는 모두 `OverlayStack`에 레이어를 등록합니다. barrier가 있는 레이어(Modal / Sheet)는 창 수준 portal(`cx.root` 아래의 `WindowOverlayPortal`)로 옮겨져 조상의 overflow 클리핑을 벗어납니다. z 값은 시맨틱 tier로 정해집니다. overlay 100, dialog 1000, toast 2000(Notifier가 있는 층), tooltip 3000이며 devtools overlay는 32000으로 고정입니다. 오버레이 안에서 연 오버레이는 항상 호스트 위에 쌓입니다. box에 absolute와 큰 z-index를 준다고 이런 동작을 얻을 수는 없습니다.

`modal.zig`

```zig
const visible = try scope.createSignal(bool, false);

const modal = try ui.widgets.Modal(.{
    .title = "Delete document?",
    .width = 420,
}).visible(visible).mount(scope, cx);

try modal.body.appendChild(cx.allocator, confirm_content);
// modal.portaled == true: the barrier already lives under the window-level
// portal. Do not append modal.overlay to the current tree.

// Open it from any handler:
visible.set(true);
```

OVERLAY PORTAL

mount를 호출한 위치가 오버레이가 그려지는 위치를 정하지 않습니다. barrier는 portal로 가고, z는 tier가 정하며, Escape는 스택 맨 위에서 아래로 내려가며 처음 닫을 수 있는 레이어를 찾습니다.

-   **내부 클릭**은 dialog / panel 안에 머물며 outside-dismiss를 일으키지 않습니다. barrier를 클릭하면 `close_on_overlay`에 따라 닫힙니다.
    
-   **Escape**는 최상위 오버레이만 닫고, 중첩된 레이어는 한 단계씩 해제됩니다.
    
-   **포커스**: 열린 Modal은 포커스를 trap하고 자동으로 포커스하며, 닫히면 원래 위치로 되돌립니다.
    
-   **Scope dispose**는 barrier 서브트리를 portal에서 능동적으로 떼어 내므로, 페이지를 바꿔도 보이지 않는 히트 레이어가 남지 않습니다.
    
-   **퇴장 트랜지션** 동안 오버레이는 보이는 상태를 유지하고, 애니메이션이 확정된 뒤에 중단됩니다. 끝 상태로 바로 건너뛰지 않습니다.
    

차단하지 않는 앱 전역 알림에는 `ui.widgets.Notifier`(이전 ToastManager 대체)를 사용합니다. `Notifier.init(scope, cx, .{})`는 toast tier에 레이어를 등록하고, 창 portal이 있으면 창 전체 컨테이너를 그곳에 마운트합니다(`portaled`가 false이면 `container`를 직접 루트에 추가하세요). 그다음 `show(.{ .tone = .success, .title = "Saved" })`가 id를 반환하고, `update`는 카드를 그 자리에서 바꾸며, `dismiss`는 카드를 거둡니다. 닫기, 만료, 버튼 동작은 `Listener` 이벤트로 앱에 돌아옵니다. 기록, 방해 금지 같은 알림 센터 로직은 앱이 직접 구현합니다.

[Video](https://zenit.z.express/media/stories/modal.mp4?v=f1b080996c)

harness가 가운데 dialog를 열고 내부 클릭으로 닫히지 않는지 검증합니다. [Modal](https://zenit.z.express/ko/components/modal)

[Video](https://zenit.z.express/media/stories/tooltip.mp4?v=64c28daff2)

가상 커서가 trigger에 hover하면 말풍선이 레이아웃을 건드리지 않고 tooltip tier로 들어갑니다. [Tooltip](https://zenit.z.express/ko/components/tooltip)

## Showcase는 회귀 테스트 모음이기도 합니다

`e2e/storybook.test.ts`는 `nav.<key>`로 각 story로 전환하고, 서브트리의 텍스트를 모아 기대한 데이터 값이 모두 있는지 단언한 뒤 스크린숏을 찍습니다. 인터랙티브 컴포넌트는 상호작용 후의 상태, 지오메트리, 픽셀도 단언합니다. 컴포넌트 사이트의 녹화는 같은 앱과 같은 시맨틱 id를 재사용하므로 ‘데모’와 ‘테스트’가 두 구현으로 갈라지지 않습니다.

| 게이트 | 잡아내는 문제 |
| --- | --- |
| 텍스트 / 상태 다시 읽기 | 빈 패널, 값을 되쓰지 않는 콜백, 잘못된 필터 / 페이지 값 |
| 지오메트리 단언 | padding box, portal 가운데 정렬, Sheet 가장자리 정렬, 드래그 이동량 |
| 픽셀 단언 | 빈 GPU 레이어, blend 회귀, 흑백이 된 이모지, 잘못된 z 순서 |
| 녹화 | hover, press, 커서, IME, 스크롤, 등장 애니메이션의 타이밍 |

> WARNING
> 
> **Storybook은 비즈니스 컴포넌트가 아닙니다.** 에디터, 파일 브라우저, 명령 팔레트, 도메인 모델은 앱에 속합니다. 공개 라이브러리는 재사용 가능한 동작과 시각 프리미티브만 약속합니다.
