본문 바로가기

Frontend Dev Log

Dialog 닫은 뒤 클릭이 안 되는 이유? pointer-events: none 이슈 해결기

개인적으로 하는 프로젝트에서 ui 라이브러리를 사용하던 도중, 모달을 닫은 후 화면의 모든 요소가 클릭되지 않는 이슈가 있었다. 클릭은 물론 스크롤도 되지 않았다. 정말 아무것도 안 되는 이슈었다.. 

Dialog 닫은 뒤 클릭이 안 되는 이유? pointer-events: none 이슈 해결기

만약, 내가 사용자 였고, 어떤 서비스를 이용 중인데 이런 이슈가 터진다? 아주아주 치명적인 UX적 버그로 이어질 것이라고 생각한다. 이 글에서는 문제 원인부터 해결 과정, 근본적인 이유까지 정리해보려고 한다.

 

문제 상황

[ 재현 단계 ]

이슈는 이런 흐름에서 발생했다.

  1. 파일 우측 상단의 점 3개 메뉴(DropdownMenu) 클릭
  2. rename, share, delete 등의 액션 클릭 → Dialog 열림
  3. X 버튼 또는 Cancel 버튼 클릭 → Dialog 닫힘
  4. 이후 화면의 모든 UI가 클릭되지 않음

 

[ 증상 ]

  • 버튼, 링크, 입력 필드, 스크롤 등 모든 interaction 요소가 반응하지 않음.
  • 마우스 커서는 정상적으로 움직이지만 화면 내 어떠한 이벤트도 발생하지 않음.
  • 페이지 새로고침을 해야만 정상 동작 복구가 됐음.

 

원인을 분석했더니?

Chrome DevTools console에서 확인해보니 <body> 태그에 다음과 같은 스타일이 남아있었다.

// 콘솔에서 확인
document.body.style.pointerEvents    // 결과: "none"

// 정상적이라면 이래야 함
document.body.style.pointerEvents    // 결과: "" 또는 "auto

이게 문제의 핵심이었다. pointer-events: none 이 body에 적용되어 있어서 모든 마우스 이벤트가 차단되고 있던 것이다.

 

[ 나의 기술 환경 ]

{
  "@radix-ui/react-dialog": "^1.1.14",
  "@radix-ui/react-dropdown-menu": "^2.1.14",
  "@radix-ui/react-popover": "^1.1.13",
  "@radix-ui/react-tooltip": "^1.2.6",
  "@tailwindcss/vite": "^4.0.0",
  "tailwindcss-animate": "^1.0.7",
  "next": "15.3.2",
  "react": "^19.1.0",
  "framer-motion": "^12.18.1",
  "zustand": "^5.0.5"
}

 

 

 

해결 과정

문제의 원인이 pointer-events: none인 걸 확인한 후, 처음에는 Dialog의 open 상태를 직접 감지하는 방법으로 시도를 했다.

const [modalOpen, setModalOpen] = useState(false);

useEffect(() => {
  if (modalOpen) {
    // Dialog가 열릴 때 pointer-events 강제 복원
    document.body.style.pointerEvents = "auto";
  }
}, [modalOpen]);

// Dialog 컴포넌트
<Dialog open={modalOpen} onOpenChange={setModalOpen}>
  <DialogContent>
    {/* Dialog 내용 */}
  </DialogContent>
</Dialog>

 

하지만 여기서 문제가 생겼다.

이 방법으로는 rename, share, delete 액션들을 해결되었찌만, details 액션만 여전히 클릭이 안 되는 현상이 발생했다. 

 

[ 원인 ]

이 문제의 원인은 이러하다.

  1. details 액션은 다른 액션들과 달리 modalOpen 상태가 적용되지 않는 구조다.
  2. Dialog 컴포넌트의 상태 관리 방식이 액션별로 달랐다.

 

[ 최종 해결책 ]

결국 이런 문제 덕에 액션 타입을 직접 감지하는 방식으로 변경하게 되었다.

useEffect(() => {
  if (
    action?.value === "details" ||
    action?.value === "rename"  ||
    action?.value === "share"   ||
    action?.value === "delete"
  ) {
    document.body.style.pointerEvents = "auto";
  }
}, [action?.value]);


const handleAction = async () => {
        if (!action) return;

        setIsLoading(true);
        
        let success = false;

        // Define the action handlers based on the selected action
        const actions = {
            
            // action.value === "rename"
            rename: async () => {
                return await renameUploadedFile (
                    { fileId: file.$id, name, extension: file.extension, path }
                )
            },

            // action.value === "share"
            share: async () => {
                return await updateFileShareUsers (
                    { fileId: file.$id, emails, path }
                )
            },

            // action.value === "delete"
            delete: async () => {
                return await deleteUploadedFile(
                    { fileId: file.$id, bucketFileId: file.bucketFileId, path }
                )
            }

        }
        
        const result = await actions[action.value as keyof typeof actions]();
        // console.log("Action result: ", result);
        success = !!result;
        // console.log("Action success: ", success);
        
        if (success) {
            setTimeout(() => {
              closeAllModals();  // 렌더 사이클 이후에 닫기
            }, 0);
          }

        setIsLoading(false);
    }

 

