---
title: "알림 — zenit Zig UI 문서"
description: "Notifier를 사용하면 현재 작업을 막지 않는 창 내 알림(성공, 오류, 진행률, 실행 취소, 메시지)을 띄울 수 있습니다."
url: https://zenit.z.express/ko/docs/advanced/notifications
language: ko
alternate_en: https://zenit.z.express/docs/advanced/notifications.md
alternate_zh: https://zenit.z.express/zh/docs/advanced/notifications.md
alternate_es: https://zenit.z.express/es/docs/advanced/notifications.md
alternate_ja: https://zenit.z.express/ja/docs/advanced/notifications.md
alternate_fr: https://zenit.z.express/fr/docs/advanced/notifications.md
alternate_de: https://zenit.z.express/de/docs/advanced/notifications.md
project: zenit v0.1.0-alpha (Zig 0.15.2, macOS)
source: https://github.com/version-next/zenit
---

# 앱 내 알림

Notifier를 사용하면 현재 작업을 막지 않는 창 내 알림(성공, 오류, 진행률, 실행 취소, 메시지)을 띄울 수 있습니다. 스택, 호버 시 펼치기, 8가지 고정 위치는 모두 Notifier가 처리합니다.

## Notifier란

`ui.widgets.Notifier`는 창 단위 서비스입니다. 한 번 초기화한 뒤 어디서든 `show`를 호출하면 됩니다. 카드는 창 오버레이 portal의 toast 계층(z 2000, 대화상자보다 위, tooltip보다 아래)에 놓이며 페이지 레이아웃에 참여하지 않습니다. 각 카드는 고정된 노드를 가지므로 하나를 제거해도 다른 카드가 다시 빌드되거나 등장 애니메이션이 재생되지 않습니다. 이전의 ToastManager를 대체합니다.

