TIL DAY.47 [코어 자바스크립트] 클로저

Dan·2020년 12월 5일
0

오늘은 면접에서 굉장히 많이 물어보면서 이해하기 어려운 클로저에 대해서 알아보는 시간을 갖도록 하겠다.

클로저의 의미 및 원리 이해

클로저(Closure)는 여러 함수형 프로그래밍 언어에 등장하는 보편적인 특성이다. 자바스크립트 고유의 개념이 아니라서 EMCAscript 명세에서도 클로저의 정의를 다루지 않고 있고,그래서 그런지 다양한 문헌에서 제각각 클로저를 다르게 정의하고 있다. 클로저를 정의한 내용을 종합해보면 클로저란 "어떤 함수에서 선언한 변수를 참조하는데 내부함수에서만 발생하는 현상" 이라고 표현 할 수 있다. 이를 글로서는 이해하기 어렵기 때문에 예제를 보면서 이해를 해보도록 하자.

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

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

위의 예제를 해석하자면,
1. outer함수에서 변수 a를 선언했고, outer의 내부함수인 inner 함수에서 a의 값을 1만큼 증가시킨 다음 출력한다.
2. inner함수 내부에서는 a를 선언하지 않았기 때문에 envirnomentRecord에서 값을 찾지 못하므로 outerEnvironmentReference에 지정된 상위 컨텍스트인 outer의 LexicalEnvironment에 접근해서 다시 a를 찾는다.
3. 4번째 줄에서는 2가 출력되고, outer함수의 실행 컨텍스트가 종료되면 LexicalEnvrionment에 저장된 식별자들(a, inner)에 대한 참조가 지워진다.
4. 그러면 각 주소에 저장돼 있던 값들은 자신을 참조하는 변수가 하나도 없게 되므로 가비지 컬렉터에 수집 대상이 될 것이다.

이번에는 비슷하지만 좀 다른 예제를 살펴 보자.

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

이번에는 6번째 줄에서 inner함수의 실행 결과가 아닌 inner함수 자체를 반환한 예제이다.
그러면 outer함수의 실행 컨텍스트가 종료될 때 outer2 변수는 outer의 실행결과인 inner함수를 참조하게 될 것이다.이후 9번째 줄에서 outer2를 호출하면 앞서 반환된 함수인 inner가 실행되며 결과로 각각 2와 3을 출력하게 될 것이다.

그렇다면, 예제 (2)는 (1)과 다르게 inner함수의 실행 시점에는 outer함수는 이미 실행이 종료된 상태인데 outer 함수의 LexicalEnvironment에 어떻게 접근 할 수 있었던 걸까?

그 이유는 바로 가비지 컬렉터의 동작 방식 때문이다. 가비지 컬렉터는 어떤 값을 참조하는 변수가 하나라도 존재한다면 그 값을 수집 대상에 포함시키지 않는다. 예제 (2)의 outer함수는 실행 종료 시점에 inner 함수를 반환했다. 외부 함수인 outer의 실행이 종료되더라도 내부함수인 inner 함수는 언젠간 outer2를 실행함으로써 호출될 가능성이 열린 것이다. 언젠가 inner 함수의 실행 컨텍스트가 활성화되면 outerEnvironmentReference가 outer함수의 LexicalEnvironment를 필요로 할 것이므로 수집 대상에서 제외되고 그 덕분에 inner함수가 이 변수에 접근 할 수 있게되는 것이다.

그렇다면, 클로저란 무엇일까??

클로저는 어떤 함수에서 선언한 변수를 참조하는 내부함수에서만 발생하는 현상이라고 앞에서 말 했었다. 예제 (1)은 outer의 LexicalEnvironment에 속하는 변수가 모두 가비지 컬렉팅 대상에 포함된 반면 예제 (2)는 변수 a가 대상에서 제외됬다. 즉, 앞에 정의한 클로저 개념이란 "외부 함수의 LexicalEnvironment가 가비지 컬렉팅되지 않는 현상"이라고 할 수 있다. 이를 좀 더 풀어서 말하자면,

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

클로저와 메모리 관리

메모리 누수의 위험을 이유로 클로저 사용을 조심해야 한다거나 심지어 지양해야 한다고 주장하는 사람들도 있지만 메모리 소모는 클로저의 본질적인 특성일 뿐이다. 의도대로 설계한 '메모리 소모'에 대한 관리법만 잘 파악해서 적용하면 상관없다.

관리 방법은 간단하다. 클로저는 어떤 필요에 의해 의도적으로 함수의 지역변수를 메모리를 소모하도록 함으로써 발생한다. 그렇다면 그 필요성이 사라진 시점에는 더는 메모리를 소모하지 않게 해주면 된다. 참조 카운트를 0으로 만들면 언젠가 GC가 수거해갈 것이고, 이때 소모됐던 메모리가 회수될 것이다.

그렇다면, 참조 카운트 0으로 만드는 방법은?
식별자에 참조형이 아닌 기본형 데이터(보통 null이나 undefined)를 할당하면 된다. 예제를 보며 생각해보자.

var outer = (function () {
	var a = 1;
  	var inner = function () {
      	return ++a;
    };
  	return inner;
})();
console.log(outer());
console.log(outer());
outer = null; //outer 식별자의 inner함수 참조를 끊음