[ 이 방식의 장점 ] 

  • 모든 액션에서 일관된 동작을 할 수 있다. Dialog 상태 관리 방식에 관계없이 동작하기 때문이다.
  • 각 액션이 실행될 때마다 확실하게 point-events를 복원한다.
  • 복잡한 상태 관리 구조를 바꾸지 않고도 문제를 해결할 수 있었다.

 

사실 아직까지도 details 액션만 동작 원리가 다르지 않았다면 modalOpen 상태 관리로 묶어서 여는 게 Best였다고 생각한다. 하지만 언제나 생각하는 이상적인 해결책이 통하는 것은 아니라는 점을 알고 있고, 기존 코드 구조와 상태 관리 방식을 고려한 실용적인 접근이 때로는 더 효과적이라고 생각한다.

 

왜 이런 문제가 생기지?

https://next.jqueryscript.net/shadcn-ui/stacked-dialogs-shadcn-ui/, https://www.youtube.com/watch?v=F5OjPBVuNys

Dialog UI는 내부적으로 포커스 관리와 외부 클릭 감지를 위해 복잡한 이벤트 처리를 한다. DialogPrimitive.Content와 DialogPrimitive.Overlay가 함께 쓰이는데,

 

Dialog가 열릴 때,

 

  • 배경 스크롤 방지 (body 스타일 조작)
  • 외부 클릭으로 닫기 기능 (pointer-events 제어)
  • 포커스 트랩 설정 (FocusScope)
  • 이 과정에서 body에 pointer-events: none 임시 적용

Dialog가 닫힐 때:

  • 포커스 복원
  • 스크롤 활성화
  • pointer-events 원래 상태로 복원 ← 여기서 문제 발생

 

 

실제 GitHub 이슈로 확인된 문제들

이 문제는 공식 문서에는 직접적으로 명시되지는 않았지만, Github Radix UI 커뮤니티에서 사례가 존재하는 실제 버그들이다.

https://dev.to/hornet_daemon/what-is-github-62b

 

 

Can't close opened menu || branch.contains is not a function · Issue #1882 · radix-ui/primitives

Bug report Current Behavior When I open the menu, everything works perfectly, but when I click outside of the menu, an error message appears and states that the branch is undefined and cannot be ca...

github.com

 

[Select] Allow positioning of options beneath trigger · Issue #1247 · radix-ui/primitives

Feature request Overview Currently the Select menu will position the currently selected option in the list on top of the trigger. This means that depending on the selected option the menu changes p...

github.com

 

What is the cleanest way to add props to extended components? (typescript) · radix-ui primitives · Discussion #1935

Hi there! I am trying to add additional props to components that I am extending. Here is an example where I am providing some default tailwind styles to Avatar.Root. I want to Root to be able to ac...

github.com

 

[Label] Is not working when JS is disabled in the browser · Issue #1577 · radix-ui/primitives

Bug report Current Behavior When there is a label element and a input element and both are "linked" with the same id then clicking the label element should move the focus to the input element. This...

github.com

 

 

문제가 발생하는 구체적인 시나리오

DropdownMenu → Dialog → Close 연쇄 작용 시 다음과 같은 문제가 발생한다.

  1. DropdownMenu가 열리며 첫 번째 오버레이 레이어가 생성되고,
  2. Dialog가 열리며 두 번째 오버레이 레이어가 생성된다. 이때 bodypointer-events: none이 적용되는데,
  3. 이후 Dialog가 닫히면서 정리 작업이 시작되고,
  4. 거의 동시에 DropdownMenu도 닫히면서 cleanup이 실행이 된다.
  5. 문제는 이 두 컴포넌트가 동시에 pointer-events를 복원하려 하면서, 정리 순서에 따라 최종 상태가 꼬인다는 점이다.

이로 인해 pointer-events: none body 계속 남아 있는 문제가 발생하게 되는 것이다. 즉, 이 문제는 "렌더링 및 언마운트 타이밍 이슈"에 기인한 문제라고 볼 수 있다.

https://devtoolstips.org/tips/en/select-pointer-events-none-elements/

 

Github 커뮤니티 pointer-events 미복원 이슈의 현재 상태와 커뮤니티 대응

