자바스크립트 에서는 가비지 컬렉션(Garbage Collection)이 동작하여 더이상 참조하지 않는 메모리를 해제한다. 근데 개발자의 의도와 달리 어떤 값이 참조 카운트가 0이 되지 않아 GC(Garbage Collector)의 수거 대상이 되지 않는 경우를 메모리 누수가 일어난다고 한다. 보통 참조 계층이 형성되면 가비지 컬렉션이 작동하지 못하고 발생하곤 한다. 그 외에도 비 객체를 참조하거나 서로를 참조하는 경우 메모리 누수가 발생하기도 한다.
function createLeak() {
let element = document.createElement('div');
// 아래의 코드에서 element를 참조하고 있다.
// 아래의 코드가 존재하는 한 element는 가비지 컬렉션의 대상이 되지 않는다.
element.onclick = function() {
alert('클릭 된 엘리먼트: ' + element.id);
}
// element가 DOM에서 제거되어도 참조하고 있는 이벤트 핸들러 때문에 가비지 컬렉션 대상이 되지 않는다.
document.body.appendChild(element);
}
// createLeak 함수를 호출하면 div element를 생성하고 이벤트 처리를 위한 함수를 등록한다.
createLeak();
위 예시 코드에서 createLeak 함수는 element를 생성하고 이벤트 핸들러를 등록하고 있다. 등록한 이벤트 핸들러가 실행되면서 element를 참조하고 있다. 이 상황에서 해당 element를 삭제하면 이벤트 핸들러가 여전히 element를 참조하기 때문에 메모리 누수가 발생한다.
function createLeak() {
let element = document.createElement('div');
element.onclick = function() {
alert('클릭 된 엘리먼트: ' + element.id);
}
// onclick 이벤트에 null을 추가 하여 참조를 제거하기
element.onclick = null;
}
createLeak();
위 예시 코드와 같이 createLeak 함수를 호출하면 element를 생성한 후에 이벤트 핸들러에 대한 참조를 제거할 수 있다.
클로저는 어떤 필요에 의해 의도적으로 함수의 지역변수를 메모리를 소모하도록 함으로써 발생한다. 그렇다면 그 필요성이 사라진 시점에는 더는 메모리를 소모하지 않게 해주면 된다. 참조 카운트를 0으로 만들면 언젠가 GC가 수거할 것이고, 이때 소모했던 메모리가 회수 된다.
참조 카운트를 0으로 만드는 방법은 식별자에 참조형이 아닌 기본형 데이터 (null, undefined)를 할당
하면 된다.
let outer = function () {
let a = 1;
let inner = function () {
return ++a;
};
return inner;
};
let outer2 = outer1;
console.log(outer2()); // 2
console.log(outer2()); // 3
위 코드 예제는 클로저를 이용하여 생성된 내부 함수에서 외부 변수를 참조하고 있다.
let outer = function () {
let a = 1;
let inner = function () {
return ++a;
};
return inner;
};
let outer2 = outer1;
console.log(outer2()); // 2
console.log(outer2()); // 3
outer = null;
마지막에 outer = null
을 할당하여 outer 변수가 참조하고 있는 함수 객체와 그 안에서 참조하고 있는 변수들에 대한 참조가 모두 제거 된다. 이렇게 명시적으로 참조를 제거하면 더이상 해당 객체에 접근할 수 없게 된다.
(function () {
let a = 0;
let intervalId = null;
let inner = function () {
if (++a >= 10) {
clearInterval(intervalId);
}
console.log(a);
};
intervalId = setInterval(inner, 1000);
})();
위 예시 코드는 즉시 실행 함수(IIFE)를 활용하여 inner 함수를 1초마다 실행하면서 a 변수를 1씩 증가시키는 예제이다.
(function() { ... })()
즉시 실행 함수로 함수 내부에 a 변수, intervalId 변수, Inner 함수를 정의하고 setInterval 함수를 사용하여 inner 함수를 1초마다 실행한다.이때, 반환되는 intervalId 값을 intervalId 변수에 할당한다.(function () {
let a = 0;
let intervalId = null;
let inner = function () {
if (++a >= 10) {
clearInterval(intervalId);
inner = null;
}
console.log(a);
};
intervalId = setInterval(inner, 1000);
})();
a 변수의 값이 10 이상이 되면 setInterval 함수로 반환된 intervalId 값을 사용하여 setInterval 타이머를 종료하고 inner 함수가 가리키고 있는 a 변수에 대한 참조를 inner = null
로 제거하여 메모리 누수를 방지할 수 있다.
(function () {
let count = 0;
let button = document.createElement('button');
button.innerText = 'Click';
let clickHandler = function () {
console.log(++count, 'times clicked');
if (count >= 10) {
button.removeEventListener('click', clickHandler);
}
};
button.addEventListener('click', clickHandler)
document.body.appendChild(button);
})();
위 예시 코드는 Click이라는 텍스트를 가진 button을 동적으로 생성하고, 클릭 이벤트 발생 시 클릭 횟수를 콘솔에 출력하고 클릭 횟수가 10 이상일 경우 클릭 이벤트 핸들러를 제거하는 즉시 실행 함수(IIFF)이다.
(function () {
let count = 0;
let button = document.createElement('button');
button.innerText = 'Click';
let clickHandler = function () {
console.log(++count, 'times clicked');
if (count >= 10) {
button.removeEventListener('click', clickHandler);
clickHandler = null;
}
};
button.addEventListener('click', clickHandler)
document.body.appendChild(button);
})();
clickHandler = null
은 clickHandler 함수를 참조하는 변수를 null로 설정하여 clickHandler 함수에 대한 참조를 제거한다. removeEventListener 함수가 호출되면서 현재 클릭 핸들러 함수가 해당 버튼에서 제거 된다. 그 후에 clickHandler 변수가 null로 설정되면 GC의 수거 대상이 되어 수거된다. 이렇게 함으로 메모리 누수를 방지할 수 있다.
메모리 누수를 제거하는 것은 자원 이용에 있어서 가장 중요한 것 중 하나이다. 메모리 누수가 발생하면 시스템 자원에 한계에 이를 수 있고, 이는 system crash와 같은 치명적인 문제를 초래할 수 있다. 따라서 메모리 누수를 방지하고 지속적인 메모리 사용량을 모니터링 함으로써, 시스템의 안정성과 성능을 극대화 할 수 있다.