[TIL] 클로저

Kyoorim LEE·2023년 8월 22일
0

스스로TIL

목록 보기
27/34

클로저의 다양한 정의

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

Lexical environment와 클로저

A closure is the combination of a function and the lexical environment within shich that function was declared - MDN

번역: 클로저는 함수와 그 함수가 선언될 당시의 lexical environment의 상호관계에 따르 현상

근데 선언될 당시가 언제???

VariableEnvironment

실행 컨텍스트 생성 시,
1. VariableEnvironment에 정보를 먼저 담는다
2. 그대로 복사해서 LexicalEnvironment를 만든다
3. 이후 주로 LexicalEnvironment를 활용한다

LexicalEnvironment

  • LexicalEnvironment
    1) environmentRecord
    2) outerEnvironmentReference

1) environmentRecord에 현재 컨택스트와 관련된 코드의 식별자 정보들이 저장됨
2) outerEnvironmentReference는 변수의 유효범위인 스코프를 결정함

★ 스코프: 식별자에 대한 유효범위

천천히 이해해보면,

  1. 어떤 컨텍스트 A에서 내부함수 B를 선언했는데
  2. B의 실행 컨텍스트가 활성화되면
  3. B의 outerEnvironmentReference가 참조하는 대상인 A의 LexicalEnvironment에 접근 가능해진다
  4. A에서는 B에서 선언한 변수에 접근할 수 없지만 B에서는 A에서 선언한 변수에 접근할 수 있다

MDN의 정의를 다시 곱씹어 보면,

내부함수에서 외부 변수를 참조하지 않는 경우라면 combination의 상황이 아닌 것.

즉, 내부함수에서 외부변수를 참조하는 경우에 한해서만 combination 조건이 충족함 - 이때 비로소 선언될 당시의 LexicalEnvironment를 들여다 볼 일이 생긴다는 뜻


아래 예제를 살펴보자
var outer = function() {
  var a = 1;
  var inner = function() {
    console.log(++a); // 2
  };
  inner();
};
outer(); 

1) outer 함수에서는 변수 a를 선언함
2) inner함수에서는 a를 선언하지 않고 a의 값을 1만큼 증가시키고 그 값을 출력함
3) inner함수에서 a를 선언하지 않았으므로 environmentRecord에서 값을 찾지 못함
4) outerEnvironmentReference를 뒤지기 시작 => 여기에서 상위 컨텍스트인 outer의 LexicalEnvironment에 접근해서 a를 찾는다
5) outer 함수가 종료되면 LexicalEnvironment에 저장된 식별자들(a, inner)에 대한 참조를 지움
6) 각 주소에 저장돼 있던 값들은 자신을 참조하는 변수가 하나도 없게 되므로 가비지 컬렉터의 수집 대상이 됨 🧹🧹🧹🧹🧹


예시를 하나 더 보자
var outer = function () {
   var a = 1;
  var inner = function () {
    return ++a;
  };
  return inner;
};

var outer2 = outer();

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

1) inner함수 내부에서 외부 변수 a를 사용함
2) 6번째 줄에서 inner함수를 실행한 결과가 아니라 inner함수 자체를 리턴하고 있음
3) outer 함수가 종료될 때(outer함수의 실행 컨텍스트가 종료될 때) outer2 변수는 outer의 실행결과인 inner함수를 참조하게 됨
4) outer2 함수가 호출될 때 앞서 반환된 함수 inner가 실행됨.

1) outer2를 통해서 inner함수가 실행될 때 outer 함수는 이미 종료된 상태이다
2) 어떻게 outer 함수의 LexicalEnvironment에 접근하는 거지? ==> 가비지 컬렉터 동작 방식 때문

가비지 컬렉터는 어떤 값을 참조하는 변수가 하나라도 있으면 그 값은 수집 대상에 포함 시키지 않음

outer함수가 실행 종료되어도 inner함수는 호출될 가능성이 있으므로 수집 대상에서 제외됨!


그래서 클로저가 뭔데?

