
React의 useEffectEvent 완벽 가이드: useEffectEvent 이해와 코딩 경험의 변화
React는 이벤트 리스너, 구독(subscription), 그리고 오래 실행되는 콜백(long-lived callback) 작업을 단순화하기 위해 useEffectEvent라는 새로운 훅(hook)을 도입했습니다. 이를 통해 불필요한 이펙트(effect) 재실행 없이 최신 상태(state)에 접근할 수 있습니다.
React 함수형 컴포넌트는 렌더링될 때마다 클로저를 재생성합니다.
useEffect 내부에서 props나 state를 사용하는 콜백을 정의하면, 그 콜백은 렌더링 시점의 값을 캡처합니다. 이로 인해 이벤트 핸들러가 예전 데이터를 바라보는 오래된 클로저(stale closures) 문제가 자주 발생합니다.
전통적으로 개발자들은 다음 중 하나를 선택해야 했습니다.
useEffectEvent는 이를 사용하는 이펙트의 의존성이 되지 않으면서도, 항상 최신 값을 바라보는 안정적인 콜백을 생성할 수 있게 하여 이 문제를 해결합니다.
다음 예시를 봅시다.
function Example() {
const [count, setCount] = useState(0);
useEffect(() => {
const handler = () => {
console.log('Current count:', count);
};
window.addEventListener('click', handler);
return () => window.removeEventListener('click', handler);
}, []); // ⛔ 빈 의존성 배열
}
count가 변경된 후에 클릭을 해도, 로그에는 항상 초기값만 찍힙니다.
핸들러가 첫 번째 렌더링의 클로저에 묶여있기 때문입니다.
개발자들은 보통 다음 3가지 패턴 중 하나를 사용했습니다.
useEffect(() => {
const handler = () => {
console.log('Current count:', count);
};
window.addEventListener('click', handler);
return () => window.removeEventListener('click', handler);
}, [count]); // 👈 deps에 count 추가
count가 바뀔 때마다 이벤트 리스너가 제거되고 다시 추가됨 (성능에 나쁨, 특히 빈번한 이벤트의 경우)const countRef = useRef(count);
useEffect(() => {
countRef.current = count;
}, [count]);
useEffect(() => {
const handler = () => {
console.log('Current count:', countRef.current);
};
window.addEventListener('click', handler);
return () => window.removeEventListener('click', handler);
}, []);
이것이 가장 널리 사용되던 우회 방법이었습니다.
많은 팀들이 useEffectEvent를 흉내 낸 커스텀 훅을 사용했습니다.
function useEventCallback(fn) {
const ref = useRef(fn);
useEffect(() => {
ref.current = fn;
});
return useCallback((...args) => ref.current(...args), []);
}
import { useEffectEvent } from 'react';
useEffectEvent를 사용하면 다음과 같은 콜백을 정의할 수 있습니다.
function Example({ user }) {
const onResize = useEffectEvent(() => {
console.log('Current width:', window.innerWidth);
console.log('Current user:', user.name);
});
useEffect(() => {
const handler = () => onResize();
window.addEventListener('resize', handler);
return () => window.removeEventListener('resize', handler);
}, []); // 👈 한 번만 실행됨
}
onResize는 항상 최신 user를 바라봄useEffectEvent(fn)는 마치 다음과 같이 동작합니다.
const ref = useRef(fn);
ref.current = fn;
const stableCallback = useCallback((...args) => ref.current(...args), []);
하지만 React가 타이밍, 업데이트, 그리고 렌더링 모델과의 통합을 대신 처리해주어 안전하고 일관성 있게 동작합니다.
resize, scroll 등) =WHY=> 리스너는 한 번만 추가하고, 콜백은 항상 최신 상태 유지setTimeout, setInterval) =WHY=> 콜백이 나중에 실행되지만 최신 상태를 사용해야 함useEffect(() => {
const handleVisibilityChange = () => {
if (document.visibilityState === 'visible' && connectionState === 'disconnected' && enabled) {
if (!connectionRef.current) {
createAndStartConnection();
} else {
connectionRef.current
.start()
.then(() => {
setConnectionState('connected');
resetInactivityTimer();
})
.catch((err) => {
console.error('Reconnect failed:', err);
setConnectionState('error');
});
}
}
};
window.addEventListener('visibilitychange', handleVisibilityChange);
return () => window.removeEventListener('visibilitychange', handleVisibilityChange);
}, [connectionState, enabled, createAndStartConnection, resetInactivityTimer]);
문제점:
✅ useEffectEvent로 리팩토링:
const handleVisibilityChangeEvent = useEffectEvent(() => {
if (document.visibilityState === 'visible' && connectionState === 'disconnected' && enabled) {
if (!connectionRef.current) {
createAndStartConnection();
} else {
connectionRef.current
.start()
.then(() => {
setConnectionState('connected');
resetInactivityTimer();
})
.catch((err) => {
console.error('Reconnect failed:', err);
setConnectionState('error');
});
}
}
});
useEffect(() => {
const handler = () => handleVisibilityChangeEvent();
window.addEventListener('visibilitychange', handler);
return () => window.removeEventListener('visibilitychange', handler);
}, []); // 👈 한 번만 실행됨
useEffectEvent는 콜백을 내부 ref에 저장합니다. 매 렌더링마다 해당 ref를 최신 콜백으로 업데이트합니다. 이벤트 리스너를 부착하는 이펙트는 새로운 의존성을 보지 못하므로 다시 실행되지 않습니다. 이벤트가 발생하면 React는 최신 콜백을 조회하여 실행합니다. 이를 통해 백그라운드 시스템(DOM이나 WebSocket 이벤트 등)은 안정적으로 유지되면서 UI 상태는 자유롭게 변경될 수 있습니다.
useEffectEvent 사용하기useEffectEvent 섞어 쓰기useEffectEvent 콜백 안으로 옮기세요.useEffectEvent는 useEffect의 대체제가 아니라 동반자입니다. 이펙트 재실행을 유발하지 않으면서 이벤트 핸들러 내에서 최신 상태를 얻을 수 있게 해줍니다.
이벤트 리스너, 구독, 타이머와 같은 백그라운드의 오래 지속되는 동작에 이상적입니다.
이는 ref + callback 편법을 깔끔한 공식 API로 대체합니다. 성능, 가독성, 정확성을 향상시킵니다.
useEffectEvent 이전에는 개발자들이 콜백을 최신으로 유지하기 위해 ref 우회법이나 의존성 조작에 의존했습니다. 이제 React는 이를 깔끔하게 처리할 수 있는 일급(first-class) API를 제공합니다.
👉 만약 당신이:
useEffectEvent가 바로 당신에게 필요한 것입니다.