Skip to content

Latest commit

 

History

History
381 lines (294 loc) · 14.3 KB

클로저.md

File metadata and controls

381 lines (294 loc) · 14.3 KB

클로저

클로저의 의미 및 원리 이해

  • 클로저 : 어떤 함수에서 선언한 변수참조하는 내부함수외부로 전달할 경우, 함수의 실행 컨텍스트가 종료된 후에도 해당 변수가 사라지지 않는 현상이다.
    • 즉, 컨텍스트 A에서 선언한 변수내부함수B에서 참조할 경우에 발생하는 현상
    • 클로저의 이점 : 함수 종료 후에도 사라지지 않는 지역변수를 만들 수 있다.
✅ 다시말하면, 컨텍스트 A에서 선언한 변수 a를 참조하는 내부함수 B를 A의 외부로 전달할 경우 A가 종료된 이후에도 a가 사라지지 않는 현상 → 함수 종료 후에도 사라지지 않느 지역변수를 만들 수 있다.

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

var outer = function () {   // 외부 함수
  var a = 1;
  var inner = function () { // 내부 함수
    console.log(++a); // 2
  };
  inner();
};
outer();
  • outer의 내부 함수인 inner함수에서 a의 값을 1만큼 증가시킨 다음 출력한다.
  • outer 함수의 실행 컨텍스트가 종료되기 이전에 inner함수의 실행 컨텍스트가 종료돼 있으며, 이후 별도로 inner 함수를 호출할 수 없다.

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

var outer = function () {   // 외부 함수
  var a = 1;
  var inner = function () { // 내부 함수
    return ++a;
  };
①  return inner();
};
② var outer2 = outer();
③ console.log(outer2()); // 2
console.log(outer2()); // 3
  • ① inner함수의 실행 결과가 아닌 inner함수 자체를 반환했다. 그러면 outer 함수의 실행 컨텍스트가 종료될 때(②) outer2 변수는 outer의 실행 결과인 inner함수를 참조하게 된다.
  • 이후 ③에 outer2를 호출하면 앞서 반환된 함수인 inner가 실행된다.

클로저 발생 시의 콜스택 흐름 )

image

  • a를 참조하는 변수가 하나도 없게 되면 가비지 컬렉터(GC)의 수집 대상이 되어 삭제된다. 하지만 호출될 가능성이 있는 경우에는 수집 대상에서 제외됨

가비지 컬렉터의 특징

  • 어떤 값을 참조하는 변수가 하나라도 있다면 그 값은 수집 대상에 포함시키지 않는다.
💡 - 클로저란 어떤 함수 A에서 선언한 변수 a를 참조하는 내부함수 B를 외부로 전달할 경우 A의 실행 컨텍스트가 종료된 이후에도 변수 a가 사라지지 않는 현상을 말함
  • 클로저란 함수를 선언할 때 만들어지는 유효범위가 사라진 후에도 호출할 수 있는 함수

클로저 다른 예시)

function user(_name) {
    let _logged = true;
    return {
        get name() {return _name},
        set name(v) {_name = v},
        login() {_logged = true},
        logout() {_logged = false},
        get status() {
            return _logged
            ? 'login'
            : 'logout';
        },
    }
}
let roy = user('재남')
console.log(roy.name) // 재남
roy.name = '제이';
console.log(roy.name) // 제이
roy._name = '로이'
console.log(roy.name) // 제이

console.log(roy._name) // 로이

console.log(roy.status) // login

클로저와 메모리 관리

  • 메모리 누수 : 개발자의 의도와 달리 어떤 값의 참조 카운트가 0이 되지 않아 Garbage Collector의 수거 대상이 되지 않는 경우 이다

클로저의 메모리 관리 )

// 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 함수 참조를 끊음

console.log(outer()); // Uncaught TypeError: outer is not a function
// setInterval에 의한 클로저의 메모리 해제
(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
..
//10
  • 1초에 한번씩 전위식인 a 변수를 출력함

클로저의 활용 사례

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

이벤트 리스너에 관한 예시 - 콜백함수와 클로저(1) )

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

 fruits.forEach(function (fruit){  // 클로저 x
	var $li = document.createElement('li');
	$li.innerText = fruit;
	$li.addEventListener('click', function (){
		alert('your choice is ' + fruit); // 클로저 o
	});
	$ul.appendChild($li);
});
document.body.appendChild($ul);

