Dialog | CoUI

Dialog

다이얼로그/모달 컴포넌트

Dialog#

사용자에게 확인, 입력, 정보를 요청하는 모달 다이얼로그 컴포넌트입니다.

Live Preview#

Web
Flutter
Loading Flutter...
class DialogDefaultExample extends StatefulComponent {
  const DialogDefaultExample({super.key});

  @override
  State<DialogDefaultExample> createState() => _DialogDefaultExampleState();
}

class _DialogDefaultExampleState extends State<DialogDefaultExample> {
  bool _open = false;

  @override
  Component build(BuildContext context) {
    return div([
      CoButton(
        variant: CoreButtonVariant.primary,
        onPressed: () => setState(() => _open = true),
        child: text('Open Dialog'),
      ),
      CoDialog(
        open: _open,
        onClose: () => setState(() => _open = false),
        title: text('Are you sure?'),
        content: text('This action cannot be undone.'),
        actions: [
          CoButton(
            variant: CoreButtonVariant.outline,
            onPressed: () => setState(() => _open = false),
            child: text('Cancel'),
          ),
          CoButton(
            variant: CoreButtonVariant.primary,
            onPressed: () => setState(() => _open = false),
            child: text('Continue'),
          ),
        ],
      ),
    ]);
  }
}
class DialogDefaultExample extends StatefulWidget {
  const DialogDefaultExample({super.key});

  @override
  State<DialogDefaultExample> createState() => _DialogDefaultExampleState();
}

class _DialogDefaultExampleState extends State<DialogDefaultExample> {
  bool _open = false;

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisSize: MainAxisSize.min,
      children: [
        CoButton(
          variant: CoreButtonVariant.primary,
          onPressed: () => setState(() => _open = true),
          child: const Text('Open Dialog'),
        ),
        CoDialog(
          open: _open,
          onClose: () => setState(() => _open = false),
          title: const Text('Are you sure?'),
          content: const Text('This action cannot be undone.'),
          actions: [
            CoButton(
              variant: CoreButtonVariant.outline,
              onPressed: () => setState(() => _open = false),
              child: const Text('Cancel'),
            ),
            CoButton(
              variant: CoreButtonVariant.primary,
              onPressed: () => setState(() => _open = false),
              child: const Text('Continue'),
            ),
          ],
        ),
      ],
    );
  }
}

사용 시기 (When to Use)#

이 컴포넌트를 사용하세요:

  • 사용자에게 중요한 확인을 요청할 때 (삭제, 저장 등 되돌리기 어려운 작업)
  • 추가 정보를 입력받아야 할 때 (폼 다이얼로그)
  • 중요한 알림이나 경고를 표시할 때

대신 다른 컴포넌트를 사용하세요:

  • Toast: 간단한 알림이나 성공/실패 피드백을 표시할 때
  • Drawer: 복잡한 내용이나 긴 폼을 사이드에서 보여줄 때
  • Popover: 특정 요소에 부착된 간단한 정보를 표시할 때
  • Tooltip: 짧은 도움말 텍스트만 필요할 때

기본 사용법 (Basic Usage)#

// 확인 다이얼로그
CoDialog(
  open: isDeleteDialogOpen,
  title: Text('삭제 확인'),
  content: Text('이 항목을 삭제하시겠습니까? 이 작업은 취소할 수 없습니다.'),
  actions: [
    CoButton(
      variant: CoreButtonVariant.outline,
      onPressed: handleCancel,
      child: Text('취소'),
    ),
    CoButton(
      variant: CoreButtonVariant.destructive,
      onPressed: handleDelete,
      child: Text('삭제'),
    ),
  ],
  onClose: handleCancel,
)

// 커스텀 다이얼로그
CoDialog(
  open: isEditDialogOpen,
  title: Text('프로필 편집'),
  content: Column(
    children: [
      CoTextField(label: '이름', onChanged: handleNameChange),
      CoTextField(label: '이메일', onChanged: handleEmailChange),
    ],
  ),
  actions: [
    CoButton(
      variant: CoreButtonVariant.ghost,
      onPressed: handleCancel,
      child: Text('취소'),
    ),
    CoButton(
      variant: CoreButtonVariant.primary,
      onPressed: handleSave,
      child: Text('저장'),
    ),
  ],
  onClose: handleCancel,
)
// 확인 다이얼로그 (open 속성으로 상태 제어)
CoDialog(
  open: isDeleteDialogOpen,
  title: text('삭제 확인'),
  content: text('이 항목을 삭제하시겠습니까? 이 작업은 취소할 수 없습니다.'),
  actions: [
    CoButton(
      variant: CoreButtonVariant.outline,
      onPressed: handleCancel,
      child: text('취소'),
    ),
    CoButton(
      variant: CoreButtonVariant.destructive,
      onPressed: handleDelete,
      child: text('삭제'),
    ),
  ],
  onClose: handleCancel,
)

