Dialog#
사용자에게 확인, 입력, 정보를 요청하는 모달 다이얼로그 컴포넌트입니다.
Live Preview#
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 배경 |
panelBorderRadius | double? | panel 보더 반경 |
panelPadding | double? | panel 내부 패딩 |
surfaceBlur | double? | 배경 backdrop blur sigma |
surfaceOpacity | double? | panel 표면 불투명도 (0–1) |
barrierColor |
Color? / String? |
배경 오버레이 색 |
titleStyle | CoreTextStyle? | 제목 텍스트 스타일 |
contentStyle | CoreTextStyle? | 본문 텍스트 스타일 |
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#
| 속성 | 타입 | 기본값 | 설명 |
|---|---|---|---|
open | bool | true | 다이얼로그 표시 여부 |
title |
Widget? |
null |
다이얼로그 제목 위젯 |
content | Widget? | 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:
openboolean 속성으로 상태 제어.onOpenChange콜백으로 닫기 이벤트 감지
스크림 (배경 오버레이)#
- 다이얼로그가 열리면 뒤쪽에 반투명 배경이 표시되어 배경 콘텐츠와 분리
-
Flutter:
ModalBackdrop으로 구현.dismissible: true이면 스크림 클릭으로 닫기 가능 - Web:
modal: true이면 스크림 표시
포커스 관리#
- 다이얼로그 열리면 첫 번째 포커스 가능 요소에 자동 포커스
- Flutter:
FocusTrap으로 포커스가 다이얼로그 내부에 갇힘 - 다이얼로그 닫히면 이전 포커스 요소로 복원
스크롤#
- 내용이 다이얼로그 높이를 초과하면 내부 스크롤 활성화
- 다이얼로그가 열린 동안 배경 스크롤 차단
애니메이션#
- Flutter:
DialogRoute로 Scale + Fade 애니메이션 적용 - Web: CSS 트랜지션 기반 (
DialogContainerStyle의 open-state 클래스 swap)
Overlay 마운트#
-
Flutter: 루트
Overlay에 마운트되어 ancestorClipRect/Transform영향 없이 항상 최상위에 표시 -
Web:
CoOverlayHost.dialog레이어로 portal —position: fixed기반이라 ancestoroverflow: 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자동 적용.AlertDialog는role="alertdialog"사용
포커스 트랩#
- 다이얼로그 내에서만 Tab 포커스 순환
- 배경 요소에 포커스가 가지 않도록 차단
크로스 플랫폼 차이점 (Platform Differences)#
v3.0부터 기본 API (
enabled,onChanged등)가 통일되었습니다. 아래는 플랫폼 고유 차이점만 나열합니다.
| 항목 | Flutter | Web |
|---|---|---|
| 클래스명 | CoDialog | CoDialog |
| 열기/닫기 | open boolean + onClose 콜백 |
open boolean + onClose 콜백 |
| 제목 | title: Widget | title: Component |
| 내용 | content: Widget | content: Component |
| 액션 | actions: List<Widget> |
actions: List<Component> |
| 스크림 | ModalBackdrop 위젯 |
CSS 기반 (backdropStyle + 클릭 핸들러) |
| 포커스 트랩 | FocusTrap 위젯 | 네이티브 <dialog> 포커스 관리 |
| 애니메이션 | Scale + Fade | CSS 트랜지션 |
| 오버레이 마운트 | 루트 Overlay |
CoOverlayHost.dialog 레이어 portal — popover / menu / tooltip 위에 위치 |
| ARIA 속성 | Semantics 사용 | role="dialog", aria-modal="true" |
관련 컴포넌트 (Related Components)#
- 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),
)