최근 회사 인턴으로 합류하면서 열심히 FrontEnd 개발자로 일하고 있다. 첫 회사여서 부담감이 크지만, 성실히 잘 수행해 나가고 있고, 계속해서 웹 트래픽이 늘고 있으면 기분이가 좋아진다.
아무튼 그래서 그 중에 코드 PR리뷰를 하게 되면서 어떠한 반복문에서 사수님의 지적이 있었다.
이 반복문에서 메모리 회수 안될 것 같은데?
에? 그럴리가.. 를 생각했지만, 한번 알아보라는 사수님의 말에 공부하게 되었고, 한번 쭉 정리를 해보려고 한다.
자바스크립트는 메모리를 자동으로 관리하는 Garbage Collector
(가비지 컬렉터)를 가지고 있습니다. 가비지 컬렉터는 메모리에 할당된 객체 중에서 더 이상 사용되지 않는 객체, 즉 Garbage(쓰레기)를 자동으로 탐지하고 제거하는 역할을 합니다.
JS에서는 개발자가 직접 메모리 할당과 해제를 하지 않아도 됩니다. 객체를 생성하면 자동으로 메모리에 할당되며, 해당 객체를 더 이상 사용하지 않을 때는 개발자가 직접 할당 해제를 하지 않아도, Garbage Collector
객체를 제거해줍니다. 이러한 기능을 통해 개발자는 코드에 집중할 수 있으며, 메모리 관리에 대한 부담을 줄일 수 있습니다.
Garbage Collector
는 주기적으로 메모리를 스캔하여 더 이상 참조되지 않는 객체를 제거합니다. 이 때, 참조되지 않는 객체를 어떻게 판단하는가에 따라 Garbage Collector
의 성능이 좌우됩니다.
대부분의 JS 엔진에서는 Mark-and-Sweep
알고리즘을 사용하여 가비지 컬렉션을 수행합니다.
이 알고리즘은 Root Set으로부터 시작하여 도달 가능한 객체를 마킹하고, 마킹되지 않은 객체를 Garbage
로 판단하여 제거합니다.
가비지 컬렉션은 자동으로 이루어지지만, 가끔씩 개발자가 메모리 누수
(memory leak)를 일으키는 경우가 있습니다.
아니!! 그러면 메모리 회수가 안되는 건 저 알고리즘 탓 아닌교!!!!
라면서 굉장히 그럼 잘 만들어진 걸 주던가라는 말을 할 수 도 있지만, 세상에 코드의 정답은 없고, 버그는 많다라는 말과 함께 우리 JS 개발자는 이를 극복해야합니다.
메모리 누수는 개발자가 의도하지 않은 객체의 참조를 유지하게 되는 경우를 말하며, 이러한 경우에는 Garbage Collector
가 객체를 제거하지 못하고 메모리를 계속 점유하게 되는 현상을 말합니다.
그렇다면 코드를 어떻게 쓰면 메모리누수가 발생되는 예시를 보여드리겠습니다.
function handleClick() {
console.log("Button clicked");
}
const button = document.querySelector("#myButton");
button.addEventListener("click", handleClick);
// ...
// 이벤트 리스너를 제거하지 않았으므로, 메모리 누수가 발생할 가능성이 있음
위 코드에서 button 요소에 클릭 이벤트 리스너를 등록하고 있습니다. 하지만 이벤트 리스너를 제거하지 않으면 해당 요소가 제거될 때까지 이벤트 리스너가 메모리에 남아있게 됩니다.
해결책으로는, 이벤트 리스너를 등록할 때 리스너 함수를 변수에 저장하고, 이벤트 리스너를 제거할 때 해당 변수를 사용하여 제거해주는 것입니다.
function handleClick() {
console.log("Button clicked");
}
const button = document.querySelector("#myButton");
button.addEventListener("click", handleClick);
// ...
button.removeEventListener("click", handleClick);
let count = 0;
setInterval(() => {
console.log(count++);
}, 1000);
// ...
// clearInterval() 함수를 호출하지 않았으므로, 메모리 누수가 발생할 가능성이 있음
해결책으로는, setInterval() 함수를 호출할 때 반환되는 타이머 ID 값을 변수에 저장하고, clearInterval() 함수를 호출할 때 해당 변수를 사용하여 타이머를 제거해주는 것입니다.
let count = 0;
let intervalId = setInterval(() => {
console.log(count++);
}, 1000);
// ...
clearInterval(intervalId);
이제 이해가 되셨나요? 한마디로 자신이 등록해놓은 변수에 대해서 회수를 하지 않게 된다면 이러한 메모리 누수를 야기할 수 있다는 사실!
하지만 이러한 상황이 아닌 단순한 순수 JS 코드에서는 메모리 누수가 일어나는 경우도 있습니다.
function addButtons() {
const buttonsContainer = document.querySelector("#buttons");
for (var i = 0; i < 5; i++) {
const button = document.createElement("button");
button.textContent = "Button " + i;
button.addEventListener("click", function() {
console.log("Button " + i + " clicked");
});
buttonsContainer.appendChild(button);
}
}
addButtons();
위 코드에서 addButtons() 함수 내부에서 for 반복문을 사용하여 버튼을 생성하고, 각 버튼에 이벤트 리스너를 등록하고 있습니다. 이벤트 리스너 함수는 클로저 함수로 정의되어 있으며, 버튼이 클릭될 때 해당 버튼의 인덱스를 출력하고 있습니다.
하지만 for 반복문에서 사용되는 i 변수는 var 키워드를 사용하여 선언되었기 때문에 함수 스코프를 가지게 됩니다. 이러한 이유로 클로저 함수 내에서 i 변수를 참조하면 반복문이 종료된 이후에도 변수의 값이 계속해서 참조되므로, 원치 않는 결과가 발생할 수 있습니다.
해결책으로는, for 반복문에서 let 키워드를 사용하여 변수를 선언하는 것입니다. let으로 변수를 선언하면 블록 스코프를 가지게 되므로, 클로저 함수 내부에서 해당 변수를 참조하더라도 원하는 값이 출력됩니다.
function addButtons() {
const buttonsContainer = document.querySelector("#buttons");
for (let i = 0; i < 5; i++) {
const button = document.createElement("button");
button.textContent = "Button " + i;
button.addEventListener("click", function() {
console.log("Button " + i + " clicked");
});
buttonsContainer.appendChild(button);
}
}
addButtons();
위와 같이 let을 사용하여 변수를 선언하면, 클로저 함수 내부에서 i 변수를 참조할 때 각각의 버튼이 클릭될 때 해당 버튼의 인덱스가 출력됩니다.
이번 학습에서 자바스크립트의 Garbage Collector
에서 일어날 수 있는 메모리 누수에 대해서 한번 학습한 내용을 정리해보았습니다. 이렇게 학습하고 보니 그때 지적받았던 코드는 크게 메모리 누수와 상관 없었던 것 같기도 하다는 생각을 했습니다. 다음번에도 GC에 대한 내용이 거론되면 뜨거운 개발 토의 현장을 만들 수 있다는 자신감이 차오르게 되었습니다.
(사수님의 표정)
언제까지고 뒤쳐져서 살 수 없습니닷!
한번 그때를 기대하면서 다음 블로깅으로 돌아오겠습니다. 감사합니다.