JavaScript

Memory Leak

나이뚜우 2022. 12. 23. 21:49
메모리 누수(Memory Leak)

메모리 누수(Memory Leak)란 더 이상 필요하지 않은 데이터가 메모리에서 해제되지 못하고 메모리를 계속 차지되는 현상입니다.

 

 

[메모리 누수가 발생하는 상황] *잘못된 코드 작성 습관은 메모리 누수로 연결됩니다.

  • 불필요한 전역 변수 사용
window.ex = "hello world";
window.rati = { name: "rati", age: 5 };

불필요한 전역 변수 사용의 예로 전역 객체 window에 선언을 한 경우 브라우저에 항상 존재해야 하기 때문에 데이터가 더 이상 필요하지 않음에도 불구하고 해제되지 못하고 메모리 누수를 발생시킵니다.

 

  • 분리된 노드 참조
// html
 <body>
    <button>Remve!</button>

    <div class="parent">
      <div class="child1">1</div>
      <div class="child2">2</div>
    </div>
 </body>
  
  
// js
const btn = document.querySelector("button");
const parent = document.querySelector(".parent");

btn.addEventListener("click", () => {
  console.log(parent);
  parent.remove();
});

Remove 버튼을 클릭하게 되면 요소 parent가 콘솔에 출력되고 해당 요소가 제거되도록 해주었습니다. 실제로 화면에서 해당 요소는 제거가 되었지만 버튼을 추가적으로 클릭하게 되면 콘솔에는 해당 요소가 계속해서 출력되는 것을 확인할 수 있습니다.

 

위의 예시가 분리된 노드를 참조하여 메모리 누수가 발생한 대표적인 예시이며, 이러한 상황이 발생한 원인은 상수 parent에 클래스가 parent를 찾아 할당을 하여 메모리 공간에 저장이 된 상태에서 remove 메서드를 통해 해당 요소를 제거를 하더라고 해당 요소가 가지고 있는 메모리 공간의 주소는 제거된 것이 아니기 때문에 상수 parent는 가비지 컬렉션에 의해 제거되지 못하게 됩니다.

 

const btn = document.querySelector("button");

btn.addEventListener("click", () => {
  const parent = document.querySelector(".parent");
  console.log(parent);
  parent && parent.remove();
});

해결 방법은 간단합니다. 클래스가 parent인 요소를 찾아 할당하는 코드를 이벤트 내부에 작성해주게 되면 버튼을 처음 클릭했을 때 해당 요소를 찾아 메모리 공간에 저장하지만, 이후 버튼 클릭 시 해당 요소를 찾지 못해 상수 parent에는 null이 할당되게 되게 됩니다.

더 이상 상수 parent는 사용되지 않고 null 값을 가지고 있기 때문에 가비지컬렉션에 의해 해제가 됩니다.

 

  • 해체하지 않은 타이머
let a = 0;
setInterval(() => {
  a += 1;
}, 100);

setTimeout(() => {
  console.log(a);
}, 1000);

함수 setInterval를 사용하여 0.1초에 한 번씩 변수 a의 값에 1을 더해주고 있으며, 함수 setTimeout를 사용하여 1초 뒤에 변수 a가 출력되도록 해주었습니다. 이때 변수 a는 계속해서 증가하고 있기 때문에 setInterval이 동작하고 있는 동안은 계속해서 메모리를 차지하게 됩니다.

 

let a = 0;
const intervalId = setInterval(() => {
  a += 1;
}, 100);

setTimeout(() => {
  console.log(a);
  clearInterval(intervalId);
}, 1000);

메모리 누수를 방지하기 위해 함수 setTimeout에서 변수 a 사용이 끝나면 clearInterval을 사용하여 interval을 해제해주어야 합니다.

 

  • 잘못된 클로저 사용
const getFn = () => {
  let a = 0;
  return (name) => {
    a += 1;
    console.log(a);
    return `Hello ${name}~`;
  };
};

const fn = getFn();
console.log(fn("rati1"));
console.log(fn("rati2"));
console.log(fn("rati3"));

정작 필요한 부분은 Hello + 인수로 전달받은 문자가 출력되는 부분인데 불필요한 a+=1, a 콘솔 출력으로 렉시컬 범위에 있는 변수 a를 계속해서 사용하고 있기 때문에 가비지 컬렉션이 해당 변수를 해제하지 못하고 메모리를 계속 차지하고 있는 상황이 발생하게 됩니다.