Sandcorp샌드코프 · 웹·AI 제품 개발

새 프로젝트 받는 중 · 갱신 2026.11sandcorp.co.krGitHub

← 팁

프론트·UI

팝오버·드롭다운·툴팁은 body 포털로 렌더한다

부모의 overflow·transform·z-index 쌓임 맥락에 갇히면 팝오버가 잘리거나 다른 요소에 가려진다. body에 렌더하고 위치는 직접 계산한다.

모바일에서 드롭다운이 카드 경계에서 잘리거나, z-index를 아무리 올려도 다른 요소 뒤에 가려진다.

팝오버를 여는 버튼 옆에 그리면 부모의 영향을 그대로 받는다. 부모에 overflow: hidden이 있으면 잘리고, transform이나 z-index가 있는 부모는 새 쌓임 맥락을 만들어 그 안의 z-index가 바깥과 비교되지 않는다.

팝오버는 document.body에 포털로 렌더하고, 위치는 여는 요소의 getBoundingClientRect()로 계산한다. 창 크기 변경, 스크롤(어느 컨테이너에서 일어나든 잡히게 캡처 단계로), 요소 크기 변경 때 다시 잡는다.

import { createPortal } from 'react-dom';
import { useLayoutEffect, useState, type RefObject, type ReactNode } from 'react';

function Popover({ anchor, children }: { anchor: RefObject<HTMLElement>; children: ReactNode }) {
  const [pos, setPos] = useState({ top: 0, left: 0 });

  useLayoutEffect(() => {
    const place = () => {
      const r = anchor.current!.getBoundingClientRect();
      setPos({ top: r.bottom + 4, left: Math.min(r.left, window.innerWidth - 280) });
    };
    place();
    window.addEventListener('resize', place);
    window.addEventListener('scroll', place, true); // 캡처: 안쪽 스크롤도 잡는다
    return () => {
      window.removeEventListener('resize', place);
      window.removeEventListener('scroll', place, true);
    };
  }, [anchor]);

  return createPortal(
    <div className="popover" style={{ position: 'fixed', ...pos }}>{children}</div>,
    document.body,
  );
}

챙길 것: 화면 가장자리에서는 반대쪽으로 뒤집고 화면 안으로 끌어들인다. 바깥 클릭으로 닫을 때는 포털 내부와 여는 버튼을 함께 빼야 한다(DOM 트리가 떨어져 있다). 모바일 폭에서는 아예 바텀 시트로 바꾸는 쪽이 더 안전하다.

문의 →