Radix UI의 Dialog, DropdownMenu, AlertDialog 등 여러 레이어 기반 컴포넌트를 중첩 사용할 경우, body에 적용된 pointer-events:none 이 정상적으로 복원되지 않는 문제가 종종 발생한다고 한다. 이로 인해 페이지 내 인터랙션이 막히는 치명적인 UX문제가 발생할 수 있으며, 해당 이슈는 Github에도 다수 보고된 바가 있다.

 

GitHub 이슈 현황 (2022–2025)

#1241:“Update of March 2025: This bug seems to be still appearing in newer versions.”

 

[Dialog] body pointer-events: none remains after closing · Issue #1241 · radix-ui/primitives

Update of March 2025 This bug seems to be still appearing in newer versions. You will find some workarounds in the recent comments. ⬇️ Check the latest fix found by @cardoso-sj : #1241 (comment) Bu...

github.com

  • 2022년에 “완료”로 처리되었으나, 2025년 3월 업데이트에서 “여전히 발생한다”는 커뮤니티 피드백 존재.

#1577: 복수의 오버레이가 렌더링되고 언마운트되는 과정에서 pointer-events가 복원되지 않는 문제를 명확히 보고.

 

[Label] Is not working when JS is disabled in the browser · Issue #1577 · radix-ui/primitives

Bug report Current Behavior When there is a label element and a input element and both are "linked" with the same id then clicking the label element should move the focus to the input element. This...

github.com

#1935: FocusScope DismissableLayer 간 충돌로 인해 focus/scroll lock 복원 타이밍이 꼬이는 현상 보고.

 

What is the cleanest way to add props to extended components? (typescript) · radix-ui primitives · Discussion #1935

Hi there! I am trying to add additional props to components that I am extending. Here is an example where I am providing some default tailwind styles to Avatar.Root. I want to Root to be able to ac...

github.com

 

Radix UI 팀의 공식 입장

Radix UI 팀은 Dialog, DropdownMenu, DismissableLayer 등의 컴포넌트가 중첩되어 발생하는 pointer-events: none 관련 이슈에 대해 다음과 같은 내용을 공식 문서에서는 명시적으로 언급하고 있지 않다:

  • body.style.pointerEvents = 'none' 상태에 대한 수동 복원 방법공식적으로 안내하거나 권장하지 않음 (내가 쓴 방법)
  • 중첩된 오버레이 컴포넌트 간 충돌 문제에 대해 “완전히 해결되었다”고 공식 입장을 밝힌 바 없음

https://logowik.com/radix-ui-logo-vector-71367.html

 

그러나 실제로는 다음과 같은 정황들이 Radix 팀이 문제를 인지하고 있음을 보여준다고 생각한다.

 

1. Issue #1577

2023년 7월, Radix 팀원(@radix-ui-team)이 직접 다음과 같이 언급:

“Thanks for the detailed report! We’re going to investigate this further.”

 

→ 문제를 인지했고, 내부적으로 개선을 검토하겠다는 입장을 표명함.

 

2. Issue #1241

해당 이슈는 2022년 초기에 “Fixed”로 처리되었으나, 2025년 3월 커뮤니티 사용자가 다음과 같이 언급:

"Update of March 2025: This bug seems to be still appearing in newer versions.”

 

→ 패치 후에도 동일한 증상이 최신 버전에서 여전히 재현되고 있음이 확인됨.

 

3. Issue #1935

2025년 6월 기준, 여전히 Open 상태로 남아 있으며, 커뮤니티에서 

“still facing pointer-events lock with nested dialogs”

 

등의 피드백이 지속적으로 달리고 있음.

 

끝내는 말

이 문제는 실제 프로덕션 환경에서 사용자 경험을 크게 해치는 심각한 버그지만, 문제의 원인을 정확히 파악하고 해결 과정을 통해 Radix UI나 Shadcn UI의 내부 동작에 대해 공부해볼 수 있는 좋은 기회였다고 생각한다.

 

비슷한 구조로 개발 중이라면 다음 체크리스트를 참고해보면 좋을 것이다.

 

[ 체크리스트 ]

  • DropdownMenu → Dialog → Close 플로우에서 클릭이 정상 작동하는가?
  • Dialog 닫힌 후 document.body.style.pointerEvents 값이 auto인가?
  • 여러 모달이 중첩될 때도 정상 동작하는가?
  • 모바일 환경에서도 동일한 문제가 없는가?

 

이런 세세한 버그 해결 경험이 쌓여야 더 견고한 프론트엔드 애플리케이션을 만들 수 있지 않을까? 싶다. 혹시 비슷한 문제를 겪고 있다면 이 해결책이 도움이 되길..!!!

 

 

참고: 이 이슈는 2025.06.18일, Radix UI v1.1.14 기준으로 작성되었으며, 향후 버전에서는 수정될 수 있습니다. 항상 최신 문서와 이슈 트래커를 확인해보시기 바랍니다.