클로저는 어떤 함수에서 선언한 변수를 참조하는 내부함수에서만 발생하는 현상이다.

방금 전 예시처럼,

함수의 실행 컨텍스트가 종료된 후에도 LexicalEnvironment가 가비지 컬렉션 대상이 되지 않는 경우는 지역 변수를 참조하는 내부함수가 외부로 전달된 경우가 유일

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


클로저와 메모리 관리

클로저는 의도적으로 함수의 지역변수가 계속 쓸일이 있게 만듦으로써 메모리를 소모하도록 하면서 발생하는 현상이다. 이로 인해 메모리 누수(memory leak) 위험 문제가 제기된다.

메무리 누수 위험을 근거로 클로저 사용을 조심해야한다는 주장들이 있다고 한다 👀

하지만 이를 다시 거꾸로 말하면,

지역변수의 필요성이 사라진 시점에는 더는 메모리를 소모하지 않게 처리를 해주면 클로저의 문제점이 사라진다는 말과 같다!! === 참조하는 카운트를 0으로 만들면 가비지 컬렉팅을 해갈테니까!


참조 카운트 0으로 만드는 방법

// return에 의한 클로저의 메모리 해제

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

var outer2 = outer(); // inner

console.log(outer()); // inner
console.log(outer()); // inner
outer = null // outer 식별자의 inner 함수 참조를 끊음

클로저 활용 사례

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);
  1. (A) 와 (B) 둘 다 인자로 넘겨지고 있으므로 콜백 함수임
  2. (A)의 경우 내부에서 외부 변수를 사용하지 않고 있으므로 클로저 ❌
  3. addEventListener안에서 외부변수(fruit)을 참조하고 있으므로 클로저 ⭕️
  4. (A)는 fruits의 length 만큼 실행됨 => 그때마다 새로운 실행 컨텍스트 생성됨
  5. (B)가 실행될 때는 (B)의 outerEnvironmentReference가 (A)의 LexicalEnvironment를 참조하게 된다
  6. 변수 fruit은 (A)가 종료된 후에도 GC 대상에서 제외되고 계속 참조 대상이 된다!!!

위 경우 (B)의 함수를 변수에 담아서 사용할 수도 있다.

var alertFruit = function(fruit) { 
    	alert('your choice is' + fruit);
    };

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', alertFruit); 
    $ul.appendChild($li);;
});


document.body.appendChild($ul);
alertFruit(fruits[1]) // your choise is [object PointerEvent]

"your choice is [object PointerEvent]"이 출력되는 이유는?

콜백함수의 인자에 대한 제어권은 누가 갖고 있다? ===> addEventListener

addEventListener는 콜백함수를 호출할 때 첫 번째 인자에 '이벤트 객체'를 주입하기 때문이다.


어떻게 문제 해결하면 될까?


고차함수로 해결하기
고차함수: 다른 함수를 인자로 받거나 함수를 반환하는 함수

var alertFruitBuilder = function(fruit) { 
  return function() {
   alert('your choice is' + fruit);
 	 } // alertFruit 
  };

var fruits = ['apple', 'banana', 'peach'];
var $ul = document.createElement('ul');

fruits.forEach(function (fruit) { 
	var $li = document.createElement('li');
    $li.innerText = fruit;
    $li.addEventListener('click', alertFruitBuilder(fruit); 
    $ul.appendChild($li);;
});


document.body.appendChild($ul);
alertFruit(fruits[1]) 
  1. alertFruitBuilder 함수 안에서 alertFruit 함수를 리턴한다
  2. alertFruitBuilder 함수 실행 결과가 다시 alertFruit라는 함수가 된다
  3. 이렇게 반환된 alertFruit함수를 addEventListener에 콜백함수로 전달한다.
  4. 즉, alertFruit는 alertFruitBuilder의 인자로 넘어온 fruit을 alertFruitBuilder의 LexicalEnvironment를 참조하고 있는 alertFruit의 outerEnvironmentReference에 의해 참조할 수 있게 된다.
profile
oneThing

0개의 댓글