removeEventListener를 불렀는데 핸들러가 계속 살아 있던 이유
문제 발생
컴포넌트를 정리하면서 리스너를 제거했는데 계속 호출됐습니다.
window.addEventListener("resize", () => recalc());
// ...
window.removeEventListener("resize", () => recalc()); // 제거되지 않는다화면을 몇 번 오가자 리스너가 수십 개 쌓여 스크롤이 눈에 띄게 느려졌습니다.
원인 분석
두 화살표 함수는 서로 다른 객체입니다. 내용이 같아도 참조가 다르므로, 제거 요청은 "등록된 적 없는 리스너"를 지우려는 시도가 됩니다.
MDN이 규칙을 명확히 적습니다 — 리스너를 제거하려면 추가할 때 사용한 것과 정확히 같은 함수 참조를 넘겨야 하고, 익명 함수는 넘길 참조가 없어 제거할 수 없습니다.
capture 설정도 일치해야 합니다 — 문서 예시대로 true로 등록한 것을 false로 제거하려 하면 실패합니다. 반면 passive 같은 다른 옵션은 일치할 필요가 없고, 매칭에는 capture만 관여합니다.
해결 방안
- 참조를 유지합니다.
const onResize = () => recalc();
window.addEventListener("resize", onResize);
window.removeEventListener("resize", onResize);- 더 나은 방법은
AbortSignal입니다. 문서가 소개하는 방식으로, 참조를 저장하지 않고도 제거할 수 있습니다 — 리스너가 여러 개일 때 특히 편합니다.
const controller = new AbortController();
window.addEventListener("resize", () => recalc(), { signal: controller.signal });
window.addEventListener("scroll", () => sync(), { signal: controller.signal });
controller.abort(); // 둘 다 한 번에 해제-
한 번만 필요한 리스너에는
once: true를 씁니다. 실행 후 자동으로 제거되므로 정리 코드 자체가 필요 없습니다. -
프레임워크의 정리 훅에 반드시 넣습니다. React의
useEffect반환값, Vue의onUnmounted가 그 자리입니다 — 여기서 빠뜨린 리스너는 화면을 오갈 때마다 누적됩니다. -
누수를 눈으로 확인합니다. DevTools의 Event Listeners 패널에서 같은 이벤트가 몇 개 붙어 있는지 보면, 이 문제는 추측 없이 드러납니다.
-
capture를 쓰는 코드는 특히 주의합니다. 등록과 해제 양쪽에 같은 값을 명시적으로 적어두면 나중에 헷갈리지 않습니다.
댓글0
댓글을 남기려면 로그인이 필요해요. 로그인
아직 댓글이 없어요. 첫 의견을 편하게 남겨 보세요.