[책정리] CoreJavaScript 5-1 클로저의 의미 및 원리 이해

이진규·2022년 9월 3일
0
post-thumbnail

Q. 클로저가 뭐에요?

클로저는 자바스크립트의 문법이 아니라 함수형 프로그래밍 언어에서 등장하는 어떤 특성입니다. 그래서 ECMASCript 명세에서도 클로저의 정의를 다루지 않으며 여러 곳에서 클로저를 다양하게 설명하고 있습니다.

MDN에서는 클로저에 대해

A closure is the combination of a function 
and the lexical environment within which that function was declared.

라고 설명하는데, 직역해보면 클로저는 함수와 그 함수가 선언될 당시의 Lexical Environment의 상호관계에 따른 현상 이라고 말할 수 있습니다.

Q. 선언될 당시의 Lexical Environment?

이것은 2장에서 얘기했던 실행 컨텍스트의 구성 요소 중 하나인 outerEnvironmentReference를 말합니다. 어떤 컨텍스트 A에서 선언한 내부함수 B의 실행 컨텍스트가 활성화된 시점에서 B는 outerEnvironmentReference가 참조하는 대상인 A의 LexicalEnvironment에도 접근이 가능하게 됩니다. A는 B에서 선언한 변수에 접근할 수 없지만 B는 A에서 선언한 변수에 접근이 가능합니다.

01 var outer = function() {
02   var a = 1;
03   var inner = function() {
04     console.log(++a); // 2
05   };
06   inner();
07 };
08 outer();

outer함수의 실행 컨텍스트에서 선언한 inner함수에서는 outer에서 선언한 a에 접근할 수 있습니다. outer함수의 실행 컨텍스트가 종료되면 LexicalEnvironment에 저장된 식별자들인 a와 inner에 대한 참조를 지우고 각 주소에 저장되어 있던 값들은 자신을 참조하는 변수가 없게 되므로 가비지 컬렉터의 수집 대상이 될 것입니다.

그림으로 보면 outer함수의 실행 컨텍스트가 종료되면 101,102번 주소에 있는 값을 지우고 201,202번 주소는 자신들을 가리키는, 참조하는 변수가 없게 되므로 201,202번 주소에 있는 값들도 가비지 컬렉터에 의해 지워지는 것이 아닐까 하고 이해했습니다.

정리하자면, inner함수가 선언될 당시의 Lexical Environment에 포함된 즉, outer함수에서 선언된 변수 a와 내부 함수인 inner함수와의 상호작용(console.log(++a))가 있으므로 이러한 현상을 클로저라 할 수 있습니다.
그런데 특별한 점은 없기에 다음 예제를 확인해봅시다.

01 var outer = function() {
02   var a = 1;
03   var inner = function() {
04     return ++a;
05   };
06   return inner();
07 };
08 var outer2 = outer(); // 2
09 console.log(outer2);

이번에도 outer함수에서 선언된 변수 a를 내부함수인 inner함수에서 사용했습니다. 6번째 줄에서 inner함수를 실행한 결과를 리턴하고 있으므로 결국 outer함수의 실행 컨텍스트가 종료되면 a 변수를 참조하는 대상이 없어집니다. outer2변수에는 2라는 값만 담기게 되므로 a, inner변수의 값들은 언젠가 가비지 컬렉터에 의해 소멸할 것입니다.
그런데 지금까지는 값만 저장했는데 inner함수를 리턴해준다면, outer함수의 실행 컨텍스트가 종료되었을 때에도 inner함수를 실행할 수 있지 않을까요?

01 var outer = function() {
02   var a = 1;
03   var inner = function() {
04     return ++a;
05   };
06   return inner;
07 };
08 var outer2 = outer();
09 console.log(outer2()); // 2
10 console.log(outer2()); // 3

이렇게 6번째 줄에서 inner함수의 실행 결과가 아닌 inner함수 자체를 반환했습니다. 그렇다면 outer함수의 실행 컨텍스트가 종료되어도 변수 outer2에 inner함수를 담았으므로 inner함수를 실행시킬 수 있습니다.
inner함수의 environmentRecord에는 수집할 정보가 없지만, outerEnvironmentReference에는 outer함수 안에 선언된 변수인 a가 참조복사될 것입니다.

09 console.log(outer2()); // 2
10 console.log(outer2()); // 3

그런데 9, 10번째 줄에서 outer2에 담긴 inner함수는 outer함수 안에서 선언된 a에 접근하게 되는데, 9, 10번째 줄에서 outer함수는 종료된 이후인데 어떻게 outer함수 안에 선언된 a에 접근할 수 있는 것일까요?
이는 가비지 컬렉터의 동작 방식 떄문인데, 가비지 컬렉터는 어떤 값을 참조하는 변수가 하나라도 있다면 그 값은 수집 대상에 포함시키지 않습니다. outer2에 inner함수가 담겨있고 inner함수는 변수 a를 참조하고 있으므로 언젠가 outer2를 통해 a에 접근할 필요가 있을테니 a가 선언된 outer함수가 종료되더라도 변수 a를 없애지 않는 것이죠.

이를 정리해서 클로저를 다시 정의해보면

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

이라고 할 수 있습니다.

Q. 외부로 어떻게 전달해요?

지금까지의 예제처럼 return을 통해 전달하는 경우도 있고 setInterval, eventListener 등 다양한 경우가 있습니다.

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

setInterval/setTimeout함수는 외부 객체인 window의 메소드에 전달할 콜백함수 내부에서 지역변수를 참조합니다. 즉시실행함수 내부에서 선언된 a를 window의 메소드에서 참조하게 됩니다.

2) 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 함수 내부에서 지역 변수인 count를 참조합니다.
두 상황 모두 즉시 실행 함수가 종료되어도 즉시 실행 함수 내부에서 선언한 변수 a/count가 사라지지 않습니다.

감사합니다.

참고

  1. MDN - Closure
  2. 정재남, ⌜코어 자바스크립트⌟, 위키북스, 2022, 115 - 122쪽
  3. 표지 근처 아이콘, 제작자: Pixel perfect - Flaticon
profile
개발자

0개의 댓글