Skip to content

모달 창 사용 예제

confirmDialog/alertDialog가 예/아니오 선택을 Promise로 돌려주는 것과 달리, Modal은 폼 등 임의의 내용을 담을 수 있는 선언형 컴포넌트다 — 열림 상태(open/handleClose)를 부모가 직접 들고 제어한다.

  1. 기본 푸터는 확인/취소 두 버튼이다. 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.Root
    open={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>
    )
    }
  2. handleConfirm이 Promise를 반환하면 완료까지 확인 버튼에 스피너가 뜬다.
  3. 크기는 fullScreen > width/height > size 순으로 우선한다 — fullScreen이면 나머지는 모두 무시하고 뷰포트에서 상하좌우 20px씩 뺀 크기로 띄운다.

사용 예: react-hook-form + zod 등록/수정 모달

Section titled “사용 예: react-hook-form + zod 등록/수정 모달”
  1. domain prop 유무로 등록/수정 모드를 가른다 — 있으면 PUT(수정), 없으면 POST(등록).
    pages/word/domain/components/DomainFormModal.tsx
    export type DomainFormModalProps = {
    open: boolean
    handleClose: () => void
    domain?: 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 (
    <Modal
    open={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>
    )
    }
  2. handleConfirm은 react-hook-form의 handleSubmit으로 감싼다 — 검증에 실패하면 onSubmit을 호출하지 않고 각 필드의 에러만 채우므로 모달은 열린 채로 남는다. 요청이 실패해도(에러 토스트는 뮤테이션이 띄운다) 모달을 열어둔 채 사용자가 입력을 고쳐 다시 시도할 수 있게 한다.
  3. 성공 시에만 handleClose()를 호출해 모달을 닫는다.