Core Javascript 05 클로저

05 클로저(Closure)

5-1 클로저의 의미 및 원리 이해

  • 함수형 프로그래밍 언어에서 등장하는 보편적인 특성이다.
  • MDN : 함수와 그 함수가 선언될 당시의 lexical environment의 상호관계에 따른 현상
  • 어떤 함수에서 선언한 변수를 참조하는 내부함수에서만 발생하는 현상
var outer = function () {
	var a = 1;
	var inner = function () {
		console.log(++a); // 내부에서는 a를 선언x, outer의 LE에서 a를 찾는다.
	};
	inner();
};
outer();
// outer 함수의 실행 컨텍스트가 종료되면 a, inner는 GC의 수집 대상이 된다.
var outer = function () {
	var a = 1;
	var inner = function () {
		return ++a;
	};
	return inner(); // inner 함수를 실행한 결과를 리턴
};
var outer2 = outer(); // outer 함수의 실행 컨텍스트가 종료되면 a 변수를 참조하는 대상이 없어짐
console.log(outer2); // 2
var outer = function () {
	var a = 1;
	var inner = function () {
		return ++a;
	};
	return inner; // inner 함수 자체를 반환
};
var outer2 = outer(); // outer2 변수는 outer의 실행 결과인 inner 함수를 참조
console.log(outer2()); // 2
console.log(outer2()); // 3

  • inner 함수의 실행 시점에는 outer 함수가 이미 실행이 종료된 상태인데 어떻게 outer 함수의 LexicalEnvironment에 접근할 수 있을까? 그 이유는 가비지 컬렉터의 동작 방식 때문. 참조하는 변수가 하나라도 있다면 그 값은 수집 대상에 포함시키지 않는다.
  • outer 함수는 실행 종료 시점에 inner 함수를 반환한다. 외부 함수인 outer의 실행이 종료되더라도 내부 함수인 inner 함수는 언젠가 outer2를 실행함으로써 호출될 가능성이 있다.
  • 함수의 실행 컨텍스트가 종료된 후에도 LE가 가비지 컬렉터의 수집 대상에서 제외되는 경우는 예제와 같이 지역변수를 참조하는 내부함수가 외부로 전달된 경우가 유일하다.
  • 클로저란 어떤 함수 A에서 선언한 변수 a를 참조하는 내부함수 B를 외부로 전달할 경우, A의 실행 컨텍스트가 종료된 이후에도 변수 a가 사라지지 않는 현상
  • setTimeout과 같은 메서드에 전달할 콜백 함수 내부에서 지역변수를 참조하거나, DOM의 메서드인 addEventListener에 등록할 handler 함수 내부에서 지역변수를 참조하는 경우도 클로저이다.

5-2 클로저와 메모리 관리

  • 메모리 누수 : 참조 카운트가 0이 되지 않아 GC의 수거 대상이 되지 않는 경우
  • 하지만 개발자가 의도적으로 설계한 경우는 메모리 누수라고 할 수 없다.
  • 클로저는 어떤 필요에 의해 의도적으로 함수의 지역변수가 메모리를 소모하도록 함으로써 발생함. 그렇다면 필요성이 사라지면 더는 메모리를 소모하지 않게 해주면 된다. ( = 참조 카운트를 0으로 만들어준다. 참조형이 아닌 기본형 데이터, null 이나 undefined를 할당해준다. )
// (1) return에 의한 클로저의 메모리 해제
var outer = (function () {
	var a = 1;
	var inner = function () {
		return ++a;
	};
	return inner;
})();
console.log(outer()); // 2
console.log(outer()); // 3
outer = null; // outer 식별자의 inner 함수 참조를 끊음
// (2) setInterval에 의한 클로저의 메모리 해제
(funtion () {
	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.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);
})();

5-3 클로저 활용 사례

5-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: fruit라는 외부 변수를 참조하므로 클로저가 있다.
		alert('your choice is ' + fruit);
	});
	$ul.appendChild($li);
});
document.body.appendChild($ul);
// B 함수의 쓰임새가 콜백 함수에 국한되지 않는 경우 반복을 줄이기 위해 외부로 분리
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); // [object MouseEvent] 출력
	$li.addEventListener('click', alertFruit.bind(null, fruit));
	// bind 활용하여 클로저가 발생하지 않지만 여러가지 제약사항 발생
	// 이벤트 객체가 인자로 넘어오는 순서가 바뀌게 되고, 함수 내부에서의 this가 원래와 달라지게 된다.
	$ul.appendChild($li);
});
document.body.appendChild($ul);
alertFruit(fruits[1]);
// 콜백함수를 고차함수로 바꿔서 클로저 활용
var alertFruitBuilder = function (fruit) {
		return function () { // 익명함수 반환
		alert('your choice is ' + fruit); // 클로저
	};
};
fruits.forEach(function (fruit) {
	var $li = document.createElement('li');
	$li.innerText = fruit;
	$li.addEventListner('click', alertFruitBuilder(fruit));
	$ul.appendChild($li);
});
...
// 클릭 이벤트가 발생하면 비로소 이 함수의 실행 컨텍스트가 열리면서 fruit 참조