// 커스텀 콘텐츠 다이얼로그 (프로필 편집 폼)
CoDialog(
  open: isEditDialogOpen,
  title: text('프로필 편집'),
  content: div([
    CoTextField(label: '이름', onChanged: handleNameChange),
    CoTextField(label: '이메일', onChanged: handleEmailChange),
  ]),
  actions: [
    CoButton(
      variant: CoreButtonVariant.ghost,
      onPressed: handleCancel,
      child: text('취소'),
    ),
    CoButton(
      variant: CoreButtonVariant.primary,
      onPressed: handleSave,
      child: text('저장'),
    ),
  ],
  onClose: handleCancel,
)

// 알림 다이얼로그 (단순 확인 버튼만)
CoDialog(
  open: isAlertOpen,
  title: text('오류'),
  content: text('네트워크 연결에 실패했습니다.'),
  actions: [
    CoButton(
      variant: CoreButtonVariant.primary,
      onPressed: handleClose,
      child: text('확인'),
    ),
  ],
  onClose: handleClose,
)

스타일 시스템 — dialogStyle (Epic #1302)#

CoDialog 의 panel chrome / 슬롯 미세 조정은 단일 dialogStyle (CoreDialogStyle) 으로 흐릅니다. 동작 / 콘텐츠 슬롯은 위젯 파라미터 그대로.

CoDialog(
  open: isOpen,
  title: Text('확인'),
  content: Text('정말 삭제하시겠습니까?'),
  actions: [
    CoButton(variant: .outline, onPressed: cancel, child: Text('취소')),
    CoButton(variant: .destructive, onPressed: confirm, child: Text('삭제')),
  ],
  onClose: handleClose,
  dialogStyle: CoreDialogStyle(
    panelBackgroundColor: cs.surface,
    panelBorderRadius: 16,
    panelPadding: 24,
    surfaceBlur: 0,
    surfaceOpacity: 1,
    barrierColor: cs.scrim.withOpacity(.4),
    titleStyle: CoreTextStyle(fontSize: 20, fontWeight: CoreFontWeight.semiBold),
    contentStyle: CoreTextStyle(fontSize: 14, color: cs.onSurfaceVariant),
    // actionButtonStyle 은 actions 안의 CoButton 이 자동 적용
    // (위 actions 처럼 variant 직접 주입 — Epic #1302 원칙 8)
  ),
)

CoreDialogStyle 필드#

필드타입설명
panelBackgroundColor Color? / String? panel 배경
panelBorderRadiusdouble?panel 보더 반경
panelPaddingdouble?panel 내부 패딩
surfaceBlurdouble?배경 backdrop blur sigma
surfaceOpacitydouble?panel 표면 불투명도 (0–1)
barrierColor Color? / String? 배경 오버레이 색
titleStyleCoreTextStyle?제목 텍스트 스타일
contentStyleCoreTextStyle?본문 텍스트 스타일
actionButtonStyle CoreButtonStyle? action 버튼 chrome 미세 조정 (variant 변경은 위젯 슬롯 주입)

asChild 패턴 — action 버튼 변경#

action 버튼의 시맨틱 (variant) 변경이 필요할 때는 actions 슬롯에 직접 위젯을 주입합니다 (Epic #1302 원칙 8):

CoDialog(
  actions: [
    CoButton(variant: .outline, child: Text('취소')),       // ← variant 직접
    CoButton(variant: .destructive, child: Text('삭제')),
  ],
)

dialogStyle.actionButtonStyle 은 chrome 미세 조정용 (paddingH / labelStyle 등).

Resolve chain#

design system default
  → CoreDialogTheme.style                       // 프로젝트 공통
  → 부모 컴포넌트 슬롯 오버라이드
  → widget.dialogStyle                          // 인스턴스별

Props / Parameters#

속성타입기본값설명
openbooltrue다이얼로그 표시 여부
title Widget? null 다이얼로그 제목 위젯
contentWidget?null내용 위젯
leading Widget? null 제목 앞 위젯 (아이콘 등)
trailing Widget? null 제목 뒤 위젯 (닫기 버튼 등)
actions List<Widget>? null 하단 액션 버튼
onClose VoidCallback? null 닫기 콜백
dialogStyle CoreDialogStyle? null panel chrome / nested 슬롯 묶음

변형 (Variants)#

알림 다이얼로그#

CoDialog(
  open: isAlertOpen,
  title: Text('오류'),
  content: Text('네트워크 연결에 실패했습니다.'),
  actions: [
    CoButton(
      variant: CoreButtonVariant.primary,
      onPressed: handleClose,
      child: Text('확인'),
    ),
  ],
  onClose: handleClose,
)

아이콘 포함#

CoDialog(
  open: isOpen,
  leading: Icon(Icons.warning),
  title: Text('경고'),
  content: Text('이 작업은 취소할 수 없습니다.'),
  onClose: handleClose,
)

동작 스펙 (Behavior)#

열기/닫기#

  • Flutter: showDialog() 함수로 열기. Navigator.pop(context) 또는 onConfirm/onCancel 콜백으로 닫기
  • Web: open boolean 속성으로 상태 제어. onOpenChange 콜백으로 닫기 이벤트 감지

스크림 (배경 오버레이)#

  • 다이얼로그가 열리면 뒤쪽에 반투명 배경이 표시되어 배경 콘텐츠와 분리
  • Flutter: ModalBackdrop으로 구현. dismissible: true이면 스크림 클릭으로 닫기 가능
  • Web: modal: true이면 스크림 표시

포커스 관리#

  • 다이얼로그 열리면 첫 번째 포커스 가능 요소에 자동 포커스
  • Flutter: FocusTrap으로 포커스가 다이얼로그 내부에 갇힘
  • 다이얼로그 닫히면 이전 포커스 요소로 복원

스크롤#

  • 내용이 다이얼로그 높이를 초과하면 내부 스크롤 활성화
  • 다이얼로그가 열린 동안 배경 스크롤 차단

애니메이션#

  • Flutter: DialogRoute로 Scale + Fade 애니메이션 적용
  • Web: CSS 트랜지션 기반 (DialogContainerStyle 의 open-state 클래스 swap)

Overlay 마운트#

  • Flutter: 루트 Overlay 에 마운트되어 ancestor ClipRect / Transform 영향 없이 항상 최상위에 표시
  • Web: CoOverlayHost.dialog 레이어로 portal — position: fixed 기반이라 ancestor overflow: hidden / transform 컨테이너 안에 CoDialog 를 배치해도 잘리거나 좌표 어긋남이 없음. 같은 페이지에 popover / menu / tooltip 이 열려있어도 z-순서상 dialog 가 그 위에 위치

사용 가이드라인 (Usage Guidelines)#

✅ Do#

위험한 작업은 결과를 명확히 설명하세요.

CoDialog(
  open: isOpen,
  title: Text('계정 삭제'),
  content: Text('계정을 삭제하면 모든 데이터가 영구적으로 제거됩니다. 이 작업은 취소할 수 없습니다.'),
  actions: [
    CoButton(variant: CoreButtonVariant.outline, onPressed: handleCancel, child: Text('취소')),
    CoButton(variant: CoreButtonVariant.destructive, onPressed: handleDeleteAccount, child: Text('계정 삭제')),
  ],
  onClose: handleCancel,
)

사용자가 결과를 충분히 이해한 뒤 결정할 수 있습니다.


❌ Don't#

모호한 메시지로 확인을 요청하지 마세요.

CoDialog(
  open: isOpen,
  title: Text('확인'),
  content: Text('진행하시겠습니까?'),
  actions: [
    CoButton(variant: CoreButtonVariant.primary, onPressed: handleDeleteAccount, child: Text('확인')),
  ],
  onClose: handleCancel,
)

무엇이 진행되는지 알 수 없어 실수로 위험한 작업을 실행할 수 있습니다.

✅ Do#

다이얼로그는 간결하게 유지하세요.

CoDialog(
  open: isOpen,
  title: Text('변경사항 저장'),
  content: Text('저장하지 않은 변경사항이 있습니다. 저장하시겠습니까?'),
  actions: [
    CoButton(variant: CoreButtonVariant.outline, onPressed: handleDiscard, child: Text('저장하지 않음')),
    CoButton(variant: CoreButtonVariant.primary, onPressed: handleSave, child: Text('저장')),
  ],
  onClose: handleDiscard,
)

하나의 결정에 집중하면 사용자가 빠르게 판단할 수 있습니다.


❌ Don't#

다이얼로그에 과도한 내용을 넣지 마세요.

CoDialog(
  open: isOpen,
  title: Text('설정'),
  content: ComplexSettingsForm(), // 긴 폼, 여러 탭
  onClose: handleClose,
)

복잡한 내용은 별도 페이지나 Drawer를 사용하세요. 다이얼로그는 간단한 확인/입력에 적합합니다.

✅ Do#

닫을 수 있는 방법을 항상 제공하세요.

CoDialog(
  open: isOpen,
  title: Text('알림'),
  content: Text('작업이 완료되었습니다.'),
  actions: [
    CoButton(variant: CoreButtonVariant.primary, onPressed: handleClose, child: Text('확인')),
  ],
  onClose: handleClose,
)

ESC 키, 스크림 클릭, 닫기 버튼 중 하나 이상의 닫기 방법이 있어야 합니다.


❌ Don't#

닫기 방법 없이 다이얼로그를 표시하지 마세요.

CoDialog(
  open: isOpen,
  title: Text('알림'),
  content: Text('작업이 완료되었습니다.'),
  // actions 없음, onClose 없음 — 닫을 수 없음
)

사용자가 다이얼로그에 갇혀 앱을 사용할 수 없게 됩니다.

접근성 (Accessibility)#

키보드 인터랙션#

동작
Escape다이얼로그 닫기 (dismissible일 때)
Tab다이얼로그 내 다음 포커스 가능 요소로 이동
Shift+Tab다이얼로그 내 이전 포커스 가능 요소로 이동
Enter포커스된 버튼 활성화

스크린 리더#

  • Flutter: Semantics로 다이얼로그 역할, 제목, 내용 전달. 포커스 트랩이 읽기 순서 보장
  • Web: role="dialog", aria-modal="true", aria-labelledby, aria-describedby 자동 적용. AlertDialogrole="alertdialog" 사용

포커스 트랩#

  • 다이얼로그 내에서만 Tab 포커스 순환
  • 배경 요소에 포커스가 가지 않도록 차단

크로스 플랫폼 차이점 (Platform Differences)#

v3.0부터 기본 API (enabled, onChanged 등)가 통일되었습니다. 아래는 플랫폼 고유 차이점만 나열합니다.

항목FlutterWeb
클래스명CoDialogCoDialog
열기/닫기 open boolean + onClose 콜백 open boolean + onClose 콜백
제목title: Widgettitle: Component
내용content: Widgetcontent: Component
액션 actions: List<Widget> actions: List<Component>
스크림 ModalBackdrop 위젯 CSS 기반 (backdropStyle + 클릭 핸들러)
포커스 트랩FocusTrap 위젯네이티브 <dialog> 포커스 관리
애니메이션Scale + FadeCSS 트랜지션
오버레이 마운트 루트 Overlay CoOverlayHost.dialog 레이어 portal — popover / menu / tooltip 위에 위치
ARIA 속성 Semantics 사용 role="dialog", aria-modal="true"
  • Drawer: 화면 측면에서 슬라이드되는 패널. 복잡한 내용이나 긴 폼에 적합
  • Toast: 간단한 알림 메시지. 사용자 확인이 필요 없는 피드백에 사용
  • Popover: 특정 요소에 부착되는 작은 오버레이. 간단한 추가 정보 표시에 적합

조합 예제#

// 삭제 확인 패턴
CoButton(
  variant: CoreButtonVariant.destructive,
  onPressed: () => setState(() => _isDeleteDialogOpen = true),
  child: Text('삭제'),
)

// 다이얼로그 (위젯 트리에 배치)
CoDialog(
  open: _isDeleteDialogOpen,
  title: Text('항목 삭제'),
  content: Text('선택한 ${items.length}개 항목을 삭제하시겠습니까?'),
  actions: [
    CoButton(
      variant: CoreButtonVariant.outline,
      onPressed: () => setState(() => _isDeleteDialogOpen = false),
      child: Text('취소'),
    ),
    CoButton(
      variant: CoreButtonVariant.destructive,
      onPressed: () {
        handleDelete();
        setState(() => _isDeleteDialogOpen = false);
      },
      child: Text('삭제'),
    ),
  ],
  onClose: () => setState(() => _isDeleteDialogOpen = false),
)