클로저

soheey·2021년 6월 8일

클로저는 여러 함수형 프로그래밍에서 등장하는 보편적인 특징이다. 자바스크립트의 고유의 개념이아니라서 ECMAScript 명시에서도 클로저의 정의를 다루지 않고 있다. 다양한 서적에서 클로저를 한 문장으로 요약해서 설명하는 부분들은 이러하다.

  • 자신을 내포하는 함수의 컨텍스트에 접근할 수 있는 함수
  • 함수가 특정 스코프에 접근할 수 있도록 의도적으로 그 스코프에서 정의하는 것
  • 함수를 선언할 때 만들어지는 유효범위가 사라진 후에도 호출할 수 있는 함수
  • 이미 생명 주기상 끝난 함수의 변수를 참조하는 함수
  • 자유변수가 있는 함수와 자유변수를 알 수 있는 환경의 결합
  • 로컬 변수를 참조하고 있는 함수 내의 함수
  • 자신이 생성될 때의 스코프에서 알 수 있었던 변수들 중 언젠가 자신이 실행될 때 사용할 변수들만를 기억하여 유지시키는 함수

MDN에서는 '클로저는 함수와 그 함수가 선언될 당시의 lexical environment의 상호관계에 따른 현상'으로 정의하고 있다.

lexical environment란, 실행 컨텍스트의 구성 요소 중 하나인 outerEnvironmentReference에 해당한다. Lexicalenvironment의 environmentRecord와 outerEnvironmentReference에 의해 변수의 유효범위인 스코프가 결정되고 스코프 체인이 가능해진다. 어떤 컨텍스트 A에서 선언한 내부함수 B의 실행 컨텍스트가 활성화된 시점에는 B의 outerEnvironmentReference가 참조하는 대상인 A의 Lexical 환경에 접근이 가능하다.

그러나 내부함수에서 외부 변수를 참조하지 않는 경우 등 내부함수 B가 A의 Lexical 환경을 사용하지 않기도 한다. 즉, 내부함수에서 외부 변수를 참조하는 경우에 한해서만 combination(선언될 당시의 Lexical 환경과의 상호관계)이 의미가 있다.

예제1 외부 함수의 변수를 참조하는 내부 함수

var outer = function () {
  var a = 1;
  
  var inner = function () {
    console.log(++a);
  };
  inner();
};

outer(); // 2

inner 함수 내부에서는 a를 선언하지 않았기 때문에 environmentRecord에서 값을 찾지 못하므로, outerEnvironmentReference에 지정된 상위 컨텍스트인 outer의 LexicalEvironment에 접근해서 다시 a를 찾는다.

outer 함수의 실행 컨텍스트가 종료되면 LexicalEnvironment에 저장된 식별자들(a, inner)에 대한 참조가 삭제된다. 그러면 각 주소에 저장되어 있던 값들은 자신을 참조하는 변수가 하나도 없게 되므로 Garbage collector의 수집 대상이 된다.

일반적인 상황에서의 콜스택 흐름


일반적인 함수 및 내부함수에서의 콜스택 동작과 동일하다.

예제2 외부 함수의 변수를 참조하는 내부 함수

var outer = function () {
  var a = 1;
  
  var inner = function () {
    return ++a;
  };
  
  return inner();
};

var outer2 = outer();
console.log(outer2); // 2

inner 함수 내에서 실행 결과를 return 한다. 결과적으로 outer 함수의 실행 컨텍스트가 종료된 시점에는 a 변수를 참조하는 대상이 없으므로, a와 inner 변수 값들은 GC에 의해 소멸하게 된다.

1과 2는 outer 함수의 실행 컨텍스트가 종료되기 이전에 inner 함수의 실행 컨텍스트가 종료돼 있으며, 이후 별도로 inner 함수를 호출할 수 없다는 공통점이 있다.

예제3 외부 함수의 변수를 참조하는 내부 함수

var outer = function () {
  var a = 1;
  
  var inner = function () {
    return ++a;
  };
  
  return inner;
};

var outer2 = outer();
console.log(outer2()); // 2
console.log(outer2()); // 3

