바깥 클릭 감지를 useEffect 없이 ref callback 하나로 만든다.

언제: React 19 이상일 때. 18 이하는 아래 함정을 보고 useEffect 쪽을 쓴다.

절차

function useClickOutside(handler) {
  const handlerRef = useRef(handler)
  handlerRef.current = handler // stale closure 방지

  return useCallback((node) => {
    const handleClick = (e) => {
      if (!node.contains(e.target)) handlerRef.current()
    }

    document.addEventListener('mousedown', handleClick)
    return () => document.removeEventListener('mousedown', handleClick)
  }, [])
}

// 사용
function Modal({ onClose }) {
  const ref = useClickOutside(onClose)
  return <div ref={ref}>...</div>
}

확인: 모달을 여러 번 열고 닫은 뒤 바깥을 클릭한다. 핸들러가 한 번만 불리면 cleanup이 돌고 있는 것이다 — 안 돌면 옛 리스너가 쌓여 여러 번 불린다.

함정

  • ref callback은 DOM 노드 참조만 전달한다. 안에서 등록한 리스너 같은 사이드이펙트는 자동 해제되지 않는다.
  • React 18 이하에서는 이 패턴이 안 된다. unmount 시 callback(null)은 오지만 동일 함수 참조가 없어 removeEventListener를 못 부른다 → 리스너를 별도 ref에 저장해야 한다. cleanup return을 공식 지원하는 건 React 19부터다.

참고