밝을희 클태

[shadcn / radix-ui] Drawer 위에서 toast 동작이 막힘 본문

PongWorld 프로젝트/트러블 슈팅

[shadcn / radix-ui] Drawer 위에서 toast 동작이 막힘

huipark 2025. 4. 11. 12:00

 

이미지처럼 drawer 위에 toast를 띄우는 상황이 있는데 사용자가 toast를 스와이프 해서 없애는 동작이 전혀 되지 않는 상황이 있다.

 

원인

drawer 내부에는 overlay라는 컴포넌트가 존재한다.
이 overlay는 단순히 배경을 어둡게 처리하는 역할만 하는 게 아니라,
drawer 외부의 모든 사용자 상호작용을 차단하는 역할을 한다.

그렇다면 왜 overlay는 drawer 외부의 동작까지 막고 있을까?

 

이는 drawer가 Radix UI의 Dialog 컴포넌트를 기반으로 만들어졌기 때문이다.
Radix의 Dialog는 모달 UI의 접근성과 정확한 동작을 보장하기 위해, 다음과 같은 기능들을 제공한다:

  • 배경 어둡게 처리 (dimmed background)
  • 포커스 트랩 (Focus Trap)
  • 외부 클릭 시 닫기 (Outside click to close)
  • Esc 키로 닫기 (Escape key to close)

이러한 기능들이 정확하게 동작하려면,
오버레이(overlay)가 시각적으로만 위에 있는 것이 아니라,
기술적으로도 가장 상단 레이어에서 모든 이벤트를 먼저 받아야 한다.

dialog 구조

<Dialog.Root>
<Dialog.Trigger />
<Dialog.Portal>
<Dialog.Overlay />
<Dialog.Content>
<Dialog.Title />
<Dialog.Description />
<Dialog.Close />
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>

 

해결 방법 1 (실패) - portal + zIndex 사용

Drawer 위에 떠야 하는 고정(fixed) 컴포넌트들을,
Drawer와 동일하게 Portal을 이용해 body의 직속 자식 노드로 렌더링했다.

그 후 해당 컴포넌트에 z-index를 DrawerOverlay보다 더 높게 설정했지만,
상호작용(클릭, 포커스 등)이 여전히 차단되는 문제는 해결되지 않았다.

 

해결 방법 2 (성공) 

DrawerContent에 onInteractOutside 핸들러를 사용해,
Drawer 외부에서 발생한 포인터 또는 포커스 이벤트를 감지하고 제어할 수 있도록 처리했다.

이 핸들러는 Radix UI에서 제공하는 기능으로,
외부 영역을 클릭하거나 포커스가 이동했을 때 실행되며,
필요 시 내부에서 event.preventDefault()를 호출해 Drawer가 닫히지 않도록 막을 수 있다.

공식 문서 설명
Event handler called when an interaction (pointer or focus event) happens outside the bounds of the component. It can be prevented by calling event.preventDefault.

 

drawer.tsx 파일 수정

const DrawerContent = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DrawerPortal>
<DrawerOverlay />
<DrawerPrimitive.Content
....
// ******* 추가 *********
onInteractOutside={e => {
const { originalEvent } = e.detail;
console.log(originalEvent)
if (
originalEvent.target instanceof Element &&
(originalEvent.target.closest(".toast"))
) {
e.preventDefault();
}
}}
// *********************
{...props}
>
{children}
</DrawerPrimitive.Content>
</DrawerPortal>
))

Toaster에 toast class 추가

<Toaster position="bottom-center" className="toast" />

toast 호출 시 className에 toast pointer-events-auto 추가

toast.success('콜 예정 거래처를 업데이트했어요.', {
className: 'success-toast toast pointer-events-auto',
icon: <Success style={{ width: 24, height: 24 }} />,
});

 

위처럼 외부 요소에 이벤트가 발생했을 때,
해당 요소가 특정 조건(.toast 클래스)을 만족하는지를 판별해,
그 경우에는 event.preventDefault()를 호출함으로써
Drawer의 닫힘을 방지하는 동시에, 해당 요소에서의 터치, 클릭, 스와이프 등 상호작용도 정상적으로 작동하도록 만들 수 있다.