TIL-52 JavaScript 클로저

PRB·2021년 11월 11일
0

JavaScript

목록 보기
17/24
post-thumbnail

1. 클로저

MDN에서는 클로저에 대해 클로저는 함수와 그 함수가 선언될 당시의 lexical environment의 상호 관계에 따른 현상이라고 한다.

선언될 당시의 lexical environment는 실행 컨텍스트의 구성 요소 중 하나인 outerEnvirnment에 해당한다. lexical environment의 envirnmentRecord 와 outerEnvirnmentReference에 의해 변수의 유효범위인 스코프가 결정되고 스코프 체인이 가능하다.

어떤 컨텍스트 A에서 선언한 내부 함수 B의 실행 컨텍스트가 활성화된 시점에는 B의 outerEnvirnmentReference가 참조하는 대상인 A의 Lexical environment에도 접근이 가능하다. A에서는 B에서 선언한 변수에 접근할 수 없지만 B에서는 A에서 선언한 변수에 접근 가능합니다.

여기서 combination의 의미를 파악할 수 있다.
내부 함수 B가 A의 Lexical environment를 언제나 사용하는 것은 아니다. 내부 함수에서 외부 변수를 참조하지 않는 경우라면 combination라고 할 수 없다. 내부 함수에서 외부 변수를 참조하는 경우에 한해서만 combination, 즉 선언될 당시의 Lexical environment와의 상호 관계가 의미가 있을 것이다.

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

위 코드는 외부함수에서 변수를 선언하고 내부함수에서 해당 변수를 참조하는 형태의 코드이다.
1. outer 함수에서 변수 a를 선언했고, outer의 내부함수인 inner 함수에서 a의 값을 1만큼 증가시킨 다음 출력한다.

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

  2. outer 함수의 실행 컨텍스트가 종료되면 LexicalEnvironment에 저장된 식별자들(a,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 

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

inner 함수의 실행 컨텍스트 environmentRecord에는 수집할 정보가 없다.
outer-EnvironmentReference에는 inner 함수가 선언된 위치의 LexicalEnvironment가 참조 복사된다. inner 함수는 outer 함수 내부에서 선언됐으므로, outer 함수의 LexicalEnvironment가 담길 것이다.

여기서 이상한 부분이 있다.
inner 함수의 실행 시점에는 outer 함수는 이미 실행이 종료된 상태인데 outer 함수의 실행 시점에는 outer 함수의 LexicalEnvironment에 어떻게 접근할 수 있는 걸까?
이것은 바로 가비지 컬렉터의 동작 방식 때문이다.
가비지 컬렉터는 어떤 값을 참조하는 변수가 하나라도 있다면 그 값은 수집 대상에 포함시키지 않는다.
outer 함수는 실행 종료 시점에 inner 함수를 반환한다.
외부 함수인 outer의 실행이 종료되더라도 내부 함수인 inner 함수는 언젠가 outer2를 실행함으로써 호출될 가능성이 열린 것이다.
언젠가 inner 함수의 실행 컨텍스트가 활성화되면 outer-EnvironmentReference가 outer 함수의 LexicalEnvironment를 필요로 할 것이므로 수집 대상에서 제외된다.
이러한 이유로 inner 함수가 이 변수에 접근할 수 있는 것이다.

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

return 없이도 클로저가 발생하는 경우도 있다.
1. setInterval / setTimeout
별도의 외부객체인 window의메서드에 전달한 콜백 함수 내부에서 지역변수를 참조 한다.
2. eventListener
별도의 외부객체인 DOM의 메서드에 등록할 handler 함수 내부에서 지역변수를 참조한다.
두 상황 모두 지역변수를 참조하는 내부함수를 와부에 전달했기 때문에 클로저

2. 클로저 메모리 관리

클로저는 어떤 필요에 의해 의도적으로 함수의 지역변수를 메모리를 소모하도록 함으로써 발생한다. 그렇다면 그 필요성이 사라진 시점에는 더는 메모리를 소모하지 않게 해주면 된다. 참조 카운트를 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 함수 참조를 끊음

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

(function () {
	var a = 0;
 	var intervalId = null;
 	var inner = function () {
       if (++a >= 10) {
           clearInterval(intervalId);
         	inner = null;  // inner 식별자의 함수 참조를 끊음  
       }
     	console.log(a);
  };
  intervalId = setInterval(inner, 1000);
})();

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

(function () {
	var count = 0;
 	var button = document.creatElement('button');
	button.innterText = 'click'

   var clickHandler = function () {
     	console.log(++count, 'time clicked');
     	if (count >= 10) {
         	button.removeEventListener('click', clickHandler);
         	clickHandler = null; // clickHandler 식별자의 함수 참조를 끊음
        }
   
   };

3. 클로저의 활용 사례

1. 콜백 함수 내부에서 외부 데이터를 사용하고자 할 때

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

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

fruites 변수를 순회하며 li를 생성하고, 각 li를 클릭하면 해당 리스너에 기억된 콜백 함수를 실행하게 했다. forEach 메서드에 넘겨준 익명의 콜백 함수 A는 그 내부에서 외부 변수를 사용하지 않고 있으므로 클로저가 없지만
addEventListener에 넘겨준 콜백 함수 B에는 fruit이라는 외부 변수를 참조하고 있으므로 클로저가 있다.
A는 fruits의 개수만큼 실행되며, 그때마다 새로운 실행 컨텍스트가 활성화될 것이며 A의 실행 종료 여부와 무관하게 클릭 이벤트에 의해 각 컨텍스트의 B가 실행될 때는 B의 outerEnvirnmentReference가 A의 Lexical environment를 참조하게 될 것이다.
따라서 최소한 B 함수가 참조할 예정인 변수 fruit에 대해서는 A가 종료된 후에도 GC 대상에서 제외되어 계속 참조 가능할 것이다.

2. 접근 권한 제어(정보 은닉)

정보 은닉은 어떤 모듈의 내부 로직에 대해 외부로의 노출을 최소화해서 모듈 간의 결합도를 낮추고 유연성을 높이고자 하는 현대 프로그래밍 언어의 중요한 개념 중 하나입니다.
흔히 접근 권한에는 public, private, protected의 세 종류가 있습니다.

자바스크립트는 기본적으로 변수 자체에 이러한 접근 권한을 직접 부여하도록 설계돼 있지 않습니다. 그렇다고 접근 권한 제어가 불가능한 것은 아니다.
클로저를 이용하면 함수 차원에서 public 한 값과 private 한 값을 구분하는 것이 가능하다.

외부에 제공하고자 하는 정보들을 모아서 return 하고, 내부에서만 사용할 정보들은 return 하지 않는 것으로 접근 권한 제어가 가능한 것이다.
return 한 변수들은 공개 멤버가 되고, 그렇지 않은 변수들은 비공개 멤버가 되는 것이다.

profile
사용자 입장에서 사용자가 원하는 것을 개발하는 프론트엔드 개발자입니다.

0개의 댓글