shadcn/ui와 함께 쓰기

shadcn/ui의 Dialog 컴포넌트를 overlay-kit과 함께 사용하는 방법을 알아볼게요.

설치

shadcn/ui는 CLI로 컴포넌트 코드를 프로젝트에 직접 복사해서 써요. 다음 명령어로 Dialog를 추가해요.

shell
npm install overlay-kit
npx shadcn@latest add dialog

Dialog는 Radix UI 기반으로 동작해요. shadcn CLI가 components/ui/dialog.tsx 파일을 만들고, 필요한 의존성(@radix-ui/react-dialog, lucide-react 등)을 자동으로 설치해줘요.

기본 사용법

shadcn Dialogopen prop으로 열림 상태를, onOpenChange로 닫기 이벤트를 처리해요. onOpenChangeboolean을 인자로 받기 때문에 !open일 때 close()를 호출해주면 backdrop 클릭·ESC 키로 닫히는 경우까지 자연스럽게 처리돼요.


import { OverlayProvider, overlay } from 'overlay-kit';
import {
  Dialog,
  DialogContent,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogTitle,
} from './components/ui/dialog';

function App() {
  return (
    <button
      className="inline-flex items-center justify-center rounded-md bg-gray-900 px-4 py-2 text-sm font-medium text-white hover:bg-gray-800"
      onClick={() => {
        overlay.open(({ isOpen, close }) => (
          <Dialog open={isOpen} onOpenChange={(open) => !open && close()}>
            <DialogContent>
              <DialogHeader>
                <DialogTitle>정말로 계속하시겠어요?</DialogTitle>
                <DialogDescription>이 작업은 되돌릴 수 없어요.</DialogDescription>
              </DialogHeader>
              <DialogFooter className="gap-2">
                <button
                  className="rounded-md border border-gray-200 px-4 py-2 text-sm font-medium hover:bg-gray-100"
                  onClick={close}
                >
                  아니요
                </button>
                <button
                  className="rounded-md bg-gray-900 px-4 py-2 text-sm font-medium text-white hover:bg-gray-800"
                  onClick={close}
                ></button>
              </DialogFooter>
            </DialogContent>
          </Dialog>
        ));
      }}
    >
      Confirm Dialog 열기
    </button>
  );
}

export function Example() {
  return (
    <OverlayProvider>
      <App />
    </OverlayProvider>
  );
}

비동기 결과 받기

overlay.openAsync를 사용하면 사용자가 “네”를 눌렀는지 “아니요”를 눌렀는지 Promise 결과로 받을 수 있어요. 각 버튼의 close(value)에 결과를 전달하고, onOpenChange에서 backdrop·ESC로 닫힐 때는 기본값을 넘겨주면 돼요.


import { useState } from 'react';
import { OverlayProvider, overlay } from 'overlay-kit';
import {
  Dialog,
  DialogContent,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogTitle,
} from './components/ui/dialog';

function App() {
  const [result, setResult] = useState<boolean | null>(null);

  return (
    <div>
      <p className="mb-2 text-sm">결과: {result === null ? '선택 없음' : result ? '네' : '아니요'}</p>
      <button
        className="inline-flex items-center justify-center rounded-md bg-gray-900 px-4 py-2 text-sm font-medium text-white hover:bg-gray-800"
        onClick={async () => {
          const confirmed = await overlay.openAsync<boolean>(({ isOpen, close }) => (
            <Dialog open={isOpen} onOpenChange={(open) => !open && close(false)}>
              <DialogContent>
                <DialogHeader>
                  <DialogTitle>정말로 계속하시겠어요?</DialogTitle>
                  <DialogDescription>이 작업은 되돌릴 수 없어요.</DialogDescription>
                </DialogHeader>
                <DialogFooter className="gap-2">
                  <button
                    className="rounded-md border border-gray-200 px-4 py-2 text-sm font-medium hover:bg-gray-100"
                    onClick={() => close(false)}
                  >
                    아니요
                  </button>
                  <button
                    className="rounded-md bg-gray-900 px-4 py-2 text-sm font-medium text-white hover:bg-gray-800"
                    onClick={() => close(true)}
                  ></button>
                </DialogFooter>
              </DialogContent>
            </Dialog>
          ));
          setResult(confirmed);
        }}
      >
        Confirm Dialog 열기
      </button>
    </div>
  );
}

export function Example() {
  return (
    <OverlayProvider>
      <App />
    </OverlayProvider>
  );
}

애니메이션 후 메모리 해제

shadcn Dialog는 닫기 애니메이션이 끝나는 시점을 알려주는 콜백을 따로 제공하지 않아요. onOpenChange에서 close를 호출한 뒤 애니메이션 지속 시간만큼 setTimeout으로 unmount를 예약하면 버튼·backdrop 클릭·ESC 키·X 버튼까지 모두 처리돼요.


import { OverlayProvider, overlay } from 'overlay-kit';
import {
  Dialog,
  DialogContent,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogTitle,
} from './components/ui/dialog';

function App() {
  return (
    <button
      className="inline-flex items-center justify-center rounded-md bg-gray-900 px-4 py-2 text-sm font-medium text-white hover:bg-gray-800"
      onClick={() => {
        overlay.open(({ isOpen, close, unmount }) => (
          <Dialog
            open={isOpen}
            onOpenChange={(open) => {
              if (!open) {
                close();
                // shadcn dialog의 기본 닫기 애니메이션은 약 200ms예요.
                setTimeout(unmount, 200);
              }
            }}
          >
            <DialogContent>
              <DialogHeader>
                <DialogTitle>정말로 계속하시겠어요?</DialogTitle>
                <DialogDescription>이 작업은 되돌릴 수 없어요.</DialogDescription>
              </DialogHeader>
              <DialogFooter className="gap-2">
                <button
                  className="rounded-md border border-gray-200 px-4 py-2 text-sm font-medium hover:bg-gray-100"
                  onClick={close}
                >
                  아니요
                </button>
                <button
                  className="rounded-md bg-gray-900 px-4 py-2 text-sm font-medium text-white hover:bg-gray-800"
                  onClick={close}
                ></button>
              </DialogFooter>
            </DialogContent>
          </Dialog>
        ));
      }}
    >
      Confirm Dialog 열기
    </button>
  );
}

export function Example() {
  return (
    <OverlayProvider>
      <App />
    </OverlayProvider>
  );
}