TIL 97 | 코어자바스크립트(5) 클로저

hyounglee·2021년 1월 2일
1

JavaScript

목록 보기
38/46

시대의 명작 코어 자바스크립트를 읽고 자바스크립트 마스터에 도전합니다...
JavaScript 클로저(Closure)

클로저

클로저는 자바스크립트 고유의 개념이 아니라 여러 함수형 프로그래밍 언어에서 등장하는 보편적인 특성이다. 클로저는 어떤 함수에서 선언한 변수를 참조하는 내부함수에서만 발생하는 현상이다.

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

var outer = function () {
  var a = 1;
  var inner = function () {
    return ++a;
  }
  return inner; // inner 함수 자체를 반환
};
var outer2 = outer();
console.log(outer2()); // 2
console.log(outer2()); // 3

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

클로저와 메모리 관리

클로저는 어떤 필요에 의해 의도적으로 함수의 지역변수를 메모리를 소모함으로써 발생한다. 필요성이 사라진 시점에는 더는 메모리를 소모하지 않게 해주면 된다. 참조 카운트를 0으로 만들면 언젠가 가비지 컬렉터가 수거해갈 것이고 메모리가 회수될 것이다.

이러한 경우 식별자에 참조형이 아닌 기본형 데이터(null이나 undefined)를 할당하면 된다.

클로저 활용 사례

1. 콜백 함수 내부에서 외부데이터 사용

콜백 함수를 내부 함수로 선언해서 외부변수를 직접 참조 (클로저)

(B) 함수가 참조할 예정인 변수 fruit에 대해서는 (A)가 종료된 후에도 GC 대상에서 제외되어 계속 참조가 가능해진다. (B) 함수의 쓰임새가 콜백함수에 국한되지 않는 경우라면 반복을 줄이기 위해 외부로 분리하는 것이 낫다.

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);

bind 메서드로 값을 직접 넘겨주고 외부로 분리 (클로저x)

그냥 alertFruit를 넘기면 인자에 대한 제어권을 addEventListener가 가지게 되고, 첫번째 인자에 이벤트 객체를 주입하기 때문에 [object MouseEvent]가 출력된다. 따라서 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);
alertFruit(fruits[1]);

다만 이렇게 하면 이벤트 객체가 인자로 넘어오는 순서가 바뀌고, 함수 내부에서의 this가 원래의 그것과 달라진다. 이런 점을 보완하기 위해서는 고차함수를 활용할 수 있다.

고차함수 : 함수를 인자로 받거나 함수를 리턴하는 함수

고차함수 활용 (클로저)

alertFruitBuilder 안에서 기존의 alertFruit 함수를 익명함수로 반환한다. alertFruitBuilder 함수가 실행하면 이 함수의 실행 결과가 다시 함수가 되며 반환된 함수를 리스너에 콜백 함수로써 전달한다. 이후 언젠가 클릭 이벤트가 발생하면 이 함수의 실행 컨텍스트가 열리면서 alertFruitBuilder의 인자로 넘어온 fruitouterEnvironmnetReference에 의해 참조할 수 있게 된다.

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);
alertFruit(fruits[1]);

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

정보 은닉 : 어떤 모듈의 내부 로직에 대해 외부로의 노출을 최소화해서 모듈간의 결합도를 낮추고 유연성을 높이고자 하는 현대 프로그래밍 언어의 중요한 개념 중 하나. 접근 권한에는 public, private, protected의 세 종류가 있다.

클로저를 이용하면 함수 차원에서 public한 값과 private한 값을 구분하는 것이 가능하다. 외부 스코프에서 함수 내부 변수들 중 선택적으로 일부 변수에 대한 접근 권한을 부여할 수 있는데, 바로 return 을 활용하면 된다.

  • 함수에서 지역변수 및 내부함수 등을 생성한다.
  • 외부에 접근권한을 주고자 하는 대상들로 구성된 참조형 데이터(대상이 여럿일 때는 객체 또는 배열, 하나일 때는 함수)를 return한다.

3. 부분 적용 함수

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

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

var add = funtcion () {
	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

부분 적용으로 구현한 디바운스 함수

var debounce = (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);
    };
};

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

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

document.body.addEventListener('mousemove',debounce('move', moveHandler, 500));

document.body.addEventListener('mousewheel',debounce('wheel', moveHandler, 700));

디바운스 함수에서는 변수 eventName, func, wait, timeoutId가 클로저로 처리된다.

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

필요한 인자의 개수만큼 함수를 만들어 계속 리턴해주다가 마지막에 조합해서 리턴한다. 다만 이자가 많아질수록 가독성이 떨어진다는 단점이 있다.

ES6에서는 화살표 함수를 써서 간단하게 표기할 수 있다

var curry = func => a => b => c => d => e => func(a, b, c, d, e); 

커링 함수를 이해하기에도 훨씬 수월하다. 각 단계에서 받은 인자들을 모두 마지막 단계에서 참조할 것이므로 가비지 컬렉팅 되지 않고 메모리에 쌓였다가. 마지막 호출로 실행 컨텍스트가 종료된 후에야 비로소 한꺼번에 수거된다.

커링 함수는 지연 실행에 유용하게 사용된다. 당장 필요한 정보만 받아서 전달하고, 또 필요한 정보가 들어오면 전달하는 식으로 결국 마지막 인자가 넘어갈 때까지 함수 실행을 미루는 것을 의미한다. 프로젝트 내에서 자주 쓰이는 함수의 매개변수가 항상 비슷하고 일부만 바뀌는 경우에도 적절하다.

profile
(~˘▾˘)~♫❝ 쉽게만 살아가면 재미없어 빙고 .ᐟ ❞•*¨*•.¸¸♪

0개의 댓글