클로저

이윤환·2022년 4월 12일
1

자바스크립트에서 제일 애매한 개념이자 아마 당신이 신입이라면 면접에서 가장 많이 들어볼 질문인 클로저(closure)에 대해서 알아보겠다.

우선 클로저는 자바스크립트의 고유 개념은 아닐뿐더러 다른데에서도 명확한 정의를 하지 않는다. 안그래도 어려운걸 다들 다르게 말해서 더 어렵게 느껴진다.

MDN에서는 클로저에 대해 다음과 같이 정의했다.

함수와 그 함수가 선언될 당시의 lexical environment의 상호관계에 따른 현상

봐도 무슨 말인지 모르니까 대충 넘기고 클로저의 상황인 간단한 예시를 들겠다.

예시 01
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 함수에서 a를 선언했고 inner 함수에서는 a를 리턴했으며 최종적으로 inner 함수 자체를 반환했다.
그러면 outer 함수의 실행 컨텍스트가 종료 될 때 outer2 변수는 outer의 실행 결과인 inner 함수를 참조 할 것이다.
inner 함수의 실행 컨텍스트의 environmentRecord에는 수집할 정보가 없다. outer-EnvironmentReference에는 inner 함수가 선언된 위치의 LexicalEnvironment가 참조복사 된다. inner 함수는 outer 함수 내부에서 선언됐으므로, outer 함수의 LexicalEnvironment 가 담길것이다.
이제 스코프 체이닝에 따라 outer에서 선언한 변수 a에 접근해서 1만큼 증가시키고 outer2를 호출하면 같은 방식으로 2에서 1만큼 증가시킨 3이된다.

outer는 이미 실행이 끝났지만 outer2 함수에 의해 inner를 반환하고 다시 inner는 outer의 a라는 변수를 참조할 가능성이 생겼으므로 outer2를 실행하는 순간을 위해 자바스크립트가 outer의 변수 a가 사라지지 않게끔 해준 것이다.

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

그럼 이렇게 길고 어렵게 이해한걸 과연 어디다 써먹는지 살펴보자.

클로져 활용 사례

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

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

8번째 줄의 forEach문에 넘겨준 콜백 함수(외부)는 그 내부에서 외부 변수를 사용하고 있지 않아 클로저가 없으나
그 안의 addEventListener에 넘겨준 콜백 함수(내부) 안에서는 fruit라는 외부 변수를 사용하므로 클로저가 생성 됐다.

외부 콜백 함수는 fruits의 개수만큼 실행되며 그때마다 새로운 실행 컨텍스트가 활성화 될 것이다.
외부 콜백 함수의 실행 종료 여부와 관계 없이 클릭 이벤트에 의해 각 컨텍스트의 내부 콜백함수가 실행 될 때에는 outerEnvironmentReference가 외부 콜백 함수의 LexicalEnvironment를 참조한다.

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

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

보통 접근 권한에는 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());

위 코드에서 outer 함수를 종료할 때 inner 함수를 반환함으로써 outer 함수의 지역 변수인 a의 값을 외부에서도 읽을 수 있게 됐다. 바로 return을 통해서 말이다. 우리는 outer 함수를 실행할 수는 있지만 outer 함수 내부에는 간섭 할 수 없다. 오직 outer 함수가 return 하는 a 에만 접근이 가능하다. 이게 바로 클로저의 폐쇄성을 이용한 private 한 값이다.

부분 적용 함수

부분 적용 함수는 n개의 인자를 받는 함수에 미리 m개의 인자만 넘겨 기억시켜놨다가, 나중에 나머지 인자를 넘기면 비로소 원래 함수의 실행 결과를 받게끔 해주는 함수다.
보통 실무에서 사용되는 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);
    }
}

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

최초 event가 발생하면 7번째 줄에 의해 timeout의 대기열에 wait만큼의 시간 뒤에 func를 실행하라는 내용이 있으나, wait 시간이 지나기 이전에 동일한 event가 발생할 경우엔 6번째 줄에 의해 앞서 저장했던 대기열을 초기화하고, 다시 7번째 줄에서 새로운 대기열을 등록한다.
결국 각 이벤트가 바로 이전 이벤트로부터 wait 시간 이내에 발생하는 한 마지막에 발생한 이벤트만이 초기화가 되지 않고 실행될 것이다.

정리

  • 클로저란 어떤 함수에서 선언한 변수를 참조하는 내부함수를 외부로 전달할 경우, 함수의 실행 컨텍스트가 종료된 후에도 해당 변수가 사라지지 않는 현상이다.
  • 내부함수를 외부로 전달하는 대표적인 방법은 return을 사용하는 것과 콜백 함수를 통해 전달하는 경우가 있다.
profile
나는 이윤환

0개의 댓글