portal 기반의 Tooltip을 구현하던 중,
모달 내부에서 스크롤할 때 툴팁 위치가 갱신되지 않는 문제가 발생했습니다.
window.addEventListener("scroll", updatePosition);
하지만 모달 내부 스크롤은 감지되지 않았습니다.
왜 그럴까요?
일반적으로 click, keydown 등의 이벤트는
자식 요소에서 발생해도 부모로 버블링(bubbling) 됩니다.
하지만 scroll 이벤트는 버블링되지 않습니다.
즉,
모달 내부 스크롤은 window까지 올라오지 않기 때문에
window.addEventListener('scroll')로는 감지되지 않습니다.
이럴 때는 캡처링(capturing) 단계를 활용해야 합니다.
이벤트가 “아래로 내려가는 과정”에서 미리 감지할 수 있습니다.
window.addEventListener("scroll", updatePosition, {
capture: true, // 캡처링 단계에서 감지
});
즉,
실제로 스크롤이 일어나기 전에 window가 먼저 이벤트를 받는 구조가 됩니다.
scroll 이벤트는 매우 자주 발생하는 이벤트입니다.
기본적으로 브라우저는 스크롤을 처리할 때마다
preventDefault()가 호출될 가능성이 있는지 판단하기 위해 잠시 대기합니다.
하지만 Tooltip은 단지 스크롤을 막지 않고, 위치를 재계산하기 위한 목적으로만 사용되죠.
이럴 땐 passive: true 옵션을 추가해 성능을 개선할 수 있습니다.
window.addEventListener("scroll", updatePosition, {
passive: true, // 스크롤을 막지 않겠다는 약속 → 더 부드럽고 빠름
capture: true, // 모든 스크롤을 최상위에서 감지
});
✅ 이 두 옵션을 함께 사용하면,
모달 내부 스크롤 / 외부 스크롤 모두 빠르고 자연스럽게 감지할 수 있습니다.
Tooltip의 위치를 올바르게 계산하려면,
트리거 요소(예: 버튼)의 크기 변화도 감지해야 합니다.
이때 사용할 수 있는 API가 바로 ResizeObserver입니다.
DOM 요소의 크기가 변할 때마다 콜백을 호출해주는 API입니다.
| 구분 | 설명 |
|---|---|
window.resize | 브라우저 뷰포트 전체 크기 변화 감지 |
ResizeObserver | 특정 DOM 요소의 크기 변화 감지 |
아래는 Tooltip의 위치를 자동으로 갱신하는 코드 예시입니다.
const triggerRef = useRef<HTMLElement | null>(null);
useLayoutEffect(() => {
const scheduleUpdate = () => {
// Tooltip 위치 재계산 로직
};
// 스크롤 / 리사이즈 감지
window.addEventListener("scroll", scheduleUpdate, { passive: true, capture: true });
window.addEventListener("resize", scheduleUpdate);
// trigger 크기 변경 감지 (스로틀링 가능)
const ro = new ResizeObserver(scheduleUpdate);
if (triggerRef.current) ro.observe(triggerRef.current);
return () => {
window.removeEventListener("scroll", scheduleUpdate, { capture: true });
window.removeEventListener("resize", scheduleUpdate);
ro.disconnect(); // 반드시 해제
};
}, [open, position, align, color, offset]);
💡
ResizeObserver는 DOM 변경마다 콜백을 호출하므로,
필요 시 스로틀(throttle) 또는 디바운스(debounce) 적용이 권장됩니다.
| 항목 | 역할 | 비고 |
|---|---|---|
capture: true | 캡처링 단계에서 모든 스크롤 감지 | 버블링되지 않는 scroll 이벤트 해결 |
passive: true | 성능 최적화 (preventDefault() 비활성화) | 부드러운 스크롤 |
ResizeObserver | 특정 요소의 크기 변화 감지 | window.resize보다 세밀한 감지 |
capture: truepassive: trueResizeObserver이 세 가지를 함께 사용하면
Tooltip, Popover, Dropdown 같은 포털 기반 컴포넌트의 위치 계산 문제를
안정적이고 부드럽게 해결할 수 있습니다.
Velog에 그대로 붙여넣으면 코드 블록, 표, 강조 표시 모두 자연스럽게 렌더링됩니다 ✅