[Video](https://zenit.z.express/media/stories/notification.mp4?v=5f8cadb85b)

실제 Storybook: 성공, 오류, 경고, 진행률, 동작 필요, 사람 메시지, 실행 취소, 조용한 알림, 요약을 차례로 띄운 뒤, 4개를 연달아 띄워 스택을 만들고 호버로 펼친 다음 가운데 카드를 닫습니다.

필드별 API(`Notification`, `NotifierOptions`, 모든 메서드 시그니처)는 컴포넌트 사이트의 [Notification 페이지](https://zenit.z.express/ko/components/notification)에 있습니다.

## Notifier 만들기

루트 마운트 함수에서 한 번 만들고, 알림을 보내야 하는 코드(예: `cx.bindState`로 보관하는 앱 상태)에 포인터를 넘기십시오. Notifier는 전달한 Scope가 소유하며 Scope와 함께 제거됩니다.

`app.zig`

```zig
const notifier = try ui.widgets.Notifier.init(scope, cx, .{
    .position = .bottom_center,
    // Keep the stack clear of a 56px toolbar.
    .content_insets = .{ .top = 56 },
    .max_visible = 4,
});
```

| NotifierOptions 필드 | 역할 |
| --- | --- |
| `position` | 8가지 위치 중 하나, 기본값 .bottom\_center. 런타임에 setPosition으로 전환 가능 |
| `max_visible` | 펼쳤을 때 표시할 최대 카드 수(2–6, 기본값 4) |
| `expand_on_hover` | false이면 호버 시 타이머만 멈추고 펼치지 않음 |
| `content_insets` | 창 가장자리로부터의 인셋(제목 표시줄, 툴바, 상태 표시줄). 22px 고정 여백은 여기서부터 계산 |
| `width` | 카드 너비, 기본값 392 |
| `strings` | UI 문구("now", "Undo", "Reply…", 접힌 상태 배지 등). 현지화 시 교체 |
| `listener` | 사용자 동작과 만료 이벤트 콜백. setListener로도 설정 가능 |

## 알림 표시하기

`show`는 `ui.widgets.Notification` 값을 받아 그 id를 반환합니다. Notifier가 모든 문자열을 복사하므로 호출자의 버퍼는 바로 재사용할 수 있습니다. 화면에 6개를 넘으면 가장 오래된 카드가 일반 퇴장 애니메이션으로 사라집니다.

`notify.zig`

```zig
// Success: auto-dismisses after 3.6 s.
_ = try notifier.show(.{
    .tone = .success,
    .title = "Saved to iCloud",
    .body = "3 files synced · 2.1 MB",
});

// Error with two actions: errors stay until the user acts.
const failed = try notifier.show(.{
    .tone = .@"error",
    .title = "Upload failed",
    .body = "network timeout · dist/app.zip",
    .actions = &.{
        .{ .label = "Retry", .primary = true, .tag = "retry" },
        .{ .label = "View log", .tag = "log" },
    },
});

// Undo: a 7 s countdown ring and a default "Undo" button.
_ = try notifier.show(.{
    .kind = .undo,
    .title = "Moved 4 items to the Trash",
    .body = "You can undo this for 7 seconds.",
});

// A person's message with an inline reply box.
_ = try notifier.show(.{
    .tone = .violet,
    .avatar = "L",
    .title = "Lin",
    .body = "I finished the table widths in section three.",
    .reply = true,
});
```

> NOTE
> 
> **버튼은 최대 두 개.** 버튼은 구분선 아래에 같은 너비로 놓이며 중립 배경을 씁니다. primary는 더 진한 배경과 굵기를 뜻할 뿐, 톤 색상을 입지 않습니다.

## 종류, 톤, 표시 시간

카드의 모양은 `kind`(레이아웃)와 `tone`(의미 색상) 두 축으로 정해집니다. 앞쪽 아이콘은 둘로부터 도출되며, `lead` / `glyph`로 직접 지정할 수도 있습니다.

| kind | 레이아웃 |
| --- | --- |
| `.default` | 아이콘, 제목, 본문. 버튼, 답장 상자, 아바타는 선택 |
| `.progress` | 스피너, 확정 진행 막대, n / N 카운트. 수명 막대 없음 |
| `.undo` | 남은 초를 보여 주는 카운트다운 링과 기본 "Undo" 버튼 |

| tone | 용도 | 기본 표시 시간 |
| --- | --- | --- |
| `.success` | 작업 완료 | 3.6 s |
| `.@"error"` | 조치가 필요한 실패 | 고정 |
| `.warning` | 주의가 필요하지만 막지 않음 | 5.6 s |
| `.info` | 일반 정보(기본값) | 4.2 s |
| `.violet` | 사람, 초대 등 소셜 메시지 | 4.2 s, 버튼이 있으면 고정 |
| `.quiet` | 눈에 덜 띄는 상태 알림(아이콘 대신 작은 점) | 2.8 s |

-   **기본 고정:** progress 카드, 답장 상자가 있는 카드, error 톤, 버튼이 있는 violet 카드는 자동으로 닫히지 않습니다.
    
-   **명시적 재정의:** `sticky = true`는 고정을 강제하고, `duration_ms`는 기본 시간을 대체하며, `life_bar`는 하단 수명 막대를 켜고 끕니다.
    
-   **undo**는 기본 7초이며, 카운트다운 링이 타이머와 동기화됩니다.
    

## 제자리 갱신과 진행률

`update(id, …)`는 같은 카드의 종류, 톤, 제목, 본문을 다시 쓰고 수명을 재시작합니다. 파괴도, 재빌드도, 등장 재생도 없습니다. 진행률 카드는 진행률 레이어만 건드리는 `setProgress`로 진행시킵니다. 기존 `id`로 `show`를 호출해도 제자리에서 변환됩니다.

`upload.zig`

```zig
const id = try notifier.show(.{
    .kind = .progress,
    .title = "Uploading attachment",
    .body = "assets/hero-shot.png",
    .progress = .{ .value = 0, .label = "Uploading · about 5 s", .count = "0 / 5" },
});

// Each tick: only the progress layer is touched, the card isn't rebuilt.
notifier.setProgress(id, 0.6, "Uploading · about 2 s", "3 / 5");

// Done: the same card turns into a success card in place —
// no new entrance, and it now dismisses itself after 3.4 s.
try notifier.update(id, .{
    .tone = .success,
    .title = "Upload complete",
    .body = "assets/hero-shot.png",
    .duration_ms = 3400,
});
```

## 사용자 동작 처리

Notifier는 하나의 `ui.widgets.NotificationListener`를 통해 이벤트를 알립니다. `NotificationEvent`에는 카드 `id`, 이벤트 `kind`, 그리고 버튼의 `tag` / `index` 또는 답장 `text`가 담깁니다.

| EventKind | 발생 시점 |
| --- | --- |
| `.action` | 버튼이 클릭됨(tag로 구분) |
| `.undo` | Undo가 클릭됨. 링은 이미 ✓로 바뀜 |
| `.reply` | 답장 상자가 제출됨(Enter 또는 Send). 내용은 text |
| `.dismissed` | 사용자가 닫음(✕ 또는 밀어내기) |
| `.expired` | 시간이 만료됨 |

`uploads.zig`

```zig
const Uploads = struct {
    fn onEvent(ctx: ?*anyopaque, n: *ui.widgets.Notifier, e: ui.widgets.NotificationEvent) void {
        const self: *Uploads = @ptrCast(@alignCast(ctx orelse return));
        switch (e.kind) {
            .action => if (std.mem.eql(u8, e.tag, "retry")) {
                // Turn the sticky error card into a progress card in place.
                n.update(e.id, .{
                    .kind = .progress,
                    .title = "Retrying upload",
                    .progress = .{ .value = 0, .label = "Uploading", .count = "0 / 5" },
                }) catch return;
                self.restart(e.id);
            } else n.dismiss(e.id),
            .reply => self.send(e.text),
            .undo => self.restoreTrash(),
            .dismissed, .expired => {},
        }
    }
    // …restart / send / restoreTrash
};

const uploads = try cx.bindState(Uploads, .{});
notifier.setListener(.{ .context = @ptrCast(uploads), .callback = Uploads.onEvent });
```

> TIP
> 
> **답장 전송에 실패했다면?** `notifier.replyFailed(id, title, body)`를 호출하면 같은 카드를 제자리에서 실패 상태로 바꾸며, 사용자가 입력한 내용은 사라지지 않습니다.

## 스택과 호버

여러 알림은 하나의 스택으로 접힙니다. 가장 최신 카드가 앵커에 가장 가깝고 z 순서가 가장 높으며, 뒤의 각 층은 9px씩 드러나고 3.8%씩 작아지며 최대 세 층까지 보이고, "N개 더 · 호버하여 펼치기" 배지가 붙습니다. 포인터가 스택 경계(18px 확장)에 들어오면 9px 간격으로 펼쳐지고 모든 타이머가 멈추며, 벗어나면 다시 접히고 타이머가 재개됩니다.

STACK

수치는 notification/model.zig의 Spec에서 가져왔습니다: 드러남 9px, 층당 축소 3.8%, 펼침 간격 9px.

**모두 지우기**

배지 옆에 모두 지우기 버튼이 있습니다. 첫 클릭에 레이블이 펼쳐지고 두 번째 클릭에 실제로 지우며, 포인터를 치우면 다시 접힙니다. 코드에서는 dismissAll()을 호출합니다.

**밀어서 닫기**

카드 한 장을 끌어서 밀어내면 닫히며 dismissed 이벤트가 발생합니다.

**창 크기 조절**

창 크기가 바뀌면 스택이 앵커를 그대로 따라가며, 보간으로 인한 끌림이 없습니다.

## 위치

`setPosition`은 앵커, 성장 방향, 등장 오프셋만 바꾸며 카드는 다시 빌드되지 않습니다. 퇴장 중인 카드는 새 방향으로 퇴장을 마칩니다.

POSITIONS

[Video](https://zenit.z.express/media/stories/notification-positions.mp4?v=2df7ff87b6)

같은 알림 4개를 8가지 위치에 차례로 고정한 모습입니다. 왼쪽 위치는 창 가장자리에 붙어 있어 창 전체를 담았습니다.

## 알림 센터는 앱의 몫

히스토리, 방해 금지, 종 모양 진입점 같은 알림 센터 기능은 앱 로직이며 zenit 밖에 있습니다. show를 호출하는 곳에서 히스토리를 기록하고 카드를 띄울지 결정하며, 닫힘·만료·버튼 동작은 Listener로 전달받습니다. Storybook의 요약 카드가 이 패턴을 보여 줍니다. 방해 금지가 끝나면 quiet 카드 하나로 그동안 도착한 내용을 요약합니다.

> WARNING
> 
> **접근성은 아직 검증되지 않았습니다.** zenit의 다른 컴포넌트와 마찬가지로 알림 카드의 VoiceOver 검증은 아직 끝나지 않았습니다. 중요한 정보를 전달하는 유일한 수단으로 쓰지 마십시오.