5-3-2 접근 권한 제어 (정보 은닉)

  • 정보 은닉 (information hiding) : 어떤 모듈의 내부 로직에 대해 외부로의 노출을 최소화해서 모듈간의 결합도를 낮추고 유연성을 높인다.
  • 외부에 제공하고자 하는 정보들을 모아서 return하고, 내부에서만 사용할 정보들은 return하지 않는 것으로 접근 권한 제어가 가능
  • 자동차 경주 게임
    • 각 턴마다 주사위를 굴려 나온 숫자(km)만큼 이동한다.
    • 차량별로 연료량(fuel)과 연비(power)는 무작위로 생성된다.
    • 남은 연료가 이동할 거리에 필요한 연료보다 부족하면 이동하지 못하낟.
    • 모든 유저가 이동할 수 없는 턴에 게임이 종료된다.
    • 게임 종료 시점에 가장 멀리 이동해 있는 사람이 승리
    var createCar = function () {
    	var	fuel = Math.ceil(Math.random() * 10 + 10); // 연료(L) 10 ~ 20
    	var power = Math.ceil(Math.random() * 3 + 2); // 연비(km/L) 3 ~ 5
    	var moved = 0; // 총 이동거리
    	var publicMembers = {
        get moved () {
          return moved
        },
        run: function () {
          var km = Math.ceil(Math.random() * 6); // 1 ~ 6
          var wasteFuel = km / this.power;
          if (this.fuel < wasteFuel) {
            console.log('이동 불가');
            return;
          }
          this.fuel -= wasteFuel;
          this.moved += km;
          console.log(`${km}km 이동 (총 ${this.moved}km)`);
        }
      };
      Object.freeze(publicMembers);
      return publicMembers;
    };
    
    var car = createCar();
  • fuel, power, moved 를 외부에서 제어할 수 없도록 클로저를 활용한다.
  • Object.freeze() 메서드를 통해 동결된 객체가 더 이상 변경될 수 없도록 처리

5-3-3 부분 적용 함수

  • 부분 적용 함수 (partially applied function) : 여러개의 인자를 받는 함수에서 미리 일부의 인자만 넘겨 기억시켰다가, 나중에 나머지 인자를 넘기면 비로소 원래 함수의 실행 결과를 얻을 수 있게끔 하는 함수
// bind 메서드를 활용한 부분 적용 함수
var add = function () {
	var result = 0;
	for (var i = 0; i < arguments.length; i++) {
	  result += arguments[i];
	}
	return result;
};

var addPartial = add.bind(null, 1, 2, 3, 4, 5);
console.log(addPartial(6, 7, 8, 9, 10)); // 55
  • 위의 add 함수는 this를 사용하지 않으므로 bind 메서드만으로도 구현 가능. 그러나 this의 값을 변경할 수밖에 없기 때문에 메서드에서는 사용할수 없다.
// this에 관여하지 않는 부분 적용 함수
var partial = function () {
  var originalPartialArgs = arguments;
  var func = originalPartialArgs[0];
  if (typeof func !== 'function') {
    throw new Error('첫 번째 인자가 함수가 아닙니다.');
  }
  return function () {
    var partialArgs = Array.prototype.slice.call(originalPartialArgs, 1); // 먼저 받은 인자
    var restArgs = Array.prototype.slice.call(arguments); // 나머지 인자
    return func.apply(this, partialArgs.concat(restArgs));
  };
};

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

var dog = {
  name: '강아지',
  greet: partial(function(prefix, suffix) {
    return prefix + this.name + suffix;
  }, '왈왈, ')
};

dog.greet('입니다!');  // 왈왈, 강아지입니다!
  • partial 함수는 첫 번째 인자에 원본 함수를, 두 번째 인자 이후부터는 미리 적용할 인자들을 전달하고, 반환할 함수(부분 적용 함수)에서는 다시 나머지 인자들을 모아(concat) 원본 함수를 호출(apply)한다.
  • 실행 시점의 this를 그대로 반영함으로써 this에는 영향을 주지 않는다.
  • 그러나 인자를 반드시 앞에서부터 차례로 전달할 수 밖에 없다.
