[JavaScript] 가비지 컬렉터에서의 메모리 누수는 언제 일어날까?(Garbage Collector)

Murpin·2023년 3월 4일
2

개인공부

목록 보기
5/5
post-thumbnail

들어가면서..

최근 회사 인턴으로 합류하면서 열심히 FrontEnd 개발자로 일하고 있다. 첫 회사여서 부담감이 크지만, 성실히 잘 수행해 나가고 있고, 계속해서 웹 트래픽이 늘고 있으면 기분이가 좋아진다.
아무튼 그래서 그 중에 코드 PR리뷰를 하게 되면서 어떠한 반복문에서 사수님의 지적이 있었다.

이 반복문에서 메모리 회수 안될 것 같은데?

에? 그럴리가.. 를 생각했지만, 한번 알아보라는 사수님의 말에 공부하게 되었고, 한번 쭉 정리를 해보려고 한다.

What is GC?

자바스크립트는 메모리를 자동으로 관리하는 Garbage Collector(가비지 컬렉터)를 가지고 있습니다. 가비지 컬렉터는 메모리에 할당된 객체 중에서 더 이상 사용되지 않는 객체, 즉 Garbage(쓰레기)를 자동으로 탐지하고 제거하는 역할을 합니다.

JS에서는 개발자가 직접 메모리 할당과 해제를 하지 않아도 됩니다. 객체를 생성하면 자동으로 메모리에 할당되며, 해당 객체를 더 이상 사용하지 않을 때는 개발자가 직접 할당 해제를 하지 않아도, Garbage Collector 객체를 제거해줍니다. 이러한 기능을 통해 개발자는 코드에 집중할 수 있으며, 메모리 관리에 대한 부담을 줄일 수 있습니다.

Garbage Collector는 주기적으로 메모리를 스캔하여 더 이상 참조되지 않는 객체를 제거합니다. 이 때, 참조되지 않는 객체를 어떻게 판단하는가에 따라 Garbage Collector의 성능이 좌우됩니다.

대부분의 JS 엔진에서는 Mark-and-Sweep 알고리즘을 사용하여 가비지 컬렉션을 수행합니다.

이 알고리즘은 Root Set으로부터 시작하여 도달 가능한 객체를 마킹하고, 마킹되지 않은 객체를 Garbage로 판단하여 제거합니다.

가비지 컬렉션은 자동으로 이루어지지만, 가끔씩 개발자가 메모리 누수(memory leak)를 일으키는 경우가 있습니다.

아니!! 그러면 메모리 회수가 안되는 건 저 알고리즘 탓 아닌교!!!!

라면서 굉장히 그럼 잘 만들어진 걸 주던가라는 말을 할 수 도 있지만, 세상에 코드의 정답은 없고, 버그는 많다라는 말과 함께 우리 JS 개발자는 이를 극복해야합니다.

메모리 누수는 개발자가 의도하지 않은 객체의 참조를 유지하게 되는 경우를 말하며, 이러한 경우에는 Garbage Collector가 객체를 제거하지 못하고 메모리를 계속 점유하게 되는 현상을 말합니다.

그렇다면 코드를 어떻게 쓰면 메모리누수가 발생되는 예시를 보여드리겠습니다.


1. 이벤트 리스너 등록 후 제거하지 않는 경우

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);



2. setInterval() 사용 후 clearInterval() 호출하지 않는 경우

let count = 0;

setInterval(() => {
  console.log(count++);
}, 1000);
// ...
// clearInterval() 함수를 호출하지 않았으므로, 메모리 누수가 발생할 가능성이 있음

위 코드에서 setInterval() 함수를 사용하여 1초마다 count 값을 출력하도록 설정하고 있습니다. 하지만 clearInterval() 함수를 호출하지 않으면 setInterval() 함수가 계속해서 실행되며, count 변수와 함께 메모리에 계속해서 남아있게 됩니다.

해결책으로는, setInterval() 함수를 호출할 때 반환되는 타이머 ID 값을 변수에 저장하고, clearInterval() 함수를 호출할 때 해당 변수를 사용하여 타이머를 제거해주는 것입니다.

let count = 0;
let intervalId = setInterval(() => {
  console.log(count++);
}, 1000);
// ...
clearInterval(intervalId);

이제 이해가 되셨나요? 한마디로 자신이 등록해놓은 변수에 대해서 회수를 하지 않게 된다면 이러한 메모리 누수를 야기할 수 있다는 사실!
하지만 이러한 상황이 아닌 단순한 순수 JS 코드에서는 메모리 누수가 일어나는 경우도 있습니다.

순수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에 대한 내용이 거론되면 뜨거운 개발 토의 현장을 만들 수 있다는 자신감이 차오르게 되었습니다.

(사수님의 표정)

언제까지고 뒤쳐져서 살 수 없습니닷!
한번 그때를 기대하면서 다음 블로깅으로 돌아오겠습니다. 감사합니다.

profile
모든 것에 배움을 얻고자합니다

0개의 댓글