[JS] 클로저(Closure)

박먼지·2022년 12월 13일
0
post-thumbnail

처음에 한번 읽었을 때 이해가 안돼서 2회독 하면서 정리했더니 조금 알 것 같기도 하면서.. 또 모를 것 같기도 하면서..(???) 알 것 같다.

1. 클로저란?

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

이게 무슨 말이냐 하면... 예제로 araboza.

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에서 선언한 변수 a를 참조하는 내부함수 inner를 외부로 전달 (return) 할 경우 outer의 실행 컨텍스트가 종료된 이후 (outer()) 에도 변수 a가 사라지지 않는 현상을 말한다.

inner함수의 실행 시점에는 outer 함수가 이미 실행이 종료 되었는데 어떻게 outer 함수의 LexicalEnvironment(변수 a)에 접근할 수 있는 것일까?

이는 가비지 컬렉터의 동작 방식 때문이다.

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

outer 함수가 실행 종료 되더라도 outer 함수의 내부 함수인 inner 함수는 outer2에 참조되어있어 언제든 호출될 가능성이 있기 때문에 가비지 컬렉터의 수집 대상에서 제외된다.

따라서 inner 함수가 outer 함수의 LexicalEnvironment(변수 a)에 접근할 수 있는 것이다!

이처럼 내부 함수가 외부로 전달되는 경우는 return 뿐만 아니라 여러가지 경우가 있다.

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

window의 메서드 setInterval(외부)에 전달할 콜백 함수 inner 내부에서 지역변수 a를 참조한다.

(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);
})();
// 1 times clicked
// 2 times clicked ...

외부 객체인 DOM의 메서드 addEventListener에 등록할 함수

function() {
    console.log(++count, 'times clicked');
  };

내부에서 지역변수 count를 참조한다.

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

2. 클로저와 메모리 관리

클로저는 의도적으로 함수의 지역변수를 메모리에서 지워지지 않도록 하기 때문에 클로저의 필요성이 사라진 시점에는 메모리 관리를 해줘야 한다.

식별자에 참조형이 아닌 기본형 데이터(null이나 undefined)를 할당하면 참조 카운트가 0이 되면서 가비지 컬렉터의 수거 대상이 된다.

위의 예제들에 메모리 해제 코드를 추가해보자.

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

var outer2 = outer();

console.log(outer2());  // 2
console.log(outer2());  // 3
outer2 = nul; // outer2 식별자의 inner 함수 참조를 끊음

// 예제 2
(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);
})();
// 1 2 3 4 5 ... 10

// 예제 3
(function () {
  var count = 0;
  var button = document.createElement('button');
  button.innerText = 'click';
  
  var clickHandler = function () {
  	console.log(++count, 'times clicked');
    if (count >= 10) {
      button.removeEventListener('click', clickHandler);
      clickHandler = null; // clickHandler 식별자의 함수 참조를 끊음
    }
  };
  
  button.addEventListener('click', clickHandler);
  document.body.appendChild(button);
})();

3. 클로저 활용 사례

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


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

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

// 고차 함수
var alertFruitBuilder = function (fruit) {
	return function() {
		alert('your choice is ' + fruit);
    }
};

fruits.forEach(function(fruit){
 	var $li = document.createElement('li');
    $li.innerText = fruit;
    $li.addEventListener('click', 
// 1. 콜백 함수를 내부함수로 선언해서 외부 변수를 직접 참조하는 방법
    function () {
      alert('your choice is ' + fruit);
    };
// 2. bind 활용
   alertFruit.bind(null, fruit);   
// 3. 고차함수 활용
   alertFruitBuilder(fruit)
 );
    $ul.appendChild($li);
 });
  
document.body.appendChild($ul);

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

클로저를 이용하면 함수 차원에서 public과 private한 값을 구분할 수 있다.

외부에 제공하고자 하는 정보는 return하고, 내부에서만 사용할 정보는 return하지 않는 것으로 접근 권한 제어가 가능하다.

var createCar = function () {
	var fuel = Math.ceil(Math.random() * 10 + 10); // 연료 (L)
  	var power = Math.ceil(Math.random() * 3 + 2); // 연비 (km/L)
   	var moved = 0;

    var publicMebers = {
    	get moved() {
          return moved;
        },
        run: function () {
          var km = Math.ceil(Math.random() * 6);
          var wasteFuel = km / power;
          if (fuel < wasteFuel) {
            console.log('이동불가');
            return;
          };
          fuel -= wasteFuel;
          moved += km;
          console.log(km + 'km 이동 (총 ' + moved + 'km). 남은 연료: ' + fuel);
        }
    };
    Object.freeze(publicMebers); // run 어뷰징 막음
    return publicMebers;
};
  
var car = createCar();
    
// 
    

부분 적용 함수

profile
개발괴발

0개의 댓글