JavaScript를 공부하면서 클로저라는 키워드를 많이 접할 수 있다. 과거에 클로저에 대해 공부를 해본적이 있지만 내부 스코프에서 상위 스코프에 대한 접근? 이렇게 두루뭉실하게 알고 있던 개념이라 이번 기회에 정리를 해보고자 한다.
MDN에서는 클로저에 대해 다음과 같이 정의하고 있다.
A closure is the combination of a function and the lexical environment within which that function was declared.
이를 번역하면, 클로저는 함수와 그 함수가 선언된 렉시컬 환경과의 조합이라는 것이다.
몇 번을 읽어봐도 '이게 무슨 말인가?'라는 생각이 들 정도로 정의가 무척 난해하다.
위의 정의에서 이해해야 할 핵심 키워드는 함수가 선언된 렉시컬 환경
이다.
아래의 코드를 보자.
const a = 1;
function outerFunc() {
const a = 100;
function innerFunc() {
console.log(a); // 100
}
innerFunc();
}
outerFunc();
outerFunc 라는 함수가 정의되었고, outerFunc 함수 내부에서 중첩 함수 innerFunc가 정의되고 호출되었다. 이 때 innerFunc의 상위 스코프는 외부 함수 outerFunc의 스코프이다.
따라서 innerFunc 내부에서 자신을 포함하고 있는 외부 함수 outerFunc의 a 변수에 접근할 수 있다.
아래의 경우를 보자.
const a = 1;
function outerFunc() {
const a = 100;
innerFunc();
}
function innerFunc() {
console.log(a); // 1
}
outerFunc();
위의 코드의 경우, innerFunc 함수가 outerFunc 함수 안에서 정의되지 않고 호출되기만 했다. 이 경우, innerFunc 함수는 outerFunc의 변수에 접근할 수 없다.
위와 같은 현상이 발생하는 이유는 JavaScript가 렉시컬(lexical) 스코프
를 따르는 프로그래밍 언어이기 때문이다.
렉시컬 스코프 : 자바스크립트 엔진은 함수를 어디서 호출했는지가 아니라 함수를 어디에 정의했는지에 따라 상위 스코프를 결정한다.
렉시컬 스코프가 가능하려면 함수는 자신이 정의된 환경, 즉 상위 스코프를 기억해야 한다. 이를 위해 함수는 자신의 내부 슬롯 [[Environment]]에 자신의 정의된 환경(상위 스코프)의 참조를 저장한다.
const a = 1;
function outerFunc() {
const a = 100;
function innerFunc() {
console.log(a); // 100
}
return innerFunc;
}
const fun = outerFunc();
fun();
위의 코드에서, outerFunc 함수가 정의되었고, outerFunc 함수는 자신의 스코프 안에서 정의된 innerFunc 함수를 반환하고 생명 주기를 마감한다.즉, outerFunc 함수는 실행된 이후 콜스택(실행 컨텍스트 스택)에서 제거되었으므로 outerFunc 함수의 변수 a 또한 더이상 유효하지 않게 되어 변수 a에 접근할 수 있는 방법은 없어보인다.
하지만, 위의 코드의 실행 결과는 변수 a의 값인 100이다.
이처럼 자신을 포함하고 있는 외부함수보다 내부함수가 더 오래 유지되는 경우, 외부 함수 밖에서 내부함수가 호출되더라도 외부함수의 지역 변수에 접근할 수 있는데 이러한 함수를 클로저(Closure)
라고 부른다.
다시 MDN의 정의를 살펴보자.
A closure is the combination of a function and the lexical environment within which that function was declared.
클로저는 함수와 그 함수가 선언되었을 때의 렉시컬 환경과의 조합이다.
위의 정우에서 함수
란 반환된 내부함수를 의미하고, 렉시컬 환경
이란 내부 함수가 선언되었을 때 스코프를 의미한다.
즉, 클로저는 내부함수가 자신이 선언되었을 때의 환경(Lexical environment)인 스코프를 기억하고 자신이 선언되었을 때의 환경(스코프) 밖에서 호출되어도 그 환경(스코프)에 접근할 수 있는 함수를 말한다.
1. 상태 유지
클로저가 가장 유용하게 사용되는 상황은 현재 상태를 기억하고 변경된 최신 상태를 유지하는 것이다.
const toggle = (function () {
let status = false;
// 클로저 반환
return function () {
status = !status;
};
})();
toggle();
위의 코드에서는 toggle이라는 함수를 정의하고 즉시 실행시켰다. 즉시실행함수가 반환한 함수는 자신이 생성되었을 때의 렉시컬 환경에 속한 변수 status를 기억하는 클로저이다.
2. 정보의 은닉
function Counter() {
var counter = 0;
// 클로저
this.increase = function () {
return ++counter;
};
// 클로저
this.decrease = function () {
return --counter;
};
}
const counter = new Counter();
console.log(counter.increase()); // 1
console.log(counter.decrease()); // 0
생성자 함수 Counter는 increase, decrease 메소드를 갖는 인스턴스를 생성한다. 이 메소들은 모두 자신의 렉시컬 환경을 공유한다.
이때 생성자 함수 Counter의 변수 counter는 this에 바인딩된 프로퍼티가 아니라 변수다. 따라서, 변수 counter는 생성자 함수 Counter 외부에서 접근할 수 없다.
하지만 생성자 함수 Counter가 생성한 인스턴스 메소드인 increase, decrease는 클로저이기 때문에 자신의 렉시컬 환경인 Counter의 변수 counter에 접근할 수 있다.
이러한 클로저의 특징을 사용해 클래스 기반 언어의 private
키워드를 흉내낼 수 있다.