아카이브/JavaScript

[코어 자바스크립트] 05 클로저

kaaay 2021. 12. 6. 22:57
728x90

 

  1. 클로저의 의미 및 원리 이해
  2. 클로저와 메모리 관리
  3. 클로저 활용 사례

1. 클로저의 의미 및 원리 이해

클로저(Closure)는 여러 함수형 프로그래밍 언어에서 등장하는 보편적인 특성이다. 

"A closure is the combination of a function and the lexical environment within which that function was declared." - MDN(Mozilla Developer Network)
  • 클로저는 함수와 그 함수가 선언될 당시의 lexical environment의 상호관계에 따른 현상이다.
  • 어떤 함수에서 선언한 변수를 참조하는 내부 함수에서만 발생하는 현상이다.

 

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

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

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

outer함수는 실행 종료 시점에 inner 함수를 반환하기 때문에 외부 함수인 outer의 실행이 종료되더라도 내부함수인 inner는 언젠가 outer2를 실행함으로써 호출될 가능성이 있다.

언젠가 inner 함수의 실행 컨텍스트가 활성화되면 outerEnvironmentReference가 outer함수의 LexicalEnvironment를 필요로 할 것이므로 수집대상에서 제외된다.

 

어떤 함수에서 선언한 변수를 참조하는 내부 함수에서만 발생하는 현상이란, "외부 함수의 LexicalEnvironmet가 가비지 컬렉팅되지 않는 현상"

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

단, '외부로 전달'이 곧 return만을 의미하는 것은 아니다.


2. 클로저와 메모리 관리

클로저는 객체지향과 함수형 모드를 아우르는 매우 중요한 개념이다.

메모리 누수의 위험을 이유로 클로저 사용을 조심해야 한다거나, 지양해야 한다고 주장하는 사람들도 있지만 메모리 소모는 클로저의 본질적인 특성일 뿐이다.

 

클로저는 어떤 필요에 의해 의도적으로 함수의 지역변수가 메모리를 소모하도록 함으로써 발생한다.

필요성이 사라진 시점에서 참조카운트를 0으로 만들면 언젠가 가비지 컬렉터가 수거해갈 것이고, 이때 소모됐던 메모리가 회수된다.

참조 카운트를 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 함수 참조를 끊음

3. 클로저 활용 사례

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

[콜백 함수와 클로저]

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

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

 

[콜백 함수와 클로저2]

...
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(fruit));
    $li.appendChild($li);
});
document.body.appendChild($ul);
alertFruit(fruits[1]);

각 li를 클릭하면 클릭한 대상의 과일명이 아닌 [object MouseEvent]라는 값이 출력된다.

콜백 함수의 인자에 대한 제어권을 addEventListener가 가진 상태이며, addEventListener는 콜백 함수를 호출할 때 첫 번째 인자에 '이벤트 객체'를 주입하기 때문.

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

 

[콜백 함수와 클로저 3]

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

bind 메서드를 사용하면 이벤트 객체가 인자로 넘어오는 순서가 바뀌고, 함수 내부에서의 this가 원래의 그것과 달라지는 변경사항이 발생한다.

 

[콜백 함수와 클로저 4]

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

콜백 함수를 고차함수로 바꿔서 클로저를 적극적으로 활용한 방법.

 

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

정보 은닉 (information hiding)은 어떤 모듈의 내부 로직에 대해 외부로의 노출을 최소화해서 모듈 간의 결합도를 낮추고 유연성을 높이고자 하는 현대 프로그래밍 언어의 중요한 개념 중 하나이다.
  • public
  • private
  • protected

 

자바스크립트는 기본적으로 변수 자체에 이러한 접근 권한을 직접 부여하도록 설계되어 있지 않다.

하지만 클로저를 이용하면 훔수 차원에서 public한 값과 private한 값을 구분하는 것이 가능하다.

 

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

return을 활용한 클로저로 외부 스코프에서 함수 내부의 변수들 중 선택적으로 일부 변수에 대한 접근 권한을 부여할 수 있다.

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

 

부분 적용 함수

부분 적용 함수(partially applied function)란?
n개의 인자를 받는 함수에 미리 m개의 인자만 넘겨 기억시켰다가, 나중에 (n-m)개의 인자를 넘기면 비로소 원래 함수의 실행 결과를 얻을 수 있게끔 하는 함수.

[bind 메서드를 활용한 부분 적용 함수]

var add = function () {
	var result = 0;
	for (var i = 0l i < arguments.length; i++) {
		result += arguments[i];
	}
	return result;
};
var addPartial = add.bind(null, 1, 2, 3, 4, 5);
console.log(addPartial(5, 6, 7, 8, 9, 10);  // 55

단, this의 값을 변경할 수밖에 없기 때문에 메서드에서는 사용할 수 없다.

 

커링 함수

커링 함수(currying function)란 여러 개의 인자를 받는 함수를 하나의 인자만 받는 함수로 나눠서 순차적으로 호출될 수 있게 체인 형태로 구성한 것.

  • 커링은 한 번에 하나의 인자만 전달하는 것을 원칙으로 한다.
  • 중간 과정상의 함수를 실행한 결과는 그다음 인자를 받기 위해 대기만 할 뿐으로 마지막 인자가 전달되기 전까지는 원본 함수가 실행되지 않는다. (부분 적용 함수는 여러 개의 인자를 전달할 수 있고, 실행 결과를 재실행할 때 원본 함수가 무조건 실행)
var curry3 = function (func) {
  return function (a) {
   	return function (b) {
      return func(a, b);
    };
  };
};

var getMaxWith10 = curry3(Math.max)(10);
console.log(getMaxWith10(8));  // 10
console.log(getMaxWith10(25));  // 25

var getMinWith10 = curry3(Math.min)(10);
console.log(getMinWith10(8));  // 8
console.log(getMinWith10(25));  // 10