---
title: "멀티 윈도우 — zenit Zig UI 문서"
description: "에디터, 미리보기, 도구 패널용으로 서로 격리된 네이티브 창을 만들고, window_id로 분배하는 하나의 이벤트 루프로 구동합니다."
url: https://zenit.z.express/ko/docs/advanced/multi-window
language: ko
alternate_en: https://zenit.z.express/docs/advanced/multi-window.md
alternate_zh: https://zenit.z.express/zh/docs/advanced/multi-window.md
alternate_es: https://zenit.z.express/es/docs/advanced/multi-window.md
alternate_ja: https://zenit.z.express/ja/docs/advanced/multi-window.md
alternate_fr: https://zenit.z.express/fr/docs/advanced/multi-window.md
alternate_de: https://zenit.z.express/de/docs/advanced/multi-window.md
project: zenit v0.1.0-alpha (Zig 0.15.2, macOS)
source: https://github.com/version-next/zenit
---

# 멀티 윈도우 앱

에디터, 미리보기, 도구 패널용으로 서로 격리된 네이티브 창을 만들고, window\_id로 분배하는 하나의 이벤트 루프로 구동합니다.

## 언제 사용하나

단일 창 앱은 계속 `App`을 사용하며, 그 API는 바뀌지 않습니다. 창마다 독립된 생명주기, 입력 라우팅, GPU surface, 접근성 트리가 필요할 때만 `MultiWindowApp`(`zenit_app.Application`으로도 export됨)으로 옮깁니다.

## 창 생성

`main.zig`

```zig
const std = @import("std");
const ui = @import("ui");
const zenit_app = @import("zenit_app");

fn mountEditor(cx: *ui.Cx, scope: *ui.Scope) anyerror!*ui.Node {
    _ = scope;
    return ui.box(cx, .{ .width = .fill(), .height = .fill() }, .{});
}

fn mountPreview(cx: *ui.Cx, scope: *ui.Scope) anyerror!*ui.Node {
    _ = scope;
    return ui.box(cx, .{ .width = .fill(), .height = .fill() }, .{});
}

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();

    var application = zenit_app.MultiWindowApp.init(gpa.allocator(), .{});
    defer application.deinit();

    // createWindowWith = create + mount in one transaction: if mount fails,
    // every native / GPU resource of that window is torn down again.
    const editor = try application.createWindowWith(.{
        .window = .{ .width = 900, .height = 700, .title = "Editor" },
    }, mountEditor);

    _ = try application.createWindowWith(.{
        .window = .{ .width = 480, .height = 700, .title = "Preview" },
    }, mountPreview);

    _ = application.activateWindow(editor.windowId());
    try application.run(); // returns after the last window closes or quit()
}
```

`createWindow*` 호출은 매번 `*App`을 반환하며, 단일 창 `App`과 같은 설정(`window`, `console`, `frame_pacing` 등)을 받습니다. `createWindow`는 생성만 하고 마운트하지 않으므로 `app.mount(mountFn)`을 직접 호출합니다. `createWindowWith`는 생성과 마운트를 하나의 트랜잭션으로 처리합니다. 저장소의 `zig build multi-window` 예제가 두 방식을 모두 보여 줍니다.

> NOTE
> 
> **창은 최대 16개입니다.** `MultiWindowApp.max_windows`는 16이며, 할당 없는 생명주기 장부를 위한 명시적 상한입니다. 이를 넘으면 `createWindow*`가 `error.TooManyWindows`를 반환합니다.

## 격리 보장

PER-WINDOW STACK

이벤트는 window\_id로 선택된 정확히 하나의 창에만 들어갑니다. 공유 모델은 창 바깥에 있고, 앱이 각 창에 명시적으로 알립니다.

**창마다 독립**

네이티브 window, Cx(자체 반응형 그래프 포함), 폰트 컨텍스트, Metal surface / renderer / device queue, IME와 접근성 경로.

**이벤트 격리**

포인터, 키보드, IME, 드래그 이벤트는 네이티브 window\_id로 라우팅됩니다. 메뉴 명령은 우연히 큐에서 꺼낸 창이 아니라 발생 시점에 캡처한 key window로 전달됩니다.

**독립적인 해제**

창을 닫으면 그 창의 리소스만 해제되고 나머지 창은 계속 렌더링합니다. 마지막 창이 닫히면 run()이 반환됩니다.

[Video](https://zenit.z.express/media/devtools-console.mp4?v=05e8c163e3)

실제 2창 앱: 관찰 대상 target과 DevTools가 각자 독립된 Cx와 창을 가집니다. harness가 도구 창을 Console로 전환해 필터링하는 동안 target 창은 계속 살아 있습니다.

## 생명주기

| API | 역할 |
| --- | --- |
| `run()` | 마지막 창이 닫히거나 quit()이 호출될 때까지 루프 구동 |
| `tick()` | pump + 프레임 한 회 실행, 루프를 직접 제어할 때 |
| `closeWindow(id)` | 창 하나를 닫음. 콜백 안에서는 안전한 경계까지 큐에 대기 |
| `quit()` | 앱 전체 종료. 남은 창은 deinit()이 순서대로 해제 |
| `window(id)` | id로 살아 있는 창 조회. 닫힌 뒤에는 null |
| `activateWindow(id)` | 창을 활성 창으로 지정 |
| `setMenuModel` / `bindMenuCommand` | 메뉴 모델은 프로세스 전역. 명령 콜백은 창별로 바인딩 |

`close.zig`

```zig
const preview_id = preview.windowId(); // preview: *App from createWindowWith

// Safe from inside an input / menu / render callback: the close is queued
// and committed at the loop's iteration boundary.
_ = application.closeWindow(preview_id);

// Look windows up by id instead of holding *App across a close.
if (application.window(preview_id)) |app| {
    _ = app; // still alive
}
```

> WARNING
> 
> **닫은 뒤 \*App은 무효입니다.** 창이 해제되는 순간, 그 창에 대해 들고 있던 `*App` 포인터는 댕글링이 됩니다. window id를 보관하고 필요할 때 `application.window(id)`로 다시 조회하세요.

## 상태 공유

창을 가로지르는 모델은 앱이 소유하고, 각 창은 자신의 뷰 상태만 가집니다. Cx마다 반응형 그래프가 독립되어 있으므로 한 창의 Effect가 다른 창 Scope의 Signal을 읽게 해서는 안 되며, Node 포인터나 Scope 리소스를 Cx 간에 공유해서도 안 됩니다.

-   **모델은 창 바깥에 둡니다.** 사실 데이터는 일반 struct나 앱 수준 store에 두어 어떤 창보다도 오래 살게 합니다.
    
-   **각 창은 필요한 부분만 미러링합니다.** 창은 자신의 Scope에 Signal을 만들고, 모델이 바뀌면 앱이 각 창의 미러에 명시적으로 씁니다.
    
-   **닫을 때 등록을 해제합니다.** 창이 닫힌 뒤에는 모델이 그 창의 Signal을 가리키는 포인터를 유지해서는 안 됩니다.
    

> TIP
> 
> **DevTools도 창입니다.** Elements / Components / Console / Performance 전체 패널을 두 번째 창에 두고 다른 창의 Cx만 관찰할 수 있습니다. 레이아웃 경계만 빠르게 볼 때는 한 줄 overlay가 더 가볍습니다. [DevTools](https://zenit.z.express/ko/docs/advanced/devtools)를 참고하세요.
