우선 리액트 플젝에서 three dots 아이콘을 누르면 context menu가 랜더링 되도록하고 이후 menu의 바깥 쪽을 클릭하면 없어지게 하기 위하여 useUnmountIfClickedOutside
(이하 커스텀 훅) 를 이용하였다.
그런데 위의 커스텀 훅에서 이벤트 등록할때 캡쳐링 phase일때만 실행하도록 허용하였는데 왜 허용해야 하는지 알아보자
우선 코드부터보자
// ContextMenu.tsx
const ContextMenu = ({
contextMenuOptions,
position,
setContextMenuVisible,
}: IContextMenu) => {
const $contextMenu = useRef<HTMLDivElement>(null);
const [suitableXPos, setSuitableXPos] = useState(-100);
useUnmountIfClickedOutside({
ref: $contextMenu,
callback: () => setContextMenuVisible(false),
});
....
// useUnmountIfClickedOutside.tsx
export const useUnmountIfClickedOutside = ({
ref,
callback,
}: IUseUnmountIfClickedOutside) => {
const handleClick = useCallback(
(e: React.BaseSyntheticEvent | MouseEvent) => {
console.log("e.target", e.target);
console.log("ref.current", ref?.current);
if (!ref) return;
if (ref.current && !ref.current.contains(e.target)) {
callback();
}
},
[ref, callback],
);
useEffect(() => {
document.addEventListener("click", handleClick, true);
return () => {
document.removeEventListener("click", handleClick, true);
};
}, [handleClick]);
};
export default useUnmountIfClickedOutside;
// Header.tsx
...
const [isContextMenuVisible, setIsContextMenuVisible] = useState(false);
console.log("isContextMenuVisible", isContextMenuVisible);
const showContextMenu = (e: React.MouseEvent<HTMLElement>) => {
console.log("go!");
setIsContextMenuVisible(true);
};
<BsThreeDotsVertical
className="cursor-pointer text-xl text-panel-header-icon"
onClick={showContextMenu}
/>
</div>
{isContextMenuVisible && (
<ContextMenu
contextMenuOptions={contextMenuOptions}
position={menuCoord}
setContextMenuVisible={setIsContextMenuVisible}
/>
)}
...
커스텀 훅을 보면 addEventListener
의 세번째 인자에 true가 pass된것을 알 수 있다. 이는 캡쳐링일때만 이벤트가 실행되는 것을 의미한다.
왜 캡쳐링을 해야할까? 캡쳐링을 끄고 실행해보았다.
menu가 보이지 않는다. 아니 정확히는 보였다가 바로 unmount되는 것이다. 왜?
콘솔로그를 보면 알 수 있다.
ChatHeader.tsx:32 go!
ChatHeader.tsx:29 isContextMenuVisible true
installHook.js:251 isContextMenuVisible true
useUnmountIfClickedOutside.ts:14 e.target <path d="M9.5 13a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0zm0-5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0zm0-5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0z"></path>
useUnmountIfClickedOutside.ts:15 ref.current <div class="fixed z-[100] bg-dropdown-background" style="top: 29px; left: -100px;">…</div>
ChatHeader.tsx:29 isContextMenuVisible false
three dots을 누르는 순간 isContextMenuVisible가 true로 바뀌고 커스텀 훅안에 있는 이벤트 핸들러 함수가 실행된다. 이후 isContextMenuVisible가 false로 바뀌기 때문.
왜 이벤트 핸들러 함수가 바로 실행이 되는 걸까?
먼저, 아래 그 유명한 이벤트 phase 사진을 보자
캡쳐링이 먼저 실행되고 버블링이 실행된다. 근데 리액트에 적용되는 이벤트는 default가 버블링이다. 그래서 커스텀 훅도 버블링일때만 이벤트가 실행이 된다면, 아래와 같은 과정을 거칠것이라고 생각된다.
three dots을 누름
event phase에 의해 capture -> target -> bubbling
순으로 실행.(bubbling phase일때 three dots에 등록되어있는 이벤트가 트리거 됨)
three dots에 등록된 이벤트가 실행됨
context menu가 랜더링됨
bubbling에 의해 이벤트 파도(?)가 html 태그까지 올라가면서 커스텀훅에 의해 등록된 handleClick을 실행하여 context menu가 바로 없어짐. (참고로 커스텀훅은 document에 이벤트를 등록해두었슴.
three dots 클릭 => context menu 랜더링 => 버블링에 의해 document등록 된, 즉 커스텀 훅에 넘겨준 callback이 실행됨)
그럼 만약 capturing일때만 이벤트가 실행되도록하면 어떻게 될까?
three dots에 등록된 이벤트는 버블링일때 일어나고 커스텀훅을 통해 등록된 document는 캡쳐링이다. 아예 event가 다니는 길이 다르다고 보면된다.
그래서 three dots에 등록된 이벤트에 의해 발생하는 버블링이 커스텀 훅에 의해 등록된 이벤트를(캡쳐링) 트리거 하지 않게 된다.
즉, 커스텀함수에 넘겨준 콜백이 곧장 실행되지 않는다.
context menu가 랜더링 된 이후에 발생하는 클릭에서만 실행이 되므로 의도한 대로 동작하게 되는 것이다.