var partial2 = function () {
  var originalPartialArgs = arguments;
  var func = originalPartialArgs[0];
  if (typeof func !== 'function') {
    throw new Error('첫 번째 인자가 함수가 아닙니다.');
  }
  
  return function () {
    var partialArgs = Array.prototype.slice.call(originalPartialArgs, 1);
    var restArgs = Array.prototype.slice.call(arguments);
    for (var i = 0; i < partialArgs.length; i++) {
      if (partialArgs[i] === Symbol.for('EMPTY_SPACE')) {
        partialArgs[i] = restArgs.shift();
      }
    }
    return func.apply(this, partialArgs.concat(restArgs));
  };
};

var add = function () {
	...
};

var _ = Symbol.for('EMPTY_SPACE'); // 비워놓는 자리 표시
var addPartial = partial2(add, 1, 2, _, 4, 5, _, _, 8, 9);
console.log(addPartial(3, 6, 7, 10));  // 55
  • 빈 자리에 _로 표시하여 비워놓은 공간마다 나중에 넘어온 인자들이 차례대로 끼워넣어진다.
  • Symbol.for (ES6) : Symbol.for() 함수는 전역 심볼 레지스트리 목록에서 사용 가능한 심볼을 생성합니다. Symbol.for()는 호출할 때마다 새로운 심볼을 생성하지는 않으며 레지스트리에서 주어진 key를 갖는 심볼이 이미 존재하는지를 먼저 확인합니다. 존재하는 경우 해당하는 심볼이 반환됩니다. 주어진 키를 갖는 심볼이 존재하지 않는 경우, Symbol.for()는 새로운 전역 심볼을 생성합니다. https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Symbol/for
  • 디바운스 : 짧은 시간 동안 동일한 이벤트가 많이 발생할 경우 이를 전부 처리하지 않고 처음 또는 마지막에 발생한 이벤트에 대해 한 번만 처리. (scroll, wheel, mousemove, resize 등에 적용하기 좋다)
// 부분 적용 함수 - debounce
var debounce = function (eventName, func, wait) {
  var timeoutId = null;
  return function (event) {
    var self = this;
    console.log(eventName, 'event 발생');
    clearTimeout(timeoutId); // 앞서 저장했던 대기열 초기화
    timeoutId = setTimeout(func.bind(self, event), wait); // wait 시간 뒤에 func 실행
		// 그런데 wait 시간이 경과하기 전에 다시 동일한 event 발생하면
  };
};

var moveHandler = function (e) {
  console.log('move event 처리');
};

var wheelHandler = function (e) {
 console.log('wheel event 처리'); 
};

document.body.addEventListener('mousemove', debounce('move', moveHandler, 500));
document.body.addEventListener('mousewheel', debounce('wheel', wheelHandler, 700));

5-3-4 커링 함수

  • 커링함수 (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
  • 인자가 많아질수록 가독성이 떨어진다는 단점이 있다.
  • ES6에서는 화살표 함수 사용
var curry5 = func => a => b => c => d => e => func(a,b,c,d,e);
  • 각 단계에서 받은 인자들은 모두 마지막 단계에서 참조할 것이므로 GC의 수거대상이 되지 않고 메모리에 쌓여있다가 마지막 호출로 실행 컨텍스트가 종료된 후에야 한꺼번에 GC의 수거대상이 된다.
  • 지연실행 (lazy execution) : 당장 필요한 정보만 받아서 전달하며 마지막 인자가 넘어갈 때까지 함수 실행을 미룬다.
var getInformation = function (baseUrl) {
	return function (path) {
		return function (id) {
			return fetch(baseUrl + path + '/' + id);
		};
	};
};
// ES6
var getInformation = baseUrl => path => id => fetch(baseUrl + path + '/' + id);
var imageUrl = 'http://imageAddress.com/';

var getImage = getInformation(imageUrl);
var getEmoticon = getImage('emoticon'); // http://imageAddress.com/emoticon

var emoticon1 = getEmoticon(100); // http://imageAddress.com/emoticon/100
  • Redux에서의 커링 사용
// Redux Middleware 'Logger'
const logger = store => next => action => {
	console.log('dispatching', action);
	console.log('next state', store.getState());
	return next(action);
};

// Redux Middleware 'thunk'
const thunk = store => next => action => {
	return typeof action === 'function'
		? action(dispatch, store.getState)
		: next(action);
};
  • store, next, action 순서로 인자를 받는다. 이 중 store는 프로젝트 내에서 한 번 생성된 이후로는 바뀌지 않고, dispatch의 의미를 가지는 next도 마찬가지지만 action의 경우는 매번 달라진다.
  • store, next를 미리 넘겨서 반환된 함수를 저장시켜놓고, 이후에는 action만 받아서 처리할 수 있게 되어있다.

좋은 웹페이지 즐겨찾기