mimizae 님의 블로그
클로저(Closure)란? (1) 본문

클로저는 함수를 일급 객체로 취급하는 함수형 프로그래밍 언어의 중요한 특성이지, 자바스크립트 고유의 개념이 아니다.
그래서 클로저의 정의가 ECMAScript 사양에 등장하지 않지만, MDN에서 정의하는 클로저에 대한 설명을 발췌해 보자면,
클로저는 함수와 그 함수가 선언된 렉시컬 환경과의 조합이다
예제를 통해 이 정의를 이해해 보자. 일단 함수가 선언된 렉시컬 환경이라는 키워드를 꼭 기억해두자!!!
const x = 1;
function outerFunc(){
const x = 10;
function innerFunc(){
console.log(x); //10
}
innerFunc();
}
outerFunc();
중첩 함수 innerFunc의 상위 스코프는 외부 함수 outerFunc의 스코프이다. (선언된 곳이니까!) 따라서 중첩 함수 innerFunc 내부에서 outerFunc의 x 변수에 접근할 수 있다.
만약 innerFunc 함수가 outerFunc 내부에서 선언되지 않았다면, outerFunc 내부에서 호출한다고 하더라도, outerFunc의 변수에 접근할 수 없다.
const x = 1;
function outerFunc(){
const x = 10;
innerFunc();
}
function innerFunc(){
console.log(x); // 1
}
outerFunc();
이런 현상이 발생하는 이유는 JS가 렉시컬 스코프를 따르기 때문이다!
실행 컨텍스트 관점에서의 렉시컬 스코프
렉시컬 스코프에 관해서는 따로 정리한 바 있지만, 실행 컨텍스트의 관점에서 다시 한 번 정의 내려 보자!!
var x = 1;
function foo(){
var x = 10;
bar();
}
function bar(){
console.log(x);
}
foo(); //?
bar(); //?
위 예제의 foo 함수와 bar 함수는 전역에서 정의된 함수이기에 두 함수의 상위 스코프는 전역이다.
실행 컨텍스트 글에서 살펴 보았듯이, 스코프의 실체는 실행 컨텍스트의 렉시컬 환경이다.
(렉시컬 환경은 환경 레코드와 외부 렉시컬 환경에 대한 참조로 이루어져 있는데, 이 참조가 상위 스코프를 가리키기 때문!)
이 렉시컬 환경은 자신의 외부 렉시컬 환경에 대한 참조를 통해 상위 렉시컬 환경과 연결된다!! 이것이 바로 스코프 체인이다.
따라서, 함수의 상위 스코프를 결정한다는 것은 렉시컬 환경의 외부 렉시컬 환경에 대한 참조에 저장할 참조값을 결정한다는 의미와 같다.
(렉시컬 환경의 외부 렉시컬 환경에 대한 참조에 저장할 참조 값이 바로 상위 렉시컬 환경에 대한 참조이며, 이것이 상위 스코프이기 때문이다!!!)
이 개념을 반영해 다시 한번 렉시컬 스코프에 대해 정의해 본다면,
렉시컬 환경의 외부 렉시컬 환경에 대한 참조에 저장할 참조 값(=상위 스코프에 대한 참조)은 함수 정의가 평가되는 시점에 함수가 정의된 환경에 의해 결정된다. 이것이 바로 렉시컬 스코프이다!!
함수 객체의 내부 슬롯 [[Environment]]
함수가 정의된 환경과 호출되는 환경은 다를 수 있다.
따라서, 렉시컬 스코프가 가능하려면 함수 자신이 호출되는 환경과는 별개로 자신이 정의된 환경, 즉 상위 스코프를 기억해야 한다.
이를 위해 함수는 자신의 내부 슬롯 [[Environment]] 에 상위 스코프의 참조를 저장한다!!!
정확히 말하자면, 함수 정의가 평가되어서 함수 객체를 생성할 때, 자신의 내부 슬롯 [[Environment]]에 상위 스코프의 참조를 저장한다.
이때 상위 스코프의 참조는 현재 실행 중인 실행 컨텍스트의 렉시컬 환경을 가리킨다!!
그 이유는, 함수 정의가 평가 되어 함수 객체를 생성하는 시점은 함수가 정의된 환경 즉, 상위 함수(또는 전역 코드)가 평가 또는 실행 되고 있는 시점이기 때문이다!!
그렇기에 슬롯에 저장할 상위 스코프의 참조는 '현재 실행 중인 실행 컨텍스트'의 렉시컬 환경을 가리킨다는 것이다.
예를 들어, 전역에서 정의된 함수 선언문은 전역 코드가 평가!!!되는 시점에 평가 되어 함수 객체를 생성한다.
이때 함수 객체의 내부 슬롯 [[Environment]]에는 현재 실행 중인 실행 컨텍스트의 렉시컬 환경(=전역 렉시컬 환경)의 참조가 저장된다.
즉!!!!!!
함수 객체의 내부 슬롯 [[Environment]]에 현재 실행 중인 실행 컨텍스트의 렉시컬 환경의 참조가 바로 상위 스코프이다.
또, 함수 자신이 호출될 때 평가 되며 생성될 함수 렉시컬 환경의 '외부 렉시컬 환경에 대한 참조'에 저장할 참조 값이다.
함수 객체는 내부 슬롯[[Environment]]에 저장된 상위 스코프를 자신이 존재하는 한 기억한다.
(이 점을 절대절대절대!!!! 까먹지 않기를 바란다.)
아래 예제를 다시 봐 보자.
var x = 1;
function foo(){
var x = 10;
bar();
}
// 함수 bar는 자신의 상위 스코프, 즉 전역 렉시컬 환경을 [[Environment]]에 저장하여 기억한다.
function bar(){
console.log(x);
}
foo(); //?
bar(); //?
foo 함수와 bar 함수는 모두 전역에서 함수 선언문으로 정의되었다. 따라서, 두 함수 모두 전역 코드가 평가되는 시점에 평가되어 함수 객체를 생성하고 전역 객체 window의 메서드가 된다. (왜인지는 이 글을 참고해 주길 바란다 ㅎ.ㅎ)

