
웹 개발에서 DOM 이벤트는 빠질 수 없는 필수 요소입니다. 클릭, 키보드 입력, 마우스 이동 등 대부분의 사용자 상호작용은 addEventListener()를 통해 감지하고 처리하게 되죠. 이 메서드는 자바스크립트에서 이벤트를 등록하는 가장 기본적인 방식으로, 거의 모든 UI에서 사용됩니다.
하지만 자주 쓰이는 만큼, 그만큼 쉽게 간과되는 문제가 있습니다. 바로 ‘메모리 누수(memory leak)’입니다. 이벤트 핸들러를 잘못 관리하면 메모리에 불필요한 참조가 계속 남게 되고, 이로 인해 브라우저 성능이 점점 저하되는 상황이 발생할 수 있습니다. 특히 React나 Vue와 같은 라이브러리, 프레임워크의 추상화 없이 직접 DOM을 다루거나, 서드파티 컴포넌트를 사용할 때 이런 문제가 더욱 뚜렷하게 나타납니다.
이번 글에서는 addEventListener가 메모리 관리와 어떤 연관이 있는지, 우리가 흔히 저지르는 실수는 무엇인지, 그리고 실무에서 어떻게 이를 방지할 수 있는지를 실제 코드와 함께 자세히 살펴보겠습니다.
자바스크립트에서 이벤트 핸들러는 단순한 콜백 함수가 아닙니다. addEventListener()로 등록하는 순간부터 브라우저는 해당 함수를 메모리에 유지합니다. 이벤트가 실제로 발생하든 말든, 핸들러는 계속 메모리에 남아있는 상태죠. 이 자체는 문제가 되지 않지만, 핸들러 내부에서 클로저를 통해 외부 변수를 참조하고 있다면 상황은 달라집니다. 아래 예제를 살펴보겠습니다.
function setup() {
const largeData = new Array(1000000).fill("????");
document.getElementById("btn").addEventListener("click", () => {
console.log(largeData[0]);
});
}
이 코드에서 이벤트 핸들러는 largeData를 참조하고 있기 때문에, 브라우저는 해당 데이터를 메모리에서 수거하지 못합니다. setup() 함수는 종료되었지만, largeData는 여전히 이벤트 핸들러 내부에서 접근 가능한 상태입니다.
이런 구조는 싱글 페이지 애플리케이션(SPA)처럼 한 번 로드된 후 계속 동작하는 앱에서 특히 자주 발생합니다. 페이지 전환이 반복되고, 이벤트가 누적되면 전체 메모리 사용량이 점차 증가하게 됩니다.
많은 개발자들이 착각하는 부분이 바로 이 지점입니다. 이벤트를 제거하려고 removeEventListener()를 호출했는데, 여전히 이벤트가 살아있는 경험을 해보신 적 있을 겁니다. 이건 버그가 아니라 함수 참조에 대한 자바스크립트의 기본 동작 때문인데요, 다음 코드를 통해 살펴보겠습니다.
element.addEventListener("click", () => console.log("clicked"));
element.removeEventListener("click", () => console.log("clicked")); // 제거 안 됨
언뜻 보기엔 동일한 코드지만, 이 두 화살표 함수는 서로 다른 메모리 주소를 가진 전혀 다른 함수입니다. 자바스크립트에서 함수는 일급 객체이므로, 동일한 코드라도 새로운 함수 객체가 생성됩니다. 결국 removeEventListener는 처음에 등록한 함수와 일치하지 않는 참조를 전달받게 되고, 핸들러는 제거되지 않습니다. 이 문제는 다음과 같이 명시적인 함수 참조를 사용하는 것으로 해결할 수 있습니다.
function handleClick() {
console.log("clicked");
}
element.addEventListener("click", handleClick);
element.removeEventListener("click", handleClick); // 정상 작동
실무에서는 이벤트 등록과 해제가 서로 다른 시점에서 이뤄지기 때문에, 익명 함수보다는 이름이 있는 함수나, 변수에 저장된 함수를 사용하는 것이 안전합니다. 나아가 여러 핸들러를 체계적으로 관리하기 위해 Map이나 WeakMap을 활용하는 방식도 자주 사용됩니다.
const handlerMap = new Map();
function addHandler(el, key, fn) {
el.addEventListener("click", fn);
handlerMap.set(key, fn);
}
function removeHandler(el, key) {
const fn = handlerMap.get(key);
if (fn) {
el.removeEventListener("click", fn);
handlerMap.delete(key);
}
}
addHandler(button, "confirm", () => console.log("확인 클릭"));
removeHandler(button, "confirm"
이 구조는 특히 컴포넌트 기반 UI나 커스텀 라이브러리에서 매우 유용합니다. 이벤트를 등록할 때마다 함수 참조를 Map에 저장해두면, 나중에 동일한 키로 접근해 정확한 핸들러를 제거할 수 있기 때문입니다.
이벤트 핸들러의 메모리 누수를 방지하기 위한 첫걸음은, 이벤트를 등록하는 순간부터 ‘언제 해제할지’를 함께 고민하는 것입니다. 아무리 짧은 코드라도, 익명 함수로 등록된 핸들러는 정확히 제거하기 어렵습니다. 가능한 경우에는 함수 선언문이나 화살표 함수를 변수에 저장해두고 재사용하는 것이 바람직합니다.
또한 DOM 요소가 삭제되었다고 해서 이벤트 핸들러도 함께 제거되는 것은 아닙니다. 다음과 같은 코드를 보면 그 차이를 쉽게 이해할 수 있습니다.
const handler = () => console.log("clicked");
button.addEventListener("click", handler);
button.remove(); // DOM에서 제거됨
// 하지만 메모리에는 여전히 handler가 남아 있음
이러한 경우, 핸들러는 여전히 메모리에 남아 있으며, 참조가 존재하기 때문에 GC가 회수하지 못하게 됩니다. 따라서 removeEventListener를 명시적으로 호출해 주는 것이 안전합니다.
다행히도 자바스크립트는 once: true라는 옵션을 제공하는데요. 이 옵션을 설정하면 이벤트가 한 번만 실행되고 자동으로 제거되기 때문에, 일회성 이벤트에는 유용한 패턴이 됩니다.
button.addEventListener("click", handleClick, { once: true });
추가로 WeakMap을 사용하면 DOM 요소가 GC에 의해 수거될 때, 그에 연결된 핸들러도 자연스럽게 메모리에서 해제되는 구조를 만들 수 있습니다.
const handlerMap = new WeakMap();
function addHandler(el, fn) {
handlerMap.set(el, fn);
el.addEventListener("click", fn);
}
function removeHandler(el) {
const fn = handlerMap.get(el);
if (fn) el.removeEventListener("click", fn);
}
실제로 메모리 누수가 발생했는지 확인하려면 브라우저의 개발자 도구를 활용해야 합니다. Chrome의 Memory 탭에서 힙 스냅샷을 비교하거나, Performance Monitor를 통해 Event Listeners 수를 추적하면 유용한 힌트를 얻을 수 있습니다.
특히 Detached DOM Tree처럼, DOM에서 제거되었지만, 여전히 메모리에 남아있는 노드를 발견했다면 의심해 볼 필요가 있습니다.
또한 SPA 환경에서 라우팅이 반복되는 앱이라면, 라우팅 전에 기존 이벤트 핸들러를 모두 제거하는 루틴을 추가해 두는 것도 좋은 예방책입니다.
addEventListener()는 우리가 너무나 자주 사용하는 메서드입니다. 하지만 그만큼 방심하기 쉬운 도구이기도 하죠. 핸들러 하나쯤이야, DOM 하나쯤이야 하는 생각은 결국 장기적인 성능 저하와 직결됩니다.
중요한 것은 습관입니다. 이벤트를 등록할 때는 ‘언제, 어떤 방식으로 해제할 것인지’를 항상 함께 고민해야 합니다. 명시적인 참조 관리, 해제 로직의 설계, 적절한 도구의 활용이야말로 클린한 UI, 유지보수 가능한 코드의 기본이 될 수 있습니다. 지금 이 순간에도 브라우저 메모리 어딘가에는 제거되지 않은 이벤트 핸들러가 남아 있을지도 모릅니다. 이 글을 읽고 나서, 하나씩 지워보도록 합시다.
©️요즘IT의 모든 콘텐츠는 저작권법의 보호를 받는 바, 무단 전재와 복사, 배포 등을 금합니다.