const [isDragging, setIsDragging] = useState(false); const handleMouseUp = (event: MouseEvent) => { if (isDragging) { setIsDragging(false); onMouseUp(event); } }; useEffect(() => { window.addEventListener('mouseup', handleMouseUp); return () => window.removeEventListener('mouseup', handleMouseUp); }, []);
handleMouseUp
이벤트 핸들러를 window 전역 객체에 등록하는 간단한 코드이지만 정상적으로 동작하지 않습니다.
isDragging
이 변경되어 리랜더링 되면 handleMouseUp
은 참조가 바뀌게되고
useEffect 안에서 window 전역 객체가 변경되기 전 handleMouseUp
을 이벤트 핸들러로 등록하고 있기 때문에 발생하는 문제입니다.useEffect(() => { window.addEventListener('mouseup', handleMouseUp); return () => window.removeEventListener('mouseup', handleMouseUp); }, [isDragging]);
간단하게
isDragging
을 useEffect 종속성 배열에 넣으면 isDragging이 변경될 때마다 이벤트 핸들러도 같이 재등록시키는 방법이 있습니다.그렇다면 이벤트 핸들러를
useCallback
으로 감쌌을때는 어떻게 동작할까요?const handleMouseUp = useCallback((event: MouseEvent) => { if (isDragging) { setIsDragging(false); onMouseUp(event); } }, []); useEffect(() => { window.addEventListener('mouseup', handleMouseUp); return () => window.removeEventListener('mouseup', handleMouseUp); }, [isDragging]);
이 코드도 정상적으로 동작하지 않습니다.
useEffect 안에서 isDragging의 값이 변하여 이벤트 핸들러를 재등록 하더라도 이벤트 핸들러 자체가 변하지 않기 때문입니다.
이 말인즉슨,
handleMouseUp
안의 isDragging은 밖에서 무슨 일이 일어나든 초기값을 가진다는 의미입니다.이 문제를 해결하는 방법도 간단합니다.
const handleMouseUp = useCallback((event: MouseEvent) => { if (isDragging) { setIsDragging(false); onMouseUp(event); } }, [isDragging]); useEffect(() => { window.addEventListener('mouseup', handleMouseUp); return () => window.removeEventListener('mouseup', handleMouseUp); }, [handleMouseUp]);
위와 같이
handleMouseUp
이벤트 핸들러는 useCallback의 종속성 배열에 isDragging
을 넣고,useEffect의 종속성 배열에는 isDragging의 변경으로 바뀐 handleMouseUp을 넣어주면 깔끔쓰하게 해결됩니다.
정리
- JSX에 직접 넣는 이벤트 핸들러(onClick 등) 말곤 전부 종속성 관리를 해줘야한다.
- useCallback으로 감싸진 이벤트 핸들러는 자체적으로 종속성 관리를 하되 만일 useEffect에서 dom 이벤트 리스너에 등록한다면 useCallback 함수를 useEffect의 종속성으로 넣어야한다.