모달 창 사용 예제
confirmDialog/alertDialog가 예/아니오 선택을 Promise로 돌려주는 것과 달리, Modal은 폼 등 임의의
내용을 담을 수 있는 선언형 컴포넌트다 — 열림 상태(open/handleClose)를 부모가 직접 들고 제어한다.
Modal 공용 컴포넌트
Section titled “Modal 공용 컴포넌트”- 기본 푸터는 확인/취소 두 버튼이다.
footer를 직접 넘기면 그 내용을 그대로 쓰고,handleConfirm만 넘기면 기본 확인/취소 버튼이 생성된다.components/molecules/modal/Modal.tsx export const Modal = ({open,handleClose,title,footer,confirmText,cancelText,confirmColorPalette,handleConfirm,size,width,height,fullScreen = false,children,}: ModalProps) => {const resolvedFooter =footer ??(handleConfirm && (<><Button variant="outline" colorPalette="gray" handleClick={handleCancelClick}>{cancelText ?? '취소'}</Button><Button colorPalette={confirmColorPalette ?? 'brand'} handleClick={handleConfirm}>{confirmText ?? '확인'}</Button></>))return (<Dialog.Rootopen={open}onOpenChange={handleOpenChange}size={size ?? 'md'}placement="center"scrollBehavior="inside"><Portal><Dialog.Content width={resolvedWidth} height={resolvedHeight}>{title && (<Dialog.Header><Dialog.Title>{title}</Dialog.Title></Dialog.Header>)}<Dialog.Body>{children}</Dialog.Body>{resolvedFooter && <Dialog.Footer>{resolvedFooter}</Dialog.Footer>}</Dialog.Content></Portal></Dialog.Root>)} handleConfirm이Promise를 반환하면 완료까지 확인 버튼에 스피너가 뜬다.- 크기는
fullScreen>width/height>size순으로 우선한다 —fullScreen이면 나머지는 모두 무시하고 뷰포트에서 상하좌우 20px씩 뺀 크기로 띄운다.
사용 예: react-hook-form + zod 등록/수정 모달
Section titled “사용 예: react-hook-form + zod 등록/수정 모달”domainprop 유무로 등록/수정 모드를 가른다 — 있으면PUT(수정), 없으면POST(등록).pages/word/domain/components/DomainFormModal.tsx export type DomainFormModalProps = {open: booleanhandleClose: () => voiddomain?: DomainResponseDto | null}const domainFormSchema = z.object({name: z.string().min(1, '이름을 입력하세요'),description: z.string().min(1, '상세설명을 입력하세요'),}) satisfies z.ZodType<DomainFormValues>export const DomainFormModal = ({open, handleClose, domain}: DomainFormModalProps) => {const {saveDomain} = useDomainSaveMutation()const {updateDomain} = useDomainUpdateMutation()const {register,handleSubmit,reset,formState: {errors},} = useForm<DomainFormValues>({resolver: zodResolver(domainFormSchema),defaultValues: DOMAIN_FORM_INITIAL,})// 모달이 열릴 때마다 그 시점의 domain 기준으로 폼을 다시 채운다.useEffect(() => {if (open) {reset(domain ? toDomainFormValues(domain) : DOMAIN_FORM_INITIAL)}}, [open, domain, reset])const handleConfirm = handleSubmit(async (values) => {const payload = toDomainRequestPayload(values)try {if (domain && isNotNull(domain.id)) {await updateDomain({id: domain.id, payload})} else {await saveDomain(payload)}handleClose()} catch {// 실패 토스트는 useDomainSaveMutation/useDomainUpdateMutation의 onError가 이미 띄운다.}})return (<Modalopen={open}handleClose={handleClose}title={domain ? '도메인 수정' : '도메인 등록'}confirmText={domain ? '수정' : '등록'}handleConfirm={handleConfirm}><FormField<DomainFormValues>name="name"label="이름"register={register}error={errors.name}required/><FormField<DomainFormValues>name="description"label="상세설명"register={register}error={errors.description}required/></Modal>)}handleConfirm은 react-hook-form의handleSubmit으로 감싼다 — 검증에 실패하면onSubmit을 호출하지 않고 각 필드의 에러만 채우므로 모달은 열린 채로 남는다. 요청이 실패해도(에러 토스트는 뮤테이션이 띄운다) 모달을 열어둔 채 사용자가 입력을 고쳐 다시 시도할 수 있게 한다.- 성공 시에만
handleClose()를 호출해 모달을 닫는다.