outer 함수에서 inner 함수 자체를 반환한다. 그러면 outer 함수의 실행 컨텍스트가 종료될 때, outer2 변수는 outer의 실행 결과인 inner 함수를 참조하게 된다. 이후 콘솔에 outer2 함수를 호출하면 앞서 반환된 inner 함수가 실행된다. 따라서 outer2 함수의 호출을 계속할수록 a의 값이 상승한다.

inner 함수의 실행 컨텍스트의 environmentRecord에는 수집할 정보가 없다. outerEnvironmentReference에는 inner 함수가 선언된 위치의 LexicalEnvironment가 참조복사된다.

inner 함수는 outer 함수의 내부에서 선언됐으므로, outer 함수의 LexicalEnvironment에 담긴다. 스코프 체이닝에 따라 outer에서 선언한 변수 a에 접근하여 1씩 증가시킨 후 그 값인 2를 반환하고, inner 함수의 실행 컨텍스트가 종료된다. 이후 다시 outer2 함수를 호출하면 같은 방식으로 a의 값을 2에서 3으로 증가시킨 후 3을 반환한다.

inner 함수의 실행 시점에 outer 함수는 이미 실행이 종료된 상태인데 outer 함수의 LexicalEnvironment에 접근할 수 있는 이유는 Garbage colletor의 동작 방식 때문이다.
GC는 어떤 값을 참조하는 변수가 하나라도 있다면 그 값은 수집 대상에 포함시키지 않는다.

outer 함수는 실행 종료 시점에 inner 함수를 반환한다. 따라서 외부함수인 outer의 실행이 종료되더라도 내부 함수인 inner 함수는 언젠가 outer2를 실행함으로써 호출될 가능성이 열려 있다. 언젠가 inner 함수의 실행 컨텍스트가 활성화되면 outerEnvironmentReference가 outer 함수의 LexicalEnvironment를 필요로 할 것이므로 수집 대상에서 제외된다. 때문에 inner 함수가 이 변수에 접근할 수 있다.

클로저 발생 시의 콜스택 흐름

클로저는 어떤 함수에서 선언한 변수를 참조하는 내부 함수에서만 발생하는 현상이다. 1과 2에서는 일반적인 함수와 같이 outer의 LexicalEnvironment에 속하는 변수가 모두 GC의 수거대상이 된 반면, 3에서는 변수 a가 GC의 대상에서 제외되었다.

이처럼 함수의 실행 컨텍스트가 종료된 후에도 LexicalEnvironment가 GC의 수집 대상에서 제외되는 경우는, 지역변수를 참조하는 내부함수가 외부로 전달된 경우가 유일하다.

즉, 클로저는 "외부 함수의 LexicalEnvironment가 GC되지 않은 현상"이다. 다시 말해, 클로저는 "어떤 함수 A에서 선언한 변수 a를 참조하는 내부함수 B를 외부로 전달할 경우, A의 실행 컨텍스트가 종료된 이후에도 변수 a가 사라지지 않는 현상"이다.

주의할 점은, return 뿐만 아니라 다른 경우에서도 클로저가 발생한다는 점이다.

예제4 return 없이 클로저가 발생하는 경우

// setIntercal / setTimeout

(function () {
  var a = 0;
  var intervalId = null;
  var inner = function () {
    if (++a >= 10) {
      clearInterval(intervalId);
    }
    console.log(a);
  };
  intervalId = setInterval(inner, 100);
})();
// eventListener

(function () {
  var count = 0;
  var button = document.createElement('button');
  button.innerText = 'click';
  button.addEventListener('click', function () {
    console.log(++count, 'times clicked');
  });
  document.body.appendChild(button);
})();

별도의 외부객체인 window의 메서드(setIntercal / setTimeout)에 전달할 콜백 함수 내부에서 지역변수를 참조한다.

// eventListener

(function () {
  var count = 0;
  var button = document.createElement('button');
  button.innerText = 'click';
  button.addEventListener('click', function () {
    console.log(++count, 'times clicked');
  });
  document.body.appendChild(button);
})();

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

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

레퍼런스: 코어 자바스크립트

0개의 댓글