[Core Js] 클로저와 메모리 관리

Doha Lee·2023년 5월 27일
0

JavaScript

목록 보기
2/4

클로저와 메모리 관리

메모리 누수란?

자바스크립트 에서는 가비지 컬렉션(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)를 할당하면 된다.


(1) return에 의한 클로저의 메모리 해제

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함수가 실행될 때, a 변수와 inner 함수가 생성된다.
  • inner 함수는 a 변수를 참조하고 있으므로 inner 함수가 반환 되고 outer 함수가 종료되더라도 a 변수가 메모리에 살아있게 된다. 이것이 클로저에 의한 동작 방식이기도 하다.
  • outer 함수에서 생성한 inner 함수가 outer2 변수에 할당되어 참조된다.
  • 즉, outer2 변수를 통해 inner 함수를 호출하면, 내부의 자유 변수 a가 증가한 뒤, 반환 된다.
  • 이때 outer2 변수가 inner 함수를 참조하고 있으므로 inner 함수와 a 변수에 대한 참조가 지속된다.
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 변수가 참조하고 있는 함수 객체와 그 안에서 참조하고 있는 변수들에 대한 참조가 모두 제거 된다. 이렇게 명시적으로 참조를 제거하면 더이상 해당 객체에 접근할 수 없게 된다.


(2) setInterval에 의한 클로저 메모리 해제

(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 변수에 할당한다.
  • inner 함수 내부에 정의된 변수 a를 0으로 초기화한다.
  • intervalId 변수를 null로 초기화한다.
  • inner 함수를 정의한다. 함수 내부에서는 a 변수를 1씩 증가시키고, a 변수의 값이 10 이상이 되면 setInterval 함수로 반환된 intervalId 값을 사용하여 setInterval 타이머를 종료한다.
  • inner 함수를 1초마다 실행하는 타이머를 설정한다. setInterval 함수는 inner 함수의 실행 결과를 반환하지 않으므로, 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로 제거하여 메모리 누수를 방지할 수 있다.


(3) eventListener에 의한 클로저의 메모리 해제

(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)이다.

  • 즉시 실행 함수로 함수 내부에 count 변수, button 변수, clickHandler 함수를 정의한다.
  • count 변수를 0으로 초기화 한다. count 변수는 클릭 횟수 저장을 담당한다.
  • button element를 생성한다.
  • button에 'Click'이라는 텍스트를 할당한다.
  • 클릭 이벤트를 처리하는 clickHandler 함수를 정의한다. 이 함수는 클릭 된 횟수를 콘솔에 출력하고, 클릭 횟수가 10 이상이 되면 button에 등록된 click 이벤트 핸들러를 제거한다.
  • clickHandler 함수를 클릭 이벤트의 콜백 함수로 등록하여 button이 클릭 될 때마다 클릭 수를 콘솔에 남긴다.
(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와 같은 치명적인 문제를 초래할 수 있다. 따라서 메모리 누수를 방지하고 지속적인 메모리 사용량을 모니터링 함으로써, 시스템의 안정성과 성능을 극대화 할 수 있다.

0개의 댓글

관련 채용 정보