클로저의 활용 사례

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

다음은 대표적인 콜백 함수 중 하나인 이벤트 리스너에 관한 예시이다. 클로저의 '외부 데이터'에 주목하면서 흐름대로 따라가보자.

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

fruits.forEach(function(fruit) { //(A)
  var $li = document.createElement('li');
  $li.innerText = fruit;
  $li.addEventListner('click', function() { //(B)
    alert('your choice is' + fruit);
  });
  $ul.appendChild($li);
});
document.body.appendChild($ul);
  1. fruits 변수를 순회하며 li를 생성하고 각 li를 클릭하면 해당 리스너의 콜백 함수가 실행된다.
  2. forEach 메서드에 넘겨준 익명의 콜백 함수(A)는 내부에서 외부 변수를 사용하지 않으므로 클로저가 없다.
  3. addEventListener에 넘겨준 콜백 함수(B)에는 함수내의 fruit라는 외부 변수를 참조하고 있으므로 클로저가 있다.
  4. (A)는 fruits의 개수만큼 실행되며, 그때마다 새로운 실행 컨텍스트가 생성된다.
  5. (A)의 실행 종료 여부와 무관하게 클릭 이벤트에 의해 각 컨텍스트의 (B)가 실행될 때는 (B)의 outerEnvironmentReference가 (A)의 LexicalEnvironment를 참조하게 된다.
  6. 따라서 (B)함수가 참조할 예정인 변수 fruit 에 대해서는 (A)함수가 종료된 후에도 CG대상에서 제외되어 계속 참조가 가능하다.

그런데 (B)함수의 쓰임이 콜백 함수에 국한되지 않는 경우라면 반복을 줄이기 위해 (B)함수를 외부로 분리하는 편이 나을 수 있다. 따라서 다음은 fruit을 인자로 받아 출력하는 형태이다.

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

var alertFruit = function(fruit) {
  alert('your choice is ' + fruit);
};
fruits.forEach(function(fruit) {
  var $li = document.createElement('li');
  $li.innerText = fruit;
  $li.addEventListener('click', alertFruit);
  $ul.appendChild($li);
});
document.body.appendChild($ul);
alertFruit(fruits[1]);
  • 공통 함수로 사용하고자 콜백 함수를 외부로 꺼내어 alertFruit라는 변수에 담았다. alertFruit를 직접 실행할 수 있게 되었다.
  • 하지만 각 li를 클리갛면 클릭한 대상의 과일명이 아닌 [objectMouseEvent]라는 값이 출력되는데 이는 콜백 함수의 인자에 대한 제어권을 addEventListener가 가진 상태이며, addEventListener는 콜백 함수를 호출할 때 첫 번째 인자에 "이벤트 객체"를 주입하기 때문이다.

이 문제는 bind메서드를 활용하면 해결 할 수 있다.

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

var alertFruit = function(fruit) {
  alert('your choice is ' + fruit);
};
fruits.forEach(function(fruit) {
  var $li = document.createElement('li');
  $li.innerText = fruit;
  $li.addEventListener('click', alertFruit.bind(null, fruit));
  $ul.appendChild($li);
});
document.body.appendChild($ul);
  • 하지만 bind를 활용하면 이벤트 객체가 인자로 넘어오는 순서가 바뀌는 점과, 함수 내부에서 this가 참조하는 값이 달라지는점을 감안해야 된다.
  • 이러한 변경사항 마저 발생하지 않게 만들려면 bind 메서드가 아닌 다른 방식으로 만들어야 한다.
var fruits = ['apple', 'banana', 'peach'];
var $ul = document.createElement('ul');

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', alertFruitBuilder(fruit));
  $ul.appendChild($li);
});
document.body.appendChild($ul);
  • 고차함수란 함수를 인자로 받거나 함수를 리턴하는 함수이다.
  1. alertFruit함수 대신 alertFruitBuilder라는 이름의 함수를 작성했다. alertFruitBuilder 함수 내부에서는 다시 익명함수를 반환한다.
  2. 이 익명함수 내부의 코드가 기존의 alertFruit 함수의 코드이다.
  3. alertFruitBuilder 함수가 실행하면서 fruit 값을 인자로 전달하면, 함수의 실행 결과가 다시 함수 (return function)가 되며, 이렇게 반환된 함수를 리스너의 콜백 함수로써 전달 할 것이다.
  4. 클릭 이벤트가 발생하면 이 함수의 실행 컨텍스트가 열리면서 alertFruitBuilder의 파라미터로 넘어온 fruit를 outerEnvironmentReference에 의해 참조할 수 있게된다. 즉, alertFruitBuilder의 실행 결과로 반환된 함수에는 클로저가 존재한다.

콜백 함수 내부에서 외부변수를 참조하기 위한 방법 정리.

  1. 콜백 함수를 내부함수로 선언하여 외부변수를 직접 참조하는법. (GC의 참조카운트 이용)
  2. bind 메서드를 활용하여 값을 직접넘겨주는 방법. 클로저는 발생하지 않지만 몇가지 제약이 생긴다.
  3. 콜백 함수를 고차함수로 바꿔서 클로저를 적극적으로 활용하는 방법.

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

to be continued..

profile
만들고 싶은게 많은 개발자

0개의 댓글