
웹 상단 네비게이션의 "Cart" 텍스트에 마우스를 올렸을 때, Popover UI 컴포넌트가 자연스럽게 열리고, 모바일에서도 터치로 동작하며, Popover UI 내부로 마우스가 이동했을 때도 닫히지 않도록 구현합니다.
또한 바깥을 클릭하면 Cart가 닫히는 동작도 추가하여 UX를 완성합니다.
onMouseEnter, onMouseLeave) 기반으로 Popover를 열고 닫음onTouchStart)로 toggle함mouseleave가 발생해서 Popover가 닫혀버릴 수 있음setTimeout과 clearTimeout을 사용하여 약간의 유예 시간을 줌mousedown)로 닫기 처리matchMedia('(pointer: coarse)')useEffect(() => {
if (typeof window !== 'undefined') {
setIsMobile(window.matchMedia('(pointer: coarse)').matches);
}
}, []);
//타입스크립트에서 setTimeout의 반환 타입은 환경에 따라 다르기 때문에 ReturnType을 사용함
const closeTimeout = useRef<ReturnType<typeof setTimeout> | null>(null);
const handleMouseLeave = (e: React.MouseEvent) => {
if (isMobile) return;
closeTimeout.current = setTimeout(() => {
// 마우스가 Popover 내부에 있었고, Popover 내부에 있지 않을거라면 (외부로 이동한다면) 닫기
const target = e.relatedTarget;
if (
cartRef.current &&
(!target ||
!(target instanceof Node) ||
!cartRef.current.contains(target))
)
setIsCartOpen(false);
}, 100);
};
const openCart = () => {
if (closeTimeout.current) clearTimeout(closeTimeout.current);
setIsPopoverOpen(true);
};
100ms 후에 닫기를 예약clearTimeout으로 닫기 예약을 취소함
e.relatedTarget에 대하여
onMouseLeave나onMouseOut이벤트에서만 존재하는 속성- 위 두 이벤트는 두 요소 사이의 마우스 이동 순간에 발생함
- 따라서,
e.currentTarget: 지금 떠나는 요소e.relatedTarget: 지금 이동한 대상 요소- 예시) 마우스가
Trigger에서 →Popover로 이동 중이라면e.relatedTarget은Popover내부 요소 중 하나가 됨
useEffect(() => {
if (!isCartOpen) return;
const handleClickOutside = (e: MouseEvent) => {
if (
popoverRef.current &&
!popoverRef.current.contains(e.target as Node) &&
popoverTriggerRef.current &&
!popoverTriggerRef.current.contains(e.target as Node)
) {
setIsPopoverOpen(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, [isPopoverOpen]);
mousedown 이벤트 등록pointer: coarse 여부를 체크해 모바일인지 판별 → isMobile 상태 설정setIsPopoverOpen(true)