결과 )

image

image

image

  • fruits 변수를 순회하며 li를 생성하고, 각 li를 클릭하면 해당 리스너에 기억된 콜백 함수를 실행된다.
  • ⓐ forEach 메서드에 넘겨준 익명의 콜백함수ⓐ는 그 내부에서 외부 변수를 사용하지 않고 있으므로 클로저가 없고,
  • ⓑ 의 addEventListener에 넘겨준 콜백 함수ⓑ에는 fruit이라는 외부 변수를 참조하고 있으므로 클로저가 있다.

콜백함수와 클로저(2) )

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);
	$ul.appendChild($li);
});
document.body.appendChild($ul);
alertFruit(fruits[0]) // your choice is apple
  • 공통함수로 쓰고자 콜백 함수를 외부로 꺼내어 alertFruit라는 변수에 담음
  • 이렇게 하면 alertFruit를 직접 실행할 수 있다.

콜백 함수와 클로저(3) - bind 메서드로 값을 직접 넘겨줌 (클로저 사용 x) )

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[0])
  • bind를 활용해서 bind 메서드로 값을 직접 넘겨준 덕분에 클로저는 발생하지 않게된 반면 여러 가지 제약사항이 생김

콜백 함수와 클로저(4) - 콜백 함수를 고차함수를 사용 (클로저 o)

var fruits = ['apple', 'banana', 'peach'];
var $ul = document.createElement('ul');
var **alertFruitbuilder** = function(fruit) {
    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[0])
  • ⓐ alerFruitBuilder 함수를 실행하면서 fruit 값을 인자로 전달함,
  • 그러면 이 함수의 실행 결과가 다시 함수가 되며, 이렇게 반환된 함수를 리스너에 콜백 함수로써 전달한 것이다.
  • 즉, alerFruitBuilder의 실행결과로 반환된 함수에는 클로저가 존재함
  • 위 예시는 고차함수로 바꿔서 클로저를 적극적으로 활용한 방안이었다.

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

  • 클로저는 외부 함수의 변수를 참조하는 내부 함수를 사용하여 접근 권한 제어(정보 은닉)를 할 수 있습니다.
  • 정보 은닉(Information hiding) : 어떤 모듈의 내부 로직에 대해 외부로의 노출을 최소화해서 모듈 간의 결합도를 낮추고 유연성을 높이고자 하는 개념
    • public : 클래스 내부, 외부에서 접근이 가능
    • protected : 클래스 내부간 같은 상속 구조라면 접근이 가능
    • private : 내부에서만 접근이 가능하며, 외부에 노출되지 않는 것
  • 자바스크립트는 기본적으로 변수 자체에 이러한 접근 권한을 직접 부여하도록 설계돼 있지 않지만 그렇다고 접근 권한 제어가 불가능한 것은 아니다!!!
  • 클로저를 이용하면 함수 차원에서 public한 값과 private한 값을 구분하는 것이 가능하다.

return을 활용해서 접근 권한 제어 - 예시 )

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함수를 종료할 때 inner 함수를 반환함으로써 outer함수의 지역변수인 a의 값을 외부에서도 읽을 수 있게 되었다.
  • 이처럼 클로저를 활용하면 외부 스코프에서 함수 내부의 변수들 중 선택적으로 일부의 변수에 대한 접근 권한을 부여할 수 있다. 바로 return을 활용해서 말이다.

return을 활용해서 접근 권한을 제어

  • 외부에 제공하고자 하는 정보들을 모아서 return 하고,
  • 내부에서만 사용할 정보들은 return하지 않는 것으로 접근 권한 제어가 가능한 것이다.
  • return한 변수들은 공개 멤버(public)가 되고, 그렇지 않은 변수들은 비공개 멤버(private)가 되는 것이다.

