[JavaScript] 클로저

빵호·2021년 11월 22일
0

JavaScript

목록 보기
19/28
post-thumbnail

클로저

클로저의 의미 및 원리 이해

클로저(Closure)는 여러 함수형 프로그래밍 언어에서 등장하는 보편적인 특성이고 어떤 함수에서 선언한 변수를 참조하는 내부함수를 외부로 전달할 경우, 함수의 실행 컨텍스트가 종료된 후에도 해당 변수가 사라지지 않는 현상을 말한다.

var outer = function () {
  var a = 1;
  var inner = function () {
    return ++a;
  };
  return inner; // 6번째 줄
};
var outer2 = outer(); // 8번째 줄
console.log(outer2); // 2    9번쨰 줄
console.log(outer2); // 3

6번째 줄에서 inner 함수 자체를 반환했다. 그러면 outer 함수의 실행 컨텍스트가 종료될 때(8번째 줄) outer2 변수는 outer의 실행 결과인 inner 함수를 참조하게 될 것이다. 이후 9번째에서 outer2를 호출하면 앞서 반환된 inner 함수가 실행된다.

inner 함수의 실행 시점에는 outer 함수는 이미 실행 종료 상태인데 outer 함수인에 어떻게 접근할 수 있는 걸까? 가비지 컬렉터는 어떤 값을 참조하는 변수 하나라도 있으면 그 값은 수집 대상에 포함시키지 않기 때문이다.

outer 함수는 실행 종료 시점에 inner 함수를 반환한다. 외부 함수 outer2로 인해 inner 함수의 호출 가능성이 열린다. 그럼 가비지 컬렉터의 수집 대상에서 제외되고 그 덕에 inner 함수가 이 변수에 접근할 수 있는 것이다.

여기서 주의할 점은 외부로 전달이 곧 return만을 의미하는 것은 아니라는 점이다.

// (1) setInterval / setTimeout
(funtion () {
 var a = 0;
 var intervalId = null;
 var inner = function () {
  if (++a >= 10) {
    clearInterval(intervalId);
  }
  console.log(a);
};
intervalId = setInterval(inner, 1000);
})();
// (2) eventListener
(function () {
  var count = 0;
  var button = document.createElement('button');
  button.innerText = 'click';
  button.addEventListener('click', function () {
    cosnole.log(++count, 'times clicked');
  });
  document.body.appendChild(button);
})();

(1)은 별로의 외부 객체 window의 메서드(setInterval, setTimeout)에 전달할 콜백 함수는 내부에서 지역변수를 참조한다.

(2) 별도의 외부 객체인 DOM의 메서드(addEventListener)에 등록할 handler 함수 내부에서 지역 변수를 참조한다.

두 상황 모두 지역변수를 참조하는 내부 함수를 외부에 전달했기 때문에 클로저이다.

클로저와 메모리 관리

메모리 누수의 위험을 이유로 클로저 사용을 조심하거나 지양해야 한다고 하는 사람들도 있지만 메모리 소모는 클로저의 본질적인 특성이다.

메모리 누수라는 표현은 개발자의 의도와 달리 가비지 컬렉터의 수거 대상이 되지 않는 경우에는 맞는 표현이지만 개발자가 의도적으로 참조 카운트를 0이 되지 않게 설계한 경우는 누수라고 할 수 없다.

메모리 관리 방법은 간단하다. 클로저의 필요성이 사라진 시점에 참조 카운트를 0으로 만들어 가비지 컬렉터의 수거 대상이 되게 하면 된다. 참조 카운트를 0으로 만드는 방법은 식별자에 기본형 데이터(보통 null이나 undefined)를 할당하며 된다.

// (1) return에 의한 클로저의 메모리 해제
var outer = (function () {
  var a = 1;
  var inner = function () {
  return ++a;
  };
  return inner;
})();
console.log(outer());
console.log(outer());
outer = null; // outer 식별자의 inner 함수 참조를 끊음

클로저 활용 사례

var fruits = ['apple', 'banana', 'peach'];
var $ul = document.createElement('ul'); // (공통 코드)

fruits.forEach(function (fruit) { // (A)
  var $li = document.createElement('li');
  $li.innerText = fruit;
  $li.addEventLisener('click', function () { // (B)
    alert('your choice is' + fruit);
  });
  $li.appendChild($li);
});
document.body.appendChild($ul);

fruits 변수를 순회하며 li를 생성하고, 각 li를 클릭하면 해당 리스너에 콜백 함수를 실행하게 했다. 4번째 줄의 forEach 메서드에 넘겨준 익명의 콜백 함수(A)는 클로저가 없지만, 7번째 줄의 addEventLisener에 의해 넘겨준 콜백 함수(B)에는 fruit이라는 외부 변수를 참조하고 있으므로 클로저가 있다. 그래서 (B) 함수가 참조할 예정인 변수 fruit에 대해서는 (A)가 종료된 후에도 가비지 컬렉터 대상에서 제외되어 계속 참조가 가능하다.

profile
늘 한결같이 꾸준히

0개의 댓글