Dialog
Dialog
명령형 API와 컴파운드 컴포넌트로 모달, 알림, 확인 다이얼로그를 구현합니다.
설치
npm install @woon-ui/dialogimport { Dialog, alert, confirm } from '@woon-ui/dialog'
import '@woon-ui/dialog/css'사용 가이드
Dialog는 기본적으로 useDialog()로 열고 제어합니다.
간단한 알림이나 확인 흐름이 필요할 때는 alert(), confirm() 같은 호출 함수를 사용할 수 있습니다.
import { Dialog, useDialog } from '@woon-ui/dialog'Note
useDialog(), alert(), confirm()을 화면에 렌더하려면 앱 루트에 DialogRuntime이 필요합니다. 앱 루트 연결은 Runtime Setup, DialogRuntime props는 아래 API 레퍼런스를 참고하세요.
기본 사용법
useDialog() 훅에서 dialog.open()을 호출하면 다이얼로그가 열립니다.
render 함수에 close, resolve, update 등의 컨텍스트가 전달됩니다. Dialog 문서를 처음 읽는다면 이 패턴부터 익히는 게 가장 좋습니다.
값 반환
dialog.open()은 핸들 객체를 반환합니다. result Promise로 다이얼로그의 결과를 받을 수 있습니다.
중첩 다이얼로그
다이얼로그 안에서 dialog.open()을 다시 호출하면 스택으로 쌓입니다.
ESC 키는 가장 위의 다이얼로그만 닫습니다. closeAll()로 전체를 닫을 수 있습니다.
API 레퍼런스
useDialog()
useDialog()는 다이얼로그를 열고 제어하는 메서드 객체를 반환합니다.
| 메서드 | 시그니처 | 설명 |
|---|---|---|
open | (render, options?) => DialogHandle | 새 다이얼로그를 열고 핸들을 반환합니다. |
close | (id: string) => void | id에 해당하는 다이얼로그를 dismissed로 닫습니다. |
closeAll | () => void | 열려 있는 모든 다이얼로그를 닫습니다. |
open()의 두 번째 인자 options에는 DialogOptions의 모든 필드를 부분 적용할 수 있습니다.
initialData?: TData를 함께 넘기면 DialogContext.data의 초기값으로 사용됩니다.
Tip
다이얼로그 내부 어디서든 useWoonDialogContext()를 호출하면 close, resolve, closeAll, options에 접근할 수 있습니다. render prop을 직접 내려보내지 않아도 됩니다.
DialogContext
open()의 render 함수가 인자로 받는 컨텍스트입니다.
type DialogContext<TData = undefined, TResult = void> = {
data: TData // initialData로 전달한 초기값
close: () => void // dismissed로 닫기
resolve: (value: TResult) => void // resolved로 닫기
update: (next: TData | ((prev: TData) => TData)) => void // data 업데이트
}data를 사용하지 않는다면 제네릭 없이 DialogContext만 써도 됩니다.
DialogHandle
open()이 반환하는 핸들입니다. 다이얼로그 바깥에서 제어가 필요할 때 사용합니다.
type DialogHandle<TData = undefined, TResult = void> = {
id: string
close: () => void
resolve: (value: TResult) => void
update: (next: TData | ((prev: TData) => TData)) => void
result: Promise<DialogResult<TResult>>
}
type DialogResult<TResult> =
| { status: 'resolved'; value: TResult }
| { status: 'dismissed'; value: undefined }result는 닫히는 방식과 무관하게 항상 resolve됩니다. status로 어떻게 닫혔는지 구분하세요.
구조
Dialog는 dialog.open()으로 여는 imperative API와, 내부 콘텐츠를 설명하는 compound parts를 함께 사용합니다.
최소 구조는 아래와 같습니다.
import { Dialog } from '@woon-ui/dialog'
<Dialog.Root>
<Dialog.Overlay />
<Dialog.Content>
<Dialog.Title />
<Dialog.Description />
<Dialog.Close />
</Dialog.Content>
</Dialog.Root>컴파운드 컴포넌트
아래 part를 조합해 다이얼로그 구조를 만듭니다.
| Part | Description | Default element |
|---|---|---|
| Dialog.Root | 다이얼로그 전체 구조와 옵션 기본값을 감싸는 루트 컨테이너입니다. `options`와 `onOpenChange`를 지원합니다. | — |
| Dialog.Overlay | 배경 레이어입니다. closeOnOverlayClick이 켜져 있으면 클릭으로 닫힙니다. | <div> |
| Dialog.Content | 포커스 트랩과 ARIA 속성이 적용되는 실제 다이얼로그 본문입니다. | <div role="dialog"> |
| Dialog.Title | 제목을 렌더링하고 aria-labelledby를 자동으로 연결합니다. | <h2> |
| Dialog.Description | 본문 설명을 렌더링하고 aria-describedby를 자동으로 연결합니다. | <p> |
| Dialog.Close | 현재 다이얼로그를 닫는 액션 파트입니다. asChild를 지원합니다. | <button> |
Dialog.Title, Dialog.Description, Dialog.Close는 asChild prop을 지원합니다.
<Dialog.Close asChild>
<button className="my-close-button">X</button>
</Dialog.Close>Dialog.Root
Dialog.Root는 공통 modal 동작을 위한 options prop과 실제 열림 상태 lifecycle을 알리는 onOpenChange를 지원합니다.
| Prop | Type | Default |
|---|---|---|
| options | Partial<DialogOptions> | — |
| `overlay`, `trapFocus`, `scrollLock`, `closeOnOverlayClick`, `scrollTarget` 같은 공통 modal 동작을 제어합니다. | ||
| onOpenChange | (open: boolean) => void | — |
| 다이얼로그가 실제로 열리고 닫힐 때 호출되는 lifecycle 콜백입니다. | ||
DialogOptions
Tip
런타임 옵션은 Dialog.Root options={{ ... }} 또는 dialog.open(render, options)의 두 번째 인자로 전달합니다.
| Prop | Type | Default |
|---|---|---|
| overlay | boolean | true |
| 배경을 어둡게 덮는 overlay를 표시합니다. | ||
| trapFocus | boolean | true |
| 열린 동안 Tab 포커스가 다이얼로그 내부에서만 순환하도록 강제합니다. | ||
| scrollLock | boolean | true |
| 열린 동안 배경 문서의 스크롤을 잠급니다. | ||
| closeOnOverlayClick | boolean | true |
| Overlay 클릭 시 다이얼로그를 닫습니다. | ||
| scrollTarget | string|Element|null | undefined |
| 스크롤을 잠글 대상 요소. CSS 선택자 또는 Element를 전달합니다. DialogRuntime의 scrollTarget이 앱 전역 기본값으로 사용되며, 이 옵션은 호출별로 오버라이드할 때 사용합니다. 미지정 시 document.body에 적용됩니다. | ||
기본값으로도 일반적인 모달은 충분합니다. 옵션은 동작을 바꿔야 할 때만 건드리면 됩니다.
옵션 적용 우선순위는 dialog.open() 두 번째 인자 → Dialog.Root options 순입니다. 나중에 전달할수록 우선순위가 높습니다.
예를 들어 배경을 막지 않는 패널처럼 쓰고 싶다면 overlay, trapFocus, scrollLock을 꺼서 비모달 패턴으로 전환할 수 있습니다.
다만 이 경우 일반 모달과 다르게 동작한다는 점에 유의하세요.
Tip
레이아웃 밀림 방지 — 다이얼로그가 열릴 때 스크롤바가 사라지면서 레이아웃이 밀릴 수 있습니다. 전역 CSS에 아래 한 줄을 추가하면 스크롤바 공간이 항상 예약되어 밀림 현상이 사라집니다.
html {
scrollbar-gutter: stable;
}Note
height: 100dvh 레이아웃 — body 대신 내부 div가 스크롤을 담당하는 구조라면 scrollLock이 body에 적용되어 효과가 없습니다. DialogRuntime의 scrollTarget으로 앱 전역 기본값을 지정하세요. 특정 호출에서만 다른 대상이 필요하면 dialog.open()의 두 번째 인자로 오버라이드할 수 있습니다.
// 앱 전역 기본값
<DialogRuntime scrollTarget="#main-scroll" />
// 특정 호출에서만 오버라이드
dialog.open(render, { scrollTarget: '#other' })DialogRuntime
앱 루트에 한 번 렌더하는 modal runtime입니다.
<DialogRuntime
zIndex={200}
scrollTarget="#main"
components={{
alert: MyAlert,
confirm: MyConfirm,
}}
/>| Prop | Type | Default |
|---|---|---|
| zIndex | number | 200 |
| 다이얼로그 z-index 시작값. 중첩될수록 1씩 증가합니다. | ||
| scrollTarget | string|Element|null | document.body |
| scrollLock의 기본 잠금 대상. CSS 선택자나 Element를 전달할 수 있고, 미지정 시 document.body에 적용됩니다. | ||
| components.alert | ComponentType<AlertRenderContext> | 내장 UI |
| alert() 기본 렌더 컴포넌트를 교체합니다. | ||
| components.confirm | ComponentType<ConfirmRenderContext> | 내장 UI |
| confirm() 기본 렌더 컴포넌트를 교체합니다. | ||
커스터마이징
Dialog는 기능적 CSS와 상태 data 속성을 제공하고, 시각 스타일은 사용자 쪽에서 덮어쓰는 방식을 전제로 합니다.
스타일
기본 스타일은 @woon-ui/dialog/css/dialog로 적용됩니다. 컴포넌트 파일 상단 또는 전역 CSS에서 한 번만 import하세요.
import '@woon-ui/dialog/css/dialog'스타일을 바꾸는 방법은 두 가지로 생각하면 됩니다.
- 기본 구조는 그대로 두고, 현재 선택자를 기준으로 값을 덮어쓴다
- 위치와 모션까지 포함해서 완전히 다른 다이얼로그를 만들려면 아래
dialog.css전체를 출발점으로 복사해 수정한다
이제 dialog.css 하나에 위치, 애니메이션, 시각 스타일이 모두 들어 있습니다. 위치나 모션까지 포함해 바꾸고 싶으면 이 파일 전체를 복사해서 출발점으로 쓰는 편이 가장 안전합니다.
/* ── 스크롤 잠금 ── */
[data-woon-scroll-lock] {
overflow: hidden;
}
/* ── 환경 격리 ── */
[data-woon-dialog-overlay],
[data-woon-dialog-content],
[data-woon-dialog-content] *,
[data-woon-dialog-content] *::before,
[data-woon-dialog-content] *::after {
box-sizing: border-box;
}
/* ── Overlay ── */
[data-woon-dialog-overlay] {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.45);
will-change: opacity;
opacity: 0;
transition: opacity 160ms ease-out;
}
[data-woon-dialog-overlay][data-entered] {
opacity: 1;
}
/* ── Content 위치 / 시각 스타일 ── */
[data-woon-dialog-content] {
position: fixed;
top: 50%;
left: 50%;
width: min(100%, 28rem);
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
font-size: 1rem;
line-height: 1.5;
color: inherit;
will-change: transform, opacity;
opacity: 0;
transform: translate(-50%, calc(-50% + 8px)) scale(0.98);
transition:
opacity 200ms cubic-bezier(0.16, 1, 0.3, 1),
transform 200ms cubic-bezier(0.16, 1, 0.3, 1);
padding: 1.25rem 1.5rem;
background: #fff;
border-radius: 8px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12);
}
[data-woon-dialog-content][data-entered] {
opacity: 1;
transform: translate(-50%, -50%) scale(1);
}
/* ── 퇴장 애니메이션 ── */
[data-woon-dialog-overlay][data-state="closed"] {
animation: woon-overlay-out 120ms ease-in forwards;
transition: none;
}
[data-woon-dialog-content][data-state="closed"] {
animation: woon-dialog-out 140ms ease-in forwards;
transition: none;
}
@keyframes woon-overlay-out {
from { opacity: 1; }
to { opacity: 0; }
}
@keyframes woon-dialog-out {
from { opacity: 1; transform: translate(-50%, -50%) scale(1); }
to { opacity: 0; transform: translate(-50%, calc(-50% + 6px)) scale(0.98); }
}
/* ── Title / Description ── */
[data-woon-dialog-title] {
display: block;
margin: 0 0 0.35rem;
font-size: 1rem;
font-weight: 600;
}
[data-woon-dialog-description] {
display: block;
margin: 0 0 0.7rem;
}data 속성
| 속성 | 대상 | 값 |
|---|---|---|
data-woon-dialog-overlay | Overlay | — |
data-woon-dialog-content | Content | — |
data-state | Overlay, Content | "open" 또는 "closed" |
상태 기반 스타일링 예시:
[data-woon-dialog-content][data-state="open"] {
animation: my-dialog-in 200ms ease-out;
}동작과 접근성
기본 Dialog는 모달로 동작합니다. 별도 설정 없이 아래 동작이 자동으로 적용됩니다.
- 포커스가 다이얼로그 내부에 머뭅니다
- 닫히면 포커스가 이전 위치로 돌아갑니다
- 열려 있는 동안 배경 페이지 스크롤이 잠깁니다
키보드 인터랙션
| 키 | 동작 |
|---|---|
Tab | 다이얼로그 내부 포커스를 순환합니다 |
Shift + Tab | 역방향으로 포커스를 순환합니다 |
Esc | 가장 위에 떠 있는 다이얼로그만 닫습니다 |
ARIA
Dialog.Content에는role="dialog"와aria-modal="true"가 적용됩니다Dialog.Title를 사용하면aria-labelledby가 자동으로 연결됩니다Dialog.Description를 사용하면aria-describedby가 자동으로 연결됩니다