이때 생성된 함수 객체의 내부 슬롯[[Environment]]에는 현재 실행 중인 실행 컨텍스트의 렉시컬 환경(=전역 렉시컬 환경)의 참조가 저장된다.
(위 그림에서 1번을 보면 된다!! ㅎㅎ)
이후 두 함수가 호출되면 함수 내부로 코드의 제어권이 넘어가며 함수 코드를 평가하기 시작한다.
이 함수 코드 평가 과정 중 외부 렉시컬 환경에 대한 참조 값에는 내부 슬롯[[Environment]]에 저장된 렉시컬 환경의 참조가 할당된다. (위의 그림에서 2와 3이다 ㅎ.ㅎ)
클로저와 렉시컬 환경
아니 그래서 클로저가 뭔데?!?!!! 싶을 것이다.
우선 다음 예제를 살펴보자.
const x = 1;
// 1
function outer(){
const x = 10;
cosnt inner = function() {console.log(x); }; // 2
return inner;
}
// outer 함수는 중첩 함수 inner을 반환하는 동시에 outer 함수의 실행 컨텍스트는 스택에서 pop되어 제거된다.
const innerFunc = outer(); // 3
innerFunc(); // 4 -> 10
이 예제가 약간 이상해 보인다. 왜 오류가 안 나지??
outer 함수를 호출하면 outer 함수는 중첩 함수 inner을 반환하고 생명 주기를 마감한다. (die)
즉, outer 함수의 실행이 종료되면 실행 컨텍스트 스택에서 outer 함수의 실행 컨텍스트는 제거된다.
outer 함수 자체의 생명 주기가 마감 되었으니 당연히 outer 함수의 지역 변수 x 또한 생명 주기를 마감할 테고 outer 함수의 x 변수에 접근할 방법은 딱히 없어 보인다.
그러나!!! 위의 코드 실행 결과는 10이 출력된다. (outer의 부활??)
그 이유는, inner 함수가 클로저이기 때문이다.
⭐️⭐️⭐️
외부 함수보다 중첩 함수가 더 오래 유지되는 경우 중첩 함수는 이미 생명 주기가 종료된 외부 함수의 변수를 참조할 수 있다.
이러한 중첩 함수를 클로저라고 부른다.
위의 MDN의 정의를 다시 가져 와 보자...
"클로저는 함수와 그 함수가 선언된 렉시컬 환경과의 조합이다"
자바스크립트의 모든 함수는 자신의 상위 스코프를 기억한다고 했다.
모든 함수가 기억하는 상위 스코프는 함수를 어디서 호출하든 상관 없이 유지된다. 따라서 함수를 어디서 호출하든 상관없이 함수는 언제나 자신이 기억하는 상위 스코프의 식별자를 참조할 수 있으며 식별자에 바인딩된 값을 변경할 수도 있다.
위 예제에서 inner 함수는 자신이 평가될 때 자신이 정의된 위치에 의해 결정된 상위 스코프를 [[Environment]] 내부 슬롯에 저장한다.
이때 저장된 상위 스코프는 함수 자신이 존재하는 한 유지된다!!! (아까 까먹지 말라고 한 바로 그것!!!!!)

outer 함수가 평가되어 함수 객체를 생성할 때 전역 렉시컬 환경을 outer 함수 객체의 내부 슬롯[[Environment]]에 저장한다.

이제 outer 함수가 호출되면, outer 함수의 렉시컬 환경이 생성되고 앞서 outer 함수 객체의 내부 슬롯[[Environment]]에 저장된 상위 스코프 값이 outer 함수 렉시컬 환경의 "외부 렉시컬 환경에 대한 참조"에 할당 된다.
그리고 중첩 함수 inner가 평가된다. 이때, inner 함수는 함수 표현식으로 정의되었기 때문에 런타임에 평가된다!
중첩함수 inner은 자신의 내부 슬롯[[Environment]]에 outer 함수의 렉시컬 환경을 상위 스코프로서 저장한다.

