Hooks
useDialog
Alert와 Confirm 형태의 다이얼로그를 쉽게 표시할 수 있습니다.
"use client";import { useDialog } from "@/hooks/use-dialog";import { Button } from "@/components/ui/button";import { Input } from "@/components/ui/input";export function UseDialogExample() { const { confirm, alert } = useDialog(); const delay = async (ms: number) => { await new Promise((resolve) => setTimeout(resolve, ms)); }; const handleAsyncConfirm = async () => { await confirm( "작업을 진행할까요?", "이 작업은 되돌릴 수 없습니다.", <Input placeholder="계정 삭제 사유를 입력해주세요." />, { isDestructive: true, onConfirm: async () => { await delay(1000); // 1초 대기 (로딩 상태 자동 반영) }, }, ); }; return ( <div className="flex flex-col gap-4 sm:flex-row"> <Button onClick={handleAsyncConfirm} variant="default"> 계정 삭제하기 </Button> </div> );}설치
pnpm
pnpm dlx shadcn@latest add @chaesunbak/use-dialognpm
npx shadcn@latest add @chaesunbak/use-dialog다음 코드를 프로젝트에 복사/붙혀넣기 하세요.
import { create } from "zustand";import { ReactNode } from "react";interface DialogOptions { cancelText?: string; confirmText?: string; isDestructive?: boolean; closeOnDimmerClick?: boolean; onConfirm?: () => void | Promise<void>; onCancel?: () => void | Promise<void>;}interface DialogState { isOpen: boolean; isLoading: boolean; title: string; description: string; content: ReactNode | null; cancelText: string; confirmText: string; isDestructive: boolean; closeOnDimmerClick: boolean; hasCancel: boolean; onConfirm: () => void | Promise<void>; onCancel: () => void | Promise<void>;}interface DialogActions { confirm: ( title: string, description?: string, content?: ReactNode | null, options?: DialogOptions, ) => Promise<boolean>; alert: ( title: string, description?: string, content?: ReactNode | null, options?: Omit<DialogOptions, "cancelText" | "onCancel">, ) => Promise<boolean>; setLoading: (isLoading: boolean) => void; close: () => void;}type DialogStore = DialogState & DialogActions;const INITIAL_STATE: DialogState = { isOpen: false, isLoading: false, title: "", description: "", content: null, cancelText: "취소", confirmText: "확인", isDestructive: false, closeOnDimmerClick: true, hasCancel: true, onConfirm: () => {}, onCancel: () => {},};export const useDialogStore = create<DialogStore>((set, get) => ({ ...INITIAL_STATE, setLoading: (isLoading) => set({ isLoading }), close: () => { const { onCancel, isOpen } = get(); if (isOpen) { onCancel(); } set({ isOpen: false, isLoading: false }); }, confirm: (title, description = "", content = null, options) => { return new Promise((resolve) => { const cancelText = options?.cancelText ?? INITIAL_STATE.cancelText; const confirmText = options?.confirmText ?? INITIAL_STATE.confirmText; const isDestructive = options?.isDestructive ?? INITIAL_STATE.isDestructive; const closeOnDimmerClick = options?.closeOnDimmerClick ?? INITIAL_STATE.closeOnDimmerClick; const onConfirmAction = options?.onConfirm; const onCancelAction = options?.onCancel; set({ isOpen: true, isLoading: false, title, description, content, cancelText, confirmText, isDestructive, closeOnDimmerClick, hasCancel: true, onConfirm: async () => { if (onConfirmAction) { set({ isLoading: true }); try { await onConfirmAction(); } finally { set({ isOpen: false, isLoading: false }); resolve(true); } } else { set({ isOpen: false }); resolve(true); } }, onCancel: async () => { if (onCancelAction) { set({ isLoading: true }); try { await onCancelAction(); } finally { set({ isOpen: false, isLoading: false }); resolve(false); } } else { set({ isOpen: false }); resolve(false); } }, }); }); }, alert: (title, description = "", content = null, options) => { return new Promise((resolve) => { const confirmText = options?.confirmText ?? INITIAL_STATE.confirmText; const isDestructive = options?.isDestructive ?? INITIAL_STATE.isDestructive; const closeOnDimmerClick = options?.closeOnDimmerClick ?? INITIAL_STATE.closeOnDimmerClick; const onConfirmAction = options?.onConfirm; set({ isOpen: true, isLoading: false, title, description, content, cancelText: INITIAL_STATE.cancelText, confirmText, isDestructive, closeOnDimmerClick, hasCancel: false, onConfirm: async () => { if (onConfirmAction) { set({ isLoading: true }); try { await onConfirmAction(); } finally { set({ isOpen: false, isLoading: false }); resolve(true); } } else { set({ isOpen: false }); resolve(true); } }, onCancel: () => { set({ isOpen: false }); resolve(false); }, }); }); },}));export const useDialog = () => { const confirm = useDialogStore((state) => state.confirm); const alert = useDialogStore((state) => state.alert); return { confirm, alert };};다음 코드를 프로젝트에 복사/붙혀넣기 하세요.
"use client";import { Loader2 } from "lucide-react";import { Button } from "@/components/ui/button";import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle,} from "@/components/ui/dialog";import { useDialogStore } from "@/hooks/use-dialog";type PointerDownOutsideEvent = CustomEvent<{ originalEvent: PointerEvent }>;export const GlobalDialog = () => { const { isOpen, isLoading, title, description, content, cancelText, confirmText, isDestructive, closeOnDimmerClick, hasCancel, onConfirm, onCancel, } = useDialogStore(); function handleOnOpenChange(open: boolean) { if (!open && !isLoading) { onCancel(); } } function handlePointerDownOutside(e: PointerDownOutsideEvent) { if (isLoading || !closeOnDimmerClick) { e.preventDefault(); } } return ( <Dialog open={isOpen} onOpenChange={handleOnOpenChange}> <DialogContent showCloseButton={false} onPointerDownOutside={handlePointerDownOutside} > <DialogHeader> <DialogTitle>{title}</DialogTitle> <DialogDescription>{description}</DialogDescription> {content} </DialogHeader> <DialogFooter> {hasCancel && ( <Button variant="outline" onClick={onCancel} disabled={isLoading}> {cancelText} </Button> )} <Button variant={isDestructive ? "destructive" : "default"} onClick={onConfirm} disabled={isLoading} > {isLoading && <Loader2 className="mr-2 size-4 animate-spin" />} {confirmText} </Button> </DialogFooter> </DialogContent> </Dialog> );};루트 레이아웃에 GlobalDialog를 추가해주세요.
관련 의존성을 설치해주세요
사용법
루트 레이아웃에 GlobalDialog를 추가해주세요
import { GlobalDialog } from "@/components/global-dialog";export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="ko">
<body>
<GlobalDialog />
{children}
</body>
</html>
);
}Alert 다이얼로그 표시하기
alert 메서드를 사용하여 기본적인 다이얼로그를 표시할 수 있어요. closeOnDimmerClick 속성을 false로 설정하면 배경 클릭으로 다이얼로그가 닫히는 것을 방지할 수 있어요.
"use client";import { useDialog } from "@/hooks/use-dialog";import { Button } from "@/components/ui/button";export function UseDialogAlertExample() { const { alert } = useDialog(); return ( <Button onClick={() => { alert("알려드릴게요", "작업이 완료됐어요.", undefined, { confirmText: "확인하기", closeOnDimmerClick: false, }); }} > 기본 Alert 다이얼로그 열기 </Button> );}Confirm 다이얼로그 표시하기
confirm 메서드는 사용자의 결정을 요구하는 상황에서 유용해요. isDestructive 속성을 true로 설정하면 위험한 액션을 나타낼 수 있어요.
"use client";import { useDialog } from "@/hooks/use-dialog";import { Button } from "@/components/ui/button";export function UseDialogConfirmExample() { const { confirm } = useDialog(); return ( <Button onClick={() => { confirm("삭제할까요?", "이 작업은 되돌릴 수 없어요.", undefined, { confirmText: "삭제하기", cancelText: "취소", isDestructive: true, }); }} > 기본 Confirm 다이얼로그 열기 </Button> );}비동기 작업 처리하기
confirm 메서드의 onConfirm 속성에 비동기 함수(Promise를 반환하는 함수)를 전달하면, 버튼을 클릭했을 때 작업이 완료될 때까지 자동으로 로딩 상태가 처리돼요.
"use client";import { useDialog } from "@/hooks/use-dialog";import { Button } from "@/components/ui/button";export function UseDialogAsyncExample() { const { confirm } = useDialog(); const delay = async (milliseconds: number) => { await new Promise((res) => setTimeout(res, milliseconds)); }; return ( <Button onClick={() => { confirm( "상담을 종료할까요?", "상담을 종료하면 대화를 이어갈 수 없어요.", undefined, { confirmText: "종료하기", cancelText: "취소", onConfirm: () => delay(2000), }, ); }} > 비동기 Confirm 다이얼로그 열기 </Button> );}인터페이스
useDialog 반환 객체
| 메서드 | 타입 | 설명 |
|---|---|---|
confirm | (title: string, description?: string, content?: ReactNode, options?: DialogOptions) => Promise<boolean> | 확인 및 취소 버튼이 있는 Confirm 다이얼로그를 띄웁니다. 사용자의 선택에 따라 true 또는 false를 반환합니다. |
alert | (title: string, description?: string, content?: ReactNode, options?: Omit<DialogOptions, "cancelText" | "onCancel">) => Promise<boolean> | 확인 버튼만 있는 Alert 다이얼로그를 띄웁니다. 확인 시 true를 반환합니다. |
DialogOptions
| 속성 | 타입 | 설명 |
|---|---|---|
cancelText | string | 취소 버튼에 표시될 텍스트입니다. |
confirmText | string | 확인 버튼에 표시될 텍스트입니다. |
isDestructive | boolean | 확인 버튼을 위험한 액션(destructive) 스타일로 표시할지 여부입니다. |
closeOnDimmerClick | boolean | 배경(Dimmer) 클릭 시 다이얼로그를 닫을지 여부입니다. 기본값은 true입니다. |
onConfirm | () => void | Promise<void> | 확인 버튼 클릭 시 실행될 (비동기) 콜백 함수입니다. 프로미스를 반환하면 대기하는 동안 버튼이 로딩 상태로 변경됩니다. |
onCancel | () => void | Promise<void> | 취소 버튼 클릭 시 실행될 (비동기) 콜백 함수입니다. |