Event Handlers in Refs
Callback이 바뀐다고 해서 useEffect를 다시 실행하고 싶지 않은 경우
해당 Callback을 useRef에 담아두고, 의존성에는 ref를 통해 Callback을 참조
Incorrect: re-subscribes on every render
tsx
function useWindowEvent(event: string, handler: (e) => void) {
useEffect(() => {
// window에 이벤트 리스너 등록
window.addEventListener(event, handler);
// 컴포넌트가 언마운트되거나 handler가 바뀌면 이전 리스너 제거
return () => window.removeEventListener(event, handler);
// event나 hanlder가 변경될 때마다 재실행
}, [event, handler]);
}Correct: stable subscription
tsx
function useWindowEvent(event: string, handler: (e) => void) {
// handler를 저장할 ref
const handlerRef = useRef(handler);
// handler가 바뀔 때마다 ref의 current 값을 최신 함수로 업데이트
useEffect(() => {
handlerRef.current = handler;
}, [handler]);
// 실제 이벤트 등록은 event이름이 바뀔 때만 수행
useEffect(() => {
// 실행 시점에 항상 최신 handlerRef.current를 호출하는 함수
const listener = e => handlerRef.current(e);
window.addEventListener(event, listener);
// 의존성 배열에 handler가 없으므로, 함수가 바뀌어도 리스너를 재등록하지 않음
return () => window.removeEventListener(event, listener);
}, [event]);
}Alternative: 최신 버전 React를 사용 중이라면 useEffectEvent를 사용
tsx
import { useEffectEvent } from 'react';
function useWindowEvent(event: string, handler: (e) => void) {
// handler를 이벤트 전용 함수로 래핑 - 항상 최신 상태 유지하며 참조값이 변하지 않음
const onEvent = useEffectEvent(handler);
useEffect(() => {
window.addEventListener(event, onEvent);
return () => window.removeEventListener(event, onEvent);
// 의존성 배열에 handler를 넣을 필요 없음
}, [event]);
}useEffectEvent는 동일한 패턴(최신 상태를 참조하면서 불필요한 재실행은 피하는 패턴)을 더욱 깔끔한 형태로 제공한다.