프론트 개발을 하다가 이벤트 리스너가 걸려있는 요소를 제거해야 할 일이 생겼다. 요소를 제거하기 전에 그 요소에 등록된 이벤트 리스너를 제거해야 할까? 결론부터 말하자면, "제거하면 좋지만 굳이 제거할 필요는 없다."이다. 제거하면 좋지만 굳이 제거할 필요가 없다니, 이게 무슨 말인가? 이를 이해하기 위해 이벤트 리스너를 등록하게 되면 어떤 일이 일어나는지 살펴보도록 하자.
자바스크립트에서 어떤 요소에 대한 이벤트에 맞추어 적절한 행동을 하게 하고 싶을 때, addEventListener() 함수를 이용하여 이벤트에 함수를 연결시켜 줄 수 있다. 이 함수를 이벤트 핸들러 (Event Handler)라고 부른다. 이처럼 이벤트와 이벤트 핸들러를 연결시켜 주는 것을 이벤트 리스너라고 한다. 이벤트가 발생했을 때 이벤트 핸들러를 실행시켜 준다는 점에서, 이벤트 리스너는 이벤트와 이벤트 핸들러 간의 일종의 매개체인 셈이다. (addEventListener() 함수를 이용하는 방법 이외에도 다른 방법 역시 존재한다.)
const button = document.createElement("button");
button.addEventListener("click", () => {
alert("버튼이 클릭됐어요.");
});
가령 위 코드처럼 button이라는 요소에 대해 click이라는 이벤트가 발생했을 때 익명 함수로 정의된 이벤트 핸들러를 실행하고 싶은 경우, 이벤트 리스너를 이용할 수 있다.
문제는 이렇게 요소에 이벤트 리스너를 등록하게 되면 메모리 상에서 저 익명 함수가 메모리 공간을 차지하게 된다는 점이다. 원래였다면 자바스크립트의 가비지 컬렉터 (Garbage Collector)에 의해 자동으로 제거됐을 익명 함수이지만, 이벤트 리스너에 의해 메모리 공간에서 계속해서 살아남는 것이다. 따라서 필요 없는 이벤트 리스너를 제때제때 제거해주지 않으면 메모리 공간이 이벤트 핸들러에 의해 꽉 차버리고 말 것이다. 메모리 누수가 발생하는 것이다.
그러면 요소를 제거하기 전에 이벤트 리스너를 제거하지 않으면 안 되는 것 아닌가? 다행히 최신 브라우저들은 가비지 컬렉터에 의해 요소를 제거하면 거기에 딸린 이벤트 리스너들도 자동으로 제거를 해준다고 한다. 정확히 말하면, DOM 내부에 그 요소가 존재하지 않아 요소에 대한 참조가 사라지게 되면 자바스크립트의 가비지 컬렉터에 의하여 이벤트 리스너가 자동으로 제거되는 것이다.
이를 검증하기 위해 필자가 직접 테스트해보기로 했다.
function test() {
setInterval(() => {
const newDiv = document.createElement('div');
document.body.appendChild(newDiv);
newDiv.addEventListener("click", () => {
const myString = "😊😊😊😊😊😊😊😊...😊😊😊😊😊😊";
});
}, 1);
}
위 코드는 0.001초마다 div 요소를 새로 만들고, body에 추가한 뒤, 이벤트 리스너를 등록하는 함수이다. 함수가 메모리 공간에서 차지하는 공간이 크게 하도록 하기 위해 일부러 myString에 크기가 큰 이모지로 이루어진 문자열을 집어넣었다. 이벤트 리스너를 제거하는 과정도 없고 요소를 제거하는 과정도 없으니, 함수를 실행하면 당연히 메모리 사용량이 급증할 것이다.
크롬 DevTools의 Performance 탭이다. 뭔지는 잘 모르겠지만 뭔가 가파르게 상승하는 걸 보니 내 예상이 맞는 모양이다. 그러면 생성한 요소를 제거하면 어떻게 되는지 살펴보도록 하자.
let intervalID;
function test() {
intervalID = setInterval(() => {
const newDiv = document.createElement('div');
document.body.appendChild(newDiv);
newDiv.addEventListener("click", () => {
const myString = "😊😊😊😊😊😊😊😊...😊😊😊😊😊😊";
});
}, 1);
}
function stopTest() {
clearInterval(intervalID);
for (const div of document.querySelectorAll('div')) {
div.remove();
}
}
이번에는 stopTest() 함수를 추가하여 div 생성을 중단하고 모든 div를 삭제하는 작업을 수행해보았다.
위 사진을 보면 stopTest() 함수를 실행한 뒤에도 잠시동안 (대략 10000ms ~ 11000ms) 이벤트 리스너가 살아남아 있는 걸 볼 수가 있는데, 이는 저 기간 동안 가비지 컬렉터가 작동하지 않아서이다. 잠시 뒤 가비지 컬렉터가 작동되어 이벤트 리스너가 모두 제거된 것을 확인할 수 있다. 어쨌든 노드를 제거하니까 이벤트 리스너도 자동으로 제거된다는 것이다.
다만 위 테스트에서 가비지 컬렉터는 필자가 수동으로 작동시킨 것인데, 원래대로라면 브라우저 자체적으로 적절한 시점에 가비지 컬렉터를 작동시켜 이벤트 리스너가 제거됐을 것이다. 하지만 가비지 컬렉터가 작동되기 전까지는 이벤트 리스너가 계속 유지되고, 따라서 메모리 누수 역시 발생하리라 여겨진다. 따라서 내 개인적인 생각은 요소를 제거하기 전에 이벤트 리스너를 제거하면 이러한 메모리 누수를 방지할 수 있을 것이지만, 일시적인 누수일 것이기에 굳이 그럴 필요가 없다는 것이다.