3) 부분 적용 함수

  • 부분 적용 함수(partially applied function) : n개의 인자를 받는 함수에 미리 m개의 인자만 넘겨 기억시켰다가, 나중에 (n-m)개의 인자를 넘기면 비로소 원래 함수의 실행 결과를 얻을 수 있게끔 하는 함수이다.
    • 즉, 원하는 만큼의 인자를 미리 넘겨놓고, 나중에 후가할 인자를 전달해서 실행하는 목적이다.

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
  • addPartal 함수는 인자 5개를 미리 적용하고, 추후 추가적으로 인자들을 전달하면 모든 인자를 모아 원래의 함수가 실행되는 부분 적용 함수이다.

  • 디바운스(debounce) : 짧은 시간 동안 동일한 이벤트가 많이 발생할 경우 이를 전부 처리하지 않고 처음 또는 마지막에 발생한 이벤트에 대해 한 번만 처리하는 것

    • 프론트엔드 성능 최적화에 큰 도움을 주는 기능 중 하나이다.

4) 커링 함수

  • 커링 함수(currying function) : 여러 개의 인자를 받는 함수를 하나의 인자만 받는 함수로 나눠서 순차적으로 호출될 수 있게 체인 형태로 구성한 것이다.
    • 한번에 하나의 인자만 전달하는 것을 원칙으로함
    • 또한, 중간 과정상의 함수를 실행한 결과는 그다음 인자를 받기 위해 대기만 하고, 마지막 인자가 전달되기 전까지는 원본 함수가 실행 되지 않는다.
  • 반면에 부분 적용 함수는 여러 개의 인자를 전달할 수 있고, 실행 결과를 재실행 할때 원본 함수가 무조건 실행된다.

커링 함수(1) )

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)); // 25
  • 커링함수는 부분 적용함수와 달리 필요한 상황에 직접 만들어 쓰기 용이하다.
  • 필요한 인자 개수만큼 함수를 만들어 계속 리턴 해주다가 마지막에 조합해서 리턴해주면 된다.
  • 커링함수 단점 : 인자가 많아질 수록 가독성이 떨어진다.

가독성이 떨어지는 예시 - 커링 함수(2) )

var curry5 = function (func) {
	return function (a) {
		return function (b) {
			return function (c) {
				return function (d) {
					return function (e) {
						return func(a, b,c,d,e);
					};
				};
			};
		};
	};
};
var getMax = curry5(Math.max);
console.log(getMax(1)(2)(3)(4)(5)); //5

위 코드를 화살표 함수를 사용해서 같은 내용을 한 줄로 줄이기 )

var curry5 = func => a => b => c => d => e => func(a,b,c,d,e);
console.log(getMax(1)(2)(3)(4)(5)); //5
  • 화살표 함수를 구현하면 커링 함수를 이해하기 쉬워짐
  • 화살표 순서에 따라 함수에 값을 차례로 넘겨주면 마지막에 func가 호출될 거라는 흐름이 파악됨
    • 순서 : 각 단계에서 받은 인자들을 모두 마지막 단계에서 참조할 것이므로 가비지 컬렉터 들어가지 않고 메모리에 쌓였다가, 마지막 호출로 실행 컨텍스트가 종료된 후에야 비로서 한꺼번에 가비지 컬렉터의 수거 대상이 된다.

커링 함수가 유용한 경우

  • 지연실행 : 다장 필요한 정보만 받아서 전달하고 또 필요한 정보가 들어오면 전달하는 식으로 하면 결국 마지막 인자가 넘어갈 때까지 함수 실행을 미루는 셈이 된다.
    • 원하는 시점까지 지연시켰다가 실행하는 것이 요긴한 상황이라면 커링을 쓰기에 적합함

정리

  • 클로저란 어떤 함수에서 선언한 변수를 참조하는 내부함수를 외부로 전달할 경우, 함수의 실행 컨텍스트가 종료된 후에도 해당 변수가 사라지지 않는 현상이다.
  • 내부함수를 외부로 전달하는 방법에는 함수를 return하는 경우뿐 아니라 콜백으로 전달하는 경우도 포함됨
  • 클로저는 그 본질이 메모리를 계속 차지하는 개념이므로, 더는 사용하지 않게 된 클로저에 대해서는 메모리를 차지하지 않도록 관리해줄 필요가 있다.

참고

정재남, 코어 자바스크립트