이 글은 '이웅모'님의 '모던 자바스크립트 Deep Dive' 책을 통해 공부한 내용을 정리한 글입니다. 저작권 보호를 위해 책의 내용은 요약되었습니다.
함수를 일급 객체로 취급하는 함수형 프로그래밍 언어의 특성으로, 함수와 그 함수가 선언되었을 때의 렉시컬 환경과의 조합을 말한다. 자바스크립트 엔진은 함수를 어디에서 정의했는지에 따라 상위 스코프를 결정한다. 이를 렉시컬 스코프라고 한다.
함수가 정의된 환경과 호출된 환경은 다를 수 있으므로 렉시컬 스코프가 가능하려면 자신의 상위 스코프를 기억해야한다. 따라서, 함수는 자신의 내부 슬롯 [[Environment]]에 상위 스코프를 저장한다. 이후 함수가 호출될 때 자바스크립트 엔진은 실행 컨텍스트의 스코프 체인을 따라 호출된 함수의 렉시컬 스코프를 조회할 수 있다.
예시를 살펴보자.
function outer() {
const age = 20;
const inner = function () { console.log(age); };
return inner;
}
const inner = outer();
inner(); // 20
outer
함수는 내부함수 inner
를 반환하고 생을 마감했지만 outer
의 변수 age
를 여전히 inner 함수가 참조하는 것을 볼 수 있다. 위와 같이, 자신을 포함하고 있는 외부함수보다 내부함수가 더 오래 유지되는 경우, 외부 함수 밖에서 내부함수가 호출 되더라도 외부함수의 지역 변수에 접근할 수 있는 함수를 클로저라 부른다.
추가로 클로저에 의해 참조되는 외부함수의 변수, 위 예제에서는 변수 age
를 자유변수(Free Variable)이라고 부른다.
위 코드를 그림으로 보면 다음과 같다.
클로저가 가장 유용하게 사용되는 상황은 현재 상태를 기억하고 변경된 최신 상태를 유지하는 경우이다.
function counter(func) {
let count = 0;
return function () {
count = func(count);
return count;
};
}
function increase(state) {
return ++state;
}
function decrease(state) {
return --state;
}
// increaser와 decreaser는 독립된 렉시컬 환경이므로 상태가 연동되지 않음
const increaser = counter(increase);
console.log(increaser()); // 1
console.log(increaser()); // 2
const decreaser = counter(decrease);
console.log(decreaser()); // -1
console.log(decreaser()); // -2
클로저를 활용하면 다음과 같은 이점을 얻을 수 있다.
위 예시에서 클로저를 활용하지 않고 동일한 기능을 구현하려면 count
란 변수를 전역 변수로 선언하여 활용하는 방법밖에 없다. 전역 변수는 언제든지 누구나 접근할 수 있으며 변경 또한 가능하다는 위험성이 있다. 즉, 누군가 의도치 않게 count
란 전역 변수를 수정한다면 이는 오류로 이어질 수 있다.
위 예시에서 count
는 private
한 변수이다. increase
또는 decrease
함수가 호출될 때만 변경이 가능하며 외부에서는 직접 접근할 수 없다. 이는 전역 변수로 사용했을 때 의도치 않은 변경을 걱정할 필요가 없어지며 보다 안정적인 프로그래밍이 가능하다.
상태 변경이나 가변(mutable) 데이터를 피하고 불변성(Immutability)을 지향하는 함수형 프로그래밍에서 부수 효과(Side effect)를 최대한 억제하여 오류를 피하고 프로그램의 안정성을 높이기 위해 클로저는 적극적으로 사용된다.
위 예시를 다음과 같이 생성자 함수로 구현하여 클로저를 활용할 수 있다.
function Counter() {
// 자유 변수 count
let count = 0;
this.increase = function() {
return ++count;
};
this.decrease = function() {
return --count;
};
}
const counter = new Counter();
console.log(counter.increase()); // 1;
console.log(counter.decrease()); // 0;
생성자 함수 Counter
에 변수 count
는 인스턴스 프로퍼티가 아닌 변수이다. 따라서 외부에서 접근할 수 없기에 private
하다. 또한 this.increase
와 this.decrease
는 클로저이기에 렉시컬 환경인 생성자 함수 Counter
의 count
변수에 접근이 가능하다. 추가로 이 두 메서드는 동일한 렉시컬 환경인 생성자 함수 Counter
에 속하기에 변수 count
를 공유한다.