클로저는 자바스크립트 고유의 개념이 아니다.
함수를 일급객체로 취급하는 함수형 프로그래밍 언어에서 공통적으로 사용되는 특성이다.
따라서 ECMAScript 사양에 정의되어있지 않다.
대신 MDN에 따른 클로저의 정의는 다음과 같다.
클로저는
함수
와그 함수가 선언된 렉시컬 환경
과의 조합이다.
렉시컬 환경? 렉시컬 환경과의 조합? 처음 읽을 때는 무슨 말인지 도통 이해가 가질 않는다.
정의를 이해하기 위해 먼저 아래 코드들을 살펴보자
// 1. inner 함수가 outer 함수 바깥에서 정의된 경우
const x = 1;
function outer() {
const x = 10;
inner();
}
function inner() {
console.log(x);
}
outer(); // 1
// 2. inner 함수가 outer 함수 내부에서 정의된 경우
const x = 1;
function outer() {
const x = 10;
function inner() {
console.log(x);
}
inner();
}
outer(); // 10
코드를 실행해보면, inner
함수가 정의된 위치에 따라 참조하는 변수 x
가 달라지는 것을 확인할 수 있다.
outer
함수 외부에서 inner
함수를 선언했을 때는 outer
외부의 변수 x
를 참조하고,
outer
함수 내부에서 inner
함수를 선언했을 때는 outer
내부의 변수 x
를 참조한다.
자바스크립트는 정적 스코프를 따르는 언어이다. (대부분의 언어가 그렇다)
정적 스코프를 따르는 언어는 함수가 선언된 위치에 따라 상위 스코프가 정해진다.
즉 어디에 함수를 정의했는지에 따라서 상위 스코프가 정해진다.
또한 자바스크립트에서 스코프라는 개념은 렉시컬 환경
을 통해 구현된다.
즉 렉시컬 환경
이 스코프의 실체라고 할 수 있다.
MDN에 의한 클로저의 정의를 다시 한번 보자
클로저는
함수
와그 함수가 선언된 렉시컬 환경
과의 조합이다.
위에서 말했듯이 정적 스코프를 따르는 언어는 함수가 선언된 위치에 따라 상위 스코프(상위 렉시컬 환경)가 정해진다.
따라서 정의를 쉽게 바꾸면 다음과 같다.
클로저는
함수
와그 함수의 상위 스코프
와의 조합이다.
const x = 1;
function outer() {
const x = 10;
const inner = function () {
console.log(x);
}
return inner;
}
const inner = outer(); // ...(*)
inner(); // 10
위 코드는 outer
함수 외부와 내부에 각각 다른 값을 가지는 변수 x
를 선언하고,
중첩함수 inner
로 하여금 console.log
를 통해 x
를 출력하게 만든다.
(*)
에서 outer
함수의 호출 후 outer
함수의 실행이 끝나면 outer
함수의 생명주기도 종료되어
outer
함수 내부 변수 x
에 접근할 수 없을 것 처럼 보인다.
하지만 코드를 실행해보면 내부 변수 x
의 값인 10을 출력한다.
이는 outer
함수의 스코프(렉시컬 환경)가 아직 유효하다는 것을 의미한다.
사실 참조되고 있는 렉시컬 환경은 해당 문맥의 생명주기가 종료되어도 가비지 컬렉터에 의해 메모리가 해제되지 않는다.
outer
함수는 outer
함수의 스코프를 외부 스코프로 참조하는 inner
함수를 반환해서 저장했기 때문에 outer
함수의 렉시컬 환경은 사라지지 않았다.
이처럼 중첩함수가 외부함수보다 오래 유지되는 경우
중첩함수는 이미 생명주기가 종료된 외부함수의 변수를 참조할 수 있는데
이 중첩함수를 클로저라고 한다.
클로저는 특정 함수에게만 상태 변경을 허용함으로써 상태가 의도치 않게 변경되는 것을 막는다.
let num = 0;
const increase() {
return ++num;
}
console.log(increase()); // 1
console.log(++num); // 2
console.log(increase()); // 3
위 코드는 increase
함수가 변수 num
의 상태를 변경하고 있지만,
++num
처럼 다른 코드에 의해 변수 num
의 상태가 변경될 수 있는 위험을 가지고 있다.
const increase() {
let num = 0;
return ++num;
}
console.log(increase()); // 1
console.log(increase()); // 1
console.log(increase()); // 1
위 코드는 함수를 사용할 때마다 변수 num
이 새롭게 선언되기 때문에 의도한 대로 사용할 수 없다.
const increase =( function () {
let num = 0;
return function () {
return ++num;
}
}())
console.log(increase()); // 1
console.log(increase()); // 2
console.log(increase()); // 3
변수 num
을 은닉하고 increase
함수에 의해서만 상태가 변경되는 올바른 예시이다.
아래는 decrease
함수까지 제공하는 예시이다.
const counter = (function () {
let num = 0;
return {
increase() {
return ++num;
},
decrease() {
return num > 0 ? --num : 0;
}
}
}());
console.log(counter.increase()); // 1
console.log(counter.increase()); // 2
console.log(counter.increase()); // 3
console.log(counter.decrease()); // 2
console.log(counter.decrease()); // 1
console.log(counter.decrease()); // 0
console.log(counter.decrease()); // 0
이웅모, 『모던 자바스크립트 Deep Dive - 자바스크립트의 기본 개념과 동작 원리』, 위키북스(2020), p388-408