outer 함수의 실행이 종료하면 inner 함수를 반환하면서 outer 함수의 생명주기가 종료된다.
이때!!! outer 함수의 실행 컨텍스트는 실행 컨텍스트에서 제거되지만, outer 함수의 렉시컬 환경까지 소멸하는 것은 아니다.
가비지 컬렉터는 누군가가 참조하고 있는 메모리 공간을 함부로 해제 하지 않는다.
inner 함수는 전역 변수 innerFunc에 의해 참조 되고 있으니 살아있고, 살아있는 inner 함수가 outer 함수의 렉시컬 환경을 [[Environment]] 내부 슬롯으로 참조하고 있으니 두 가지 모두 가비지 컬렉션의 대상이 되지 않는다!!!

중첩 함수 inner는 외부 함수 outer 보다 오래 생존했다.
이때 외부 함수보다 더 오래 생존한 중첩 함수는 외부 함수의 생존 여부(실행 컨텍스트의 스택 존재 여부)에 상관 없이 상위 스코프를 기억한다. 당연히!!! inner 내부에서는 상위 스코프를 참조할 수 있으니 outer 스코프의 식별자를 참조할 수 있고 식별자의 값을 변경할 수도 있다.
그렇다면... 모든 함수가 클로저?
자바스크립트의 모든 함수는 자신의 상위 스코프를 기억한다면... 이론적으로 모든 함수가 클로저인가??
일반적으로는 그렇지 않다.
다음 예제들을 디버깅 해보자.
<!DOCTYPE html>
<html>
<body>
<script>
function foo(){
const x = 1;
const y = 2;
// 이 중첩 함수 bar는 일반적으로 클로저라고 하지 않는다.
function bar(){
const z = 3 ;
debugger;
// 상위 스코프의 식별자를 참조하고 있지 않기 때문!
console.log(z);
}
return bar;
}
const bar = foo();
bar();
</script>
</body>
</html>

위의 예제처럼 중첩 함수 bar는 함수 foo보다 더 오래 유지되긴 하지만 상위 스코프의 어떤 식별자도 참조하지 않는다.
이런 경우 모던 브라우저는 최적화를 통해 상위 스코프를 기억하지 않는다.
참조하지도 않는 식별자를 기억하는 것은 메모리를 낭비하는 것이기 때문!!
<!DOCTYPE html>
<html>
<body>
<script>
function foo(){
const x = 1;
// 이 중첩 함수 bar는 클로저였지만 곧바로 소멸한다.
// 그래서 일반적으로 클로저라고 부르지 않는다.
function bar(){
debugger;
// 상위 스코프의 식별자를 참조하고 있긴 함
console.log(x);
}
bar();
}
foo();
</script>
</body>
</html>

상위 스코프의 식별자를 참조하고 있으므로 클로저이다. 하지만 외부 함수 foo의 밖으로 중첩 함수가 반환되지 않는다.
즉, 외부 함수보다 생명 주기가 짧다!!
이런 경우, 중첩 함수 bar는 클로저였지만 외부 함수보다 오래 남아 생명 주기가 종료된 외부 함수의 식별자를 참조할 수 있다는 클로저의 본질에 부합하지 않는다.
<!DOCTYPE html>
<html>
<body>
<script>
function foo(){
const x = 1;
const y = 2;
// 이 중첩 함수 bar는 완벽한 클로저
function bar(){
debugger;
console.log(x);
}
return bar;
}
const bar = foo();
bar();
</script>
</body>
</html>

이 예제는 외부 함수보다 중첩 함수가 더 오래 살아남고, 상위 스코프의 식별자 x를 참조하고 있으므로 클로저이다.
이러한 경우만을 클로저로 한정하는 것이 일반적이다.
다만, 클로저인 중첩 함수 bar는 상위 스코프의 x, y 식별자 중에서 x만 참조하고 있다.
이런 경우 대부분의 모던 브라우저는 최적화를 통해 클로저가 참조하고 있는 식별자만을 기억한다!!
클로저에 의해 참조되는 상위 스코프의 변수를 자유 변수라고 부른다.
즉, 클로저란 자유 변수에 묶여있는 함수라고 또 다른 정의를 내릴 수 있겠다!!!!
다음 글에서 이어집니다... ><
이 글은 모던 자바스크립트 딥 다이브의 클로저 부분을 공부한 후 쓴 글입니다.
'JS' 카테고리의 다른 글
| DOM과 브라우저 (0) | 2025.11.18 |
|---|---|
| 클로저란(Closure)? (2) (0) | 2025.11.11 |
| this에 대해서 (0) | 2025.11.11 |
| 실행 컨텍스트 보충 (전역 객체와 전역 코드 평가) (0) | 2025.11.09 |
| 실행 컨텍스트란? (0) | 2025.11.03 |