Chapter 5. 클로저
5.1 클로저의 의미 및 원리 이해
클로저(Closure)는 여러 함수형 프로그래밍 언어에서 등장하는 보편적인 특성이다. 자바스크립트 고유의 개념이 아니기 때문에 ECMAScript 명세서에서도 클로저의 정의를 다루지 않고 있기 때문에 다양한 서적에서 클로저를 한 문장으로 요약해서 설명하면 아래와 같다.
- 자신을 내포하는 함수의 컨텍스트에 접근할 수 있는 함수 ─ 더글라스 크록포드, 《자바스크립트 핵심 가이드》, 한빛미디어(p68)
- 함수가 특정 스코프에 접근할 수 있도록 의도적으로 그 스코프에서 정의하는 것 ─ 에단 브라운, 《러닝 자바스크립트》, 한빛미디어(p196)
- 함수를 선언할 때 만들어지는 유효범위가 사라진 후에도 호출할 수 있는 함수 ─ 존 레식, 《자바스크립트 닌자 비급》, 인사이트(p116)
- 이미 생명 주기 상 끝난 외부 함수의 변수를 참조하는 함수 ─ 송형주 고현준, 《인사이드 자바스크립트》, 한빛미디어(p157)
- 자유변수가 있는 함수와 자유변수를 알 수 있는 환경의 결합 ─ 에릭 프리먼, 《Head First Javascript Programming》, 한빛미디어(p534)
- 로컬 변수를 참조하고 있는 함수 내의 함수 ─ 야마다 요시히로, 《자바스크립트 마스터북》, 제이펍(p180)
- 자신이 생성될 때의 스코프에서 알 수 있었던 변수들 증 언젠가 자신이 실행될 때 사용할 변수들만을 기억하여 유지시키는 함수 ─ 유인동, 《함수형 자바스크립트 프로그래밍》, 인사이트(p31)
MDN(Mozilla Developer Network)에서는 클로저에 대해 "A closure is the combination of a function and the lexical environment within which that function was declared." ("클로저는 함수와 그 함수가 선언될 당시의 lexical environment의 상호관계에 따른 현상")이다. 선언될 당시의 lexical environment는 outerEnvironmentReference에 해당한다. 즉, 클로저는 다시 말해 "어떤 함수에서 선언한 변수를 참조하는 내부함수에서만 발생하는 현상"이라고 볼 수 있다.
var outer = function() {
var a = 1;
var inner = function() {
console.log(++a);
}
inner();
};
outer();
위 함수에서 outer 함수에서 변수 a를 선언했고, outer 함수 내부의 inner 함수에서 바깥에 있는 a를 출력하는 것을 볼 수 있다. 이때 inner 함수는 내부에 a를 가지고 있지 않으므로 outerEnvironmentReference에 지정된 상위 컨텍스트인 outer의 LexicalEnvironment에 접근해 거기서 a를 찾는다. 이후 outer 컨텍스트가 종료되면 outer 컨텍스트의 LexicalEnvironment에 저장된 식별자(변수) {a, inner}에 대한 참조는 가비지 컬렉터가 가져가게 된다. 더 이상 해당 변수를 참조하는 식별자가 따로 없기 때문이다.
var outer = function () {
var a = 1;
var inner = function() {
return ++a;
}
return inner();
};
var outer2 = outer();
console.log(outer2);
아래의 코드에서 이번에 inner은 ++a를, outer는 inner 함수를 실행한 결과를 반환하는 것을 알 수 있다. 이 경우에도 outer 컨텍스트가 종료되면 그 시점에서 a 변수를 참조하는 대상이 없어지므로 가비지 컬렉터에 의해 소멸한다는 것을 알 수 있다. 위 두 개의 코드는 outer 실행 컨텍스트가 종료되기 전에, 이미 inner 실행 컨텍스트가 종료되었다는 공통점이 있다.
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 함수의 실행 결과가 아닌 inner 함수 그 자체를 반환하고 있다. 위 경우 inner 컨텍스트의 environmentRecord에는 수집할 정보가 없다. outerEnvironmentReference에는 inner 함수가 선언된 위치의 LexicalEnvironment가 참조복사된다. 즉, inner 함수는 outer 함수의 내부에서 선언되었으므로 outer 함수의 LexicalEnvironment가 담긴다. 이후 스코프 체이닝에 따라 outer에서 선언한 변수 a에 접근해서 1만큼 증가시킨 후 2를 반환하고, inner 함수의 실행 컨텍스트가 종료된다. 그러나 inner 함수가 실행되는 시점에서 outer 함수는 이미 종료되어 있다. 어떻게 된 걸까?
이러한 현상은 가비지 컬렉터의 동작 방식 때문인데, 위 코드에서는 outer 함수가 inner 함수 그 자체를 반환하므로 inner 함수가 언젠가 다시 호출되어 a를 활용하고 함수를 활용할 수도 있기 때문에 inner 함수의 실행 컨텍스트가 언젠가 실행되면 outerEnvironmentReference가 outer 함수의 LexicalEnvironment를 필요로 하기 때문에 가비지 컬렉터의 수집 대상에서 제외된다. 그 덕분에 inner 함수가 이 변수에 접근할 수 있다.
즉, "어떤 함수에서 선언한 변수를 참조하는 내부함수에서만 발생하는 현상"이란 "외부 함수의 LexicalEnvironment가 가비지 컬렉팅 되지 않는 현상"이라고 할 수 있다.
이를 정의를 바탕으로 다시 고쳐쓰면 "클로저란 어떤 함수 A에서 선언한 변수 a를 참조하는 내부함수 B를 외부로 전달할 경우 A의 실행 컨텍스트가 종료된 이후에도 변수 a가 사라지지 않는 현상"을 말한다. 이때, '외부로의 전달'은 반드시 return만을 의미하는 것은 아니다.
// (1) setInterval/setTimeout
(function () {
var a = 0;
var intervalId = null;
var inner = function() {
if (++a >= 10) {
clearInterval(intervalId);
}
console.log(a);
};
intervalId = setInterval(inner, 1000);
})();
// (2) eventListener
(function () {
var count = 0;
var button = document.createElement('button');
button.innerText = 'click';
button.addEventListener('click', funciton() {
console.log(++count, 'times clicked');
});
document.body.appendChild(button);
})();
위 코드에서 (1)은 외부객체인 Window의 메서드(setTimeout 또는 setInterval)에 전달할 콜백 함수 내부에서 지역 변수인 intervalId, inner를 참조한다. 위 코드에서 (2)는 별도의 외부객체인 DOM의 메서드(addEventListenr)에 등록할 handler 함수 내부에서 지역 변수인 count, button을 참조한다. 두 상황 모두 지역변수를 참조하는 내부함수를 외부에 전달했기 때문이다.
5.2 클로저와 메모리 관리
메모리 소모는 클로저의 본질적인 특성이다. 즉, 관리방법은 간단하다. 클로저는 어떤 필요에 의해 의도적으로 함수의 지역변수를 메모리를 소모하도록 함으로써 발생한다. 그렇다면 그 필요성이 사라진 시점에서 더는 메모리를 소모하지 않게 해주면 된다. 참조 카운트를 0으로 만드는 방법은 식별자에 참조형이 아닌 기본형 데이터(보통 null이나 undefined)를 할당하면 된다. 아래 코드는 메모리 해제 코드를 추가한 코드다.
// (1) return에 의한 클로저의 메모리 해제
var outer = function() {
var a = 1;
var inner = function() {
return ++a;
};
return inner;
})();
console.log(outer());
console.log(outer());
outer = null; // outer 식별자의 inner 함수 참조를 끊음.
// (2) 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);
})();
// (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 콜백 함수 내부에서 외부 데이터를 은닉하고자 할 때
5.3.2 접근 권한 제어(정보 은닉)
5.3.3 부분 적용 함수
5.3.4 커링 함수
'Javascript' 카테고리의 다른 글
인프런 리액트 강의 - Javascript 요약 ② (1) | 2024.10.28 |
---|---|
인프런 리액트 강의 - Javascript 요약 ① (1) | 2023.12.07 |
Chapter 4 요약 (0) | 2022.09.15 |
Chapter 3 요약 (0) | 2022.09.13 |
Chapter 2 요약 (0) | 2022.09.06 |