일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | ||
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 |
- virtual Dom
- 유용한 사이트
- 프로그래머스
- State
- BOJ
- 요청
- 상태
- API
- 상태 끌어올리기
- 가상 DOM
- deep
- DoM
- 개발
- Hook
- Dive
- memory
- 꿀팁
- LeetCode
- javascript
- 원리
- axios
- 백준
- react
- programmers
- EventListener
- 프로젝트
- Java
- Today
- Total
탄탄한 기본기!
메모리 관리와 removeEventListener의 진실 (Garbage collection) 본문
0. 개요
DOM 요소에 이벤트를 바인딩해주고 이를 해제해주는 과정에서, 문득 이벤트 리스너들이(메모리가) 잘 해제되는지 궁금해 개발자 도구를 열어 performance 탭의 memory 부분에 있는 리스너 개수를 확인해보았다.
그런데 removeEventListener를 호출할 때, 리스너 개수가 줄어드는 것이 아닌 오히려 증가했다가, 나중에 GC에 의해 리스너가 사라지는 것을 확인하고 이 부분을 조금 더 확인해보며 그 내용을 정리했다.
1. HTMLElement.prototype.removeEventListener 개념
보통 DOM요소에 이벤트 핸들러를 바인딩해서 다양한 이벤트를 캐치한다. 하지만 메모리 누수 방지를 위해서 불필요할 경우 (컴포넌트가 unMount 되는 등) 이를 해제해주어야 한다.
이때 바인딩해준 이벤트 핸들러를 해제해주기 위해서 사용되는 메서드가 HTMLElement.prototype.removeEventListener이다.
2. DOM요소의 삭제와 EventListner의 해제
기본적으로 DOM 요소가 삭제된 후, 그 누구도 더 이상 그 요소를 참조하지 않는다면 (참조하는 변수 등이 없다면) 자동으로 이벤트 리스너가 해제되어 메모리 할당이 해제된다.
아래는 이 내용을 확인하기 위해 직접 테스트를 해본 것이다.
let i = 0;
let $button = document.querySelector('button');
// 각각 다른 참조 값을 가진 함수(핸들러)를 생성해주기 위해 함수 크리에이터 함수를 만듬
const clickHandler = (i) => () => {
console.log('hello', i);
};
// 버튼 요소에 위의 클릭 핸들러를 바인딩해준다.
const addEv = (i) => {
$button.addEventListener('click', clickHandler(i));
};
const removeElement = () => {
document.body.removeChild($button);
$button = null; // 참조 해제 (GC 대상 설정)
};
document.body.onkeydown = (e) => {
if (e.key === 'a') {
for (let i = 0; i < 500; i++) {
addEv(i);
}
return;
}
if (e.key === 's') {
removeElement();
}
};
노란색 선인 Listeners 만 활성화해놓고 본 결과이다.
이해하기 쉽도록 시간의 흐름대로 보았을 때 아래와 같은 타임라인을 가진다고 볼 수 있다.
- 0초
- 1초
- 약 2초: 이벤트 바인딩
- 3초
- 약 4초: 요소 삭제
- 5초
- ...
- 약 9초: GC에 의해 메모리 해제 (리스너)
하지만, 만약 DOM 요소를 (button) 전역에서 계속해서 참조하고 있다면 어떻게 될까?
let i = 0;
let $button = document.querySelector('button');
// 각각 다른 참조 값을 가진 함수(핸들러)를 생성해주기 위해 함수 크리에이터 함수를 만듬
const clickHandler = (i) => () => {
console.log('hello', i);
};
// 버튼 요소에 위의 클릭 핸들러를 바인딩해준다.
const addEv = (i) => {
$button.addEventListener('click', clickHandler(i));
};
const removeElement = () => {
document.body.removeChild($button);
// $button = null; /* 주석 처리 */
};
document.body.onkeydown = (e) => {
if (e.key === 'a') {
for (let i = 0; i < 500; i++) {
addEv(i);
}
return;
}
if (e.key === 's') {
removeElement();
}
};
노란색 선을 보았을 때, GC 대상이 되지 않기 때문에 메모리 해제가 되지 않는 것을 확인할 수 있다.
3. removeEventListener의 사용
아무리 GC가 메모리 해제를 해준다지만, GC에게 메모리 책임을 떠넘기고 신경을 끄는 것은 메모리 관리 방식이 아니기 때문에 명시적으로 메모리를 해제해줄 필요가 있다고 생각한다.
removeEventListener는 DOM 요소가 가지고 있는 메서드로, 바인딩되어있는 이벤트와 (ex. click, mouseover, keydown 등) 바인딩해준 핸들러를 인자로 넘겨주어 바인딩된 이벤트 핸들러를 삭제(메모리를 해제)해준다.
확인해보자.
let i = 0;
let handlerHistory = [];
let $button = document.querySelector('button');
// 각각 다른 참조 값을 가진 함수(핸들러)를 생성해주기 위해 함수 크리에이터 함수를 만듬
const clickHandler = (i) => () => {
console.log('hello', i);
};
// 버튼 요소에 위의 클릭 핸들러를 바인딩해준다.
const addEv = (i) => {
const handler = clickHandler(i);
$button.addEventListener('click', handler);
handlerHistory.push(handler);
};
// 핸들러들을 순회하며 모두 해제해준다.
const removeElement = () => {
handlerHistory.forEach((handler) => {
$button.removeEventListener('click', handler);
});
handlerHistory = [];
};
document.body.onkeydown = (e) => {
if (e.key === 'a') {
addEv(i++);
return;
}
if (e.key === 's') {
removeElement();
}
};
'a' 키를 연타해서 eventListener를 계속 추가하다가 's' 키를 눌러 이때까지 바인딩했던 이벤트 리스너들을 모두 지워주도록 테스트했다.
노란색 그래프를 보면 알 수 있지만, 'a' 키를 연타했기 때문에 계단식으로 이벤트 리스너 개수가 증가하다가, 갑자기 큰 폭으로 이벤트 리스너 개수가 증가한 것을 확인할 수 있다.
저 부분은 's' 키를 눌러 removeEventListener를 호출한 것이고, removeEventListener 또한 리스너로 간주되는 것을 확인할 수 있었다. 이 부분이 의아해서 다시 실험해보았지만, 실험 결과는 아래처럼 같았다.
즉 removeEventListener 또한 내부 구현 과정에서 DOM 요소의 이벤트 리스너로 등록해주는 과정을 거친다는 사실을 확인할 수 있었다.
4. removeEventListener도 이벤트 리스너로 취급된다.
Chrome의 개발자 도구 performance 탭에 있는 memory 부분을 확인하던 도중 알게 된 사실은, removeEventListener 또한 리스너로 취급될 수 있다는 사실이다. (적어도 개발자 도구, 혹은 크롬에서 리스너로 인식한다...).
그리고 해당 메서드가 직접적으로 메모리를 해제하는 것이 아니라 GC의 대상이 될 수 있도록 조절해준다는 사실을 확인할 수 있었다.
자바스크립트의 GC를 맹신하고 메모리 관리를 하지 않다가, 자신도 모르게 메모리 누수를 경험하지 않도록 항상 명시적으로 메모리 관리를 해주려는 노력을 하는 것이 좋다고 생각한다.
'개인 공부 > JS (자바스크립트)' 카테고리의 다른 글
background image의 load 시점 감지하는 법? (0) | 2022.02.28 |
---|---|
Async/Await 잘 활용하는 법 (0) | 2021.07.28 |
Promise 내부 구현 따라해보기 (0) | 2021.07.02 |
javaScript - Ajax (0) | 2021.06.27 |
javaScript - 디바운스(debounce), 스로틀(throttle) (0) | 2021.06.10 |