Velog
리액트가 마침내 가장 큰 문제를 해결했습니다 (useEffectEvent)
useEffectEvent 는 '이펙트 내부에서 호출되지만 의존성 배열에 들어가지 않아야 할 함수' 를 위한 전용 훅이다. useRef 우회와 useCallback 의 수년간의 패치 레이어가 정리된다.
핵심 요약
useEffect 에서 함수를 호출할 때 개발자들은 두 가지 속성을 동시에 원했다. (1) 그 함수 안에서는 최신 props/state 를 보고 싶다, (2) 그 함수가 바뀌어도 이펙트가 재시작되면 안 된다. 이 두 요구는 의존성 배열 시스템과 정면 충돌했고 useRef, useCallback, ESLint exhaustive-deps disable 등 수년간 누더기 해법이 쌓였다. useEffectEvent 는 이 충돌을 해소하는 전용 훅이다. 훅으로 감싼 함수는 (a) 이펙트 내부에서 호출되면 항상 최신 렌더의 클로저 를 본다, (b) 의존성 배열에 포함하면 안 된다(React 가 문법적으로 막는다). 실전에서는 'socket 연결은 roomId 에만 반응하고, 메시지 수신 시엔 최신 theme 을 반영' 같은 패턴을 10줄짜리 코드로 압축한다.
리액트 이펙트 문제를 '반응 이유(reactive dependency)' 와 '읽기 전용 값(latest value)' 두 축으로 본다. 의존성 배열에 들어갈 것은 '이 값이 바뀌면 이펙트를 다시 실행해야 하는가' 의 답이 Yes 인 것만. useEffectEvent 는 이 분리를 문법적으로 강제한다.
리액트 면접에서 'stale closure' 와 useEffect 의존성 은 단골. 과거의 답은 항상 어색했다.
- 'ref 에 담아서 쓴다' — 반응성을 포기
- 'ESLint 룰 disable 한다' — 의존성 버그의 온상
useCallback으로 wrap — 또 다른 의존성 문제
useEffectEvent 는 의도를 문법으로 표현한다. '이 함수는 이펙트 안에서 호출되지만, 이 함수가 바뀌어서 이펙트가 재실행되어서는 안 된다.'
학습 포인트
면접 답변으로 연결할 학습 포인트입니다.
반응 vs 읽기 — 훅으로 분리
useEffectEvent 의 핵심은 '언제 재실행하느냐' 와 '무엇을 읽느냐' 를 분리한 것.
function ChatRoom({ roomId, theme }) {
const onMessage = useEffectEvent((msg) => {
// 항상 최신 theme 을 본다
showNotification(msg, theme);
});
useEffect(() => {
const socket = createSocket(roomId);
socket.on('message', onMessage); // 의존성에 넣지 않는다
return () => socket.close();
}, [roomId]); // roomId 만 진짜 반응 의존
}
과거에는 theme 을 의존성에 넣으면 매번 소켓이 재연결되고, 빼면 stale theme 이었다.
useEffectEvent 로 감싼 함수를 자식 컴포넌트 prop 으로 내려주는 것. React 는 이를 에러 로 처리 — 용도는 오직 이펙트 내부 호출 이다.
useCallback 과의 차이
둘 다 '함수를 안정화' 하지만 목적이 다르다.
| 훅 | 언제 바뀌나 | 어디서 쓰나 | 최신 값 |
|---|---|---|---|
useCallback | deps 바뀔 때 | 자식 prop (메모이제이션) | deps 시점 클로저 |
useEffectEvent | 매 렌더 새로 | 이펙트 내부 호출 | 항상 최신 |
useCallback 은 참조 동일성 최적화, useEffectEvent 는 의존성 설계 도구. 헷갈리면 '자식에게 넘기나?' 로 판단 — 넘긴다면 useCallback, 이펙트에서만 쓴다면 useEffectEvent.
useEffectEvent 를 '더 좋은 useCallback' 으로 오해하는 것. 용도가 다르다.
마이그레이션 트리거
기존 코드에서 useEffectEvent 로 바꿔야 할 신호 세 가지:
useEffect의존성 배열에서 특정 값을 일부러 빼고// eslint-disable-next-line주석useRef를 선언하고 렌더마다ref.current = fn으로 덮어씌우는 패턴useCallbackdeps 가 매 렌더 바뀌어 실제로 캐시 효과가 없는 함수
이 세 패턴은 거의 그대로 useEffectEvent 로 정리된다.
모든 useCallback 을 바꾸려 드는 것. 자식 prop 용으로 쓰는 useCallback 은 그대로 둬야 한다.
읽는 순서
- 1이론
React 공식 문서 'Separating Events from Effects' 챕터를 읽고, '반응 의존성' vs '읽기 값' 테이블을 본인 코드의
useEffect5개에 적용해 분류. - 2구현
채팅 소켓 예제(
roomId로 연결,theme으로 알림) 를 세 가지 스타일로 구현 후 비교: (a)useRef, (b) deps 에 모두 넣기, (c)useEffectEvent. - 3실무
본인 프로젝트의
// eslint-disable-next-line react-hooks/exhaustive-deps를 모두 찾아useEffectEvent로 해소할 수 있는지 분류하고 PR 작성. - 4설명
팀에 '
useCallbackvsuseEffectEvent— 어떤 상황에 무엇을?' 플로차트 한 장을 공유.
면접 연결 질문
[좋은 답변] React 가 런타임 에러 — '이펙트 내부에서만 호출 가능' 제약을 강제. 이는 반응성 제거 함수가 렌더 트리 밖으로 새면 버그 유발이 확정적이기 때문. 자식에 내려주려면 useCallback 을 써야 한다.
[좋은 답변] ref 패턴은 수동이라 업데이트를 잊으면 stale, 그리고 tearing 가능성. useEffectEvent 는 React 가 매 렌더 후 자동 갱신 + 의존성 시스템과 명시적 계약 + ESLint 가 오용 감지 — 세 가지 모두 수동 해법으로는 불가능.
[좋은 답변] disable 한 의존성이 (1) 반응 필요 — 배열에 넣어라, (2) 값만 읽기 — useEffectEvent 로 감싸라. 중간은 없다. ESLint 주석은 '의도를 문법으로 표현하지 못한 흔적'.
자기 점검
모든 콜백에 만능으로 쓴다. 핵심은 이펙트 내부 전용.
ref 패턴이 항상 동일 대체 가능하다는 오해 — 자식에게 노출되는 ref 는 여전히 필요.