Dialog에서 Portal 적용

PSSGYT·2025년 8월 5일
  • shadcn/ui의 dialog (= Radix 기반)
    "use client"
    
    import * as React from "react"
    import * as DialogPrimitive from "@radix-ui/react-dialog"
    import { XIcon } from "lucide-react"
    
    import { cn } from "@/lib/utils"
    
    function Dialog({
      ...props
    }: React.ComponentProps<typeof DialogPrimitive.Root>) {
      return <DialogPrimitive.Root data-slot="dialog" {...props} />
    }
    
    function DialogTrigger({
      ...props
    }: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
      return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
    }
    
    function DialogPortal({
      ...props
    }: React.ComponentProps<typeof DialogPrimitive.Portal>) {
      return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
    }
    
    function DialogClose({
      ...props
    }: React.ComponentProps<typeof DialogPrimitive.Close>) {
      return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
    }
    
    function DialogOverlay({
      className,
      ...props
    }: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
      return (
        <DialogPrimitive.Overlay
          data-slot="dialog-overlay"
          className={cn(
            "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
            className
          )}
          {...props}
        />
      )
    }
    
    function DialogContent({
      className,
      children,
      showCloseButton = true,
      ...props
    }: React.ComponentProps<typeof DialogPrimitive.Content> & {
      showCloseButton?: boolean
    }) {
      return (
        <DialogPortal data-slot="dialog-portal">
          <DialogOverlay />
          <DialogPrimitive.Content
            data-slot="dialog-content"
            className={cn(
              "bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
              className
            )}
            {...props}
          >
            {children}
            {showCloseButton && (
              <DialogPrimitive.Close
                data-slot="dialog-close"
                className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
              >
                <XIcon />
                <span className="sr-only">Close</span>
              </DialogPrimitive.Close>
            )}
          </DialogPrimitive.Content>
        </DialogPortal>
      )
    }
    
    function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
      return (
        <div
          data-slot="dialog-header"
          className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
          {...props}
        />
      )
    }
    
    function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
      return (
        <div
          data-slot="dialog-footer"
          className={cn(
            "flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
            className
          )}
          {...props}
        />
      )
    }
    
    function DialogTitle({
      className,
      ...props
    }: React.ComponentProps<typeof DialogPrimitive.Title>) {
      return (
        <DialogPrimitive.Title
          data-slot="dialog-title"
          className={cn("text-lg leading-none font-semibold", className)}
          {...props}
        />
      )
    }
    
    function DialogDescription({
      className,
      ...props
    }: React.ComponentProps<typeof DialogPrimitive.Description>) {
      return (
        <DialogPrimitive.Description
          data-slot="dialog-description"
          className={cn("text-muted-foreground text-sm", className)}
          {...props}
        />
      )
    }
    
    export {
      Dialog,
      DialogClose,
      DialogContent,
      DialogDescription,
      DialogFooter,
      DialogHeader,
      DialogOverlay,
      DialogPortal,
      DialogTitle,
      DialogTrigger,
    }
    
function DialogContent({
  className,
  children,
  showCloseButton = true,
  ...props
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
  showCloseButton?: boolean
}) {
  return (
    <DialogPortal data-slot="dialog-portal">
      <DialogOverlay />
      <DialogPrimitive.Content
        .....
  • DialogContent 안에서 DialogPortal을 자동으로 사용하고 있음 ⇒ shadcn/ui의 디자인 패턴

Portal의 장점들

  1. DOM 계층 구조 탈출: 다이얼로그가 현재 컴포넌트의 DOM 위치와 관계없이 document.body에 렌더링됨
  2. z-index 문제 해결: 부모 요소의 overflow: hidden이나 z-index 제약을 받지 않음
  3. 접근성 향상: 스크린 리더가 다이얼로그를 올바르게 인식하고 포커스 관리가 개선됨
  4. CSS 스타일링 독립성: 부모 컴포넌트의 CSS 스타일에 영향받지 않음
profile
pasongsong gayeon tak!

0개의 댓글