Skip to content

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는 동일한 패턴(최신 상태를 참조하면서 불필요한 재실행은 피하는 패턴)을 더욱 깔끔한 형태로 제공한다.