2024. 2. 16. 15:51ㆍ자바스크립트
const x = 1;
function outer() {
const x = 10;
const inner = function () {
console.log(x);
};
return inner;
}
const innerFunc = outer();
innerFunc();
맨 처음 전역 실행 컨텍스트가 콜스택에 푸쉬되고, outer함수가 호출되면서 outer함수 실행 컨텍스트 역시 콜스택에 푸쉬된다.
그리고 outer함수는 inner함수를 리턴하기 때문에 innerFunc는 inner함수를 가리키게된다.
inner함수를 리턴한 후에 outer함수는 콜스택에서 팝되어 사라지게된다.
그리고 innerFunc를 실행시켰을 때 어떤값이 찍히게 될까?
결과는 outer함수에 정의된 x값 10이 로그에 찍히게 된다.
이미 콜스택에 사라진 outer함수인데 어떻게 innerFunc는 x에 접근할 수 있을까?
이 원리를 이해하기 위해서 먼저 렉시컬 스코프 즉, 정적 스코프라는 용어에 대한 이해가 필요하다.
렉시컬 스코프
스코프와 스코프 체인을 공부할 때 한번쯤은 들어본 용어이다. 정적 스코프라고도 하는데 용어의 뜻은 다음과 같다.
자바스크립트 엔진은 함수를 어디서 호출했는지가 아니라 함수를 어디에 정의했는지에 따라 상위 스코프를 결정한다. 이를 렉시컬 스코프라 한다.
즉, 함수가 정의되어있는 위치에 따라 상위 스코프가 결정된다는 말이다. 함수가 어떻게 호출되는지에 따라 바인딩이 결정되는 this와는 다르다.
const x = 1;
function foo() {
const x = 10;
bar();
}
function bar() {
console.log(x);
}
foo(); // 1️⃣
bar(); // 2️⃣
1️⃣
foo를 호출하면 bar가 호출된다. 이때 bar에 x가 정의되어있지 않기 때문에 스코프 체이닝을 통해 x를 탐색하게 된다.
이때 탐색해야하는 경로가 렉시컬 환경에 기록되어있다.
실행 컨텍스트에서 배웠듯이 렉시컬 환경은 환경 레코드와 외부 렉시컬 환경의 참조값으로 구성되어있다.
바로 외부 렉시컬 환경의 참조값을 따라 상위 스코프로 이동하며 x를 탐색하는 것이다.
그렇다면 bar의 외부 렉시컬 환경의 참조값에는 무슨 값이 들어있을까?
여기서 렉시컬 스코프의 개념이 사용된다. bar가 foo에서 호출되었든 말든 상관없이 bar는 전역에 정의되어있기 때문에 bar의 외부 렉시컬 환경의 참조값에는 전역 렉시컬 환경이 기록되어있다.
따라서 1이 출력된다.
2️⃣
bar를 호출했더니 x를 로그에 찍어야한다. bar는 x가 없으므로 스코프 체이닝을 통해 x를 찾아야한다.
1️⃣에서 설명했듯이 상위 스코프로 이동하려는 경로는 렉시컬 환경의 외부 렉시컬 환경에 대한 참조에 저장되어있다.
저장된 참조값을 확인하니 전역 렉시컬 환경의 참조값이 기록되어있다. bar함수는 전역에 정의되었기 때문이다.
따라서 1이 출력된다.
정리하면 다음과 같다.
상위 스코프를 결정한다 = 외부 렉시컬 환경의 참조에 저장할 참조값을 결정한다
Environment 슬롯
렉시컬 스코프가 함수가 정의된 위치에 따라 상위 스코프를 결정하는 것이라고 말했다.
그렇다면 함수가 어떻게 자신이 정의된 위치를 기억해서 렉시컬 환경을 만들 때 외부 렉시컬 환경의 참조값으로 정의된 위치를 기록할 수 있는 것일까?
바로 Environment 슬롯에 미리 저장해두기 때문이다.
전역 코드가 평가되면서 환경 레코드에 선언문을 기록해두기 시작한다. 이때 함수 선언문은 런타임 이전에 평가되어 함수 객체를 생성하는데 이 때 Environment 슬롯에 현재 실행중인 전역 실행 컨텍스트의 렉시컬 환경의 참조값을 기록해둔다.
그리고 후에 함수가 실행되었을 때 렉시컬 환경을 생성하게 되는데 이때 외부 렉시컬 환경에 대한 참조값으로 Environment에 미리 기록해둔 값을 넣어주는 것이다.
미리 슬롯에 저장해두었기 때문에 자신이 정의된 위치 즉, 상위 스코프를 알 수 있고 이것을 렉시컬 환경이 생성될 때 외부 렉시컬 환경에 대한 참조값으로 저장해둠으로써 후에 스코프 체이닝을 통해 상위 스코프에 접근할 수 있는 것이다.
정리하면 다음과 같다.
Environment 슬롯에 기록한 참조값 = 외부 렉시컬 환경에 대한 참조 = 상위 스코프
클로저
const x = 1;
function outer() {
const x = 10;
const inner = function () {
console.log(x);
};
return inner;
}
const innerFunc = outer(); // ✅
innerFunc();
✅ 표시한 것까지의 함수 간의 렉시컬 환경과 콜스택을 간략하게 그려보았다.
세세한 렉시컬 환경에 대한 정보는 제외하고 Environment 슬롯과 외부 렉시컬 환경에 대한 참조에 집중해보자.
innerFunc에 outer함수가 리턴한 inner를 할당하게 되면 outer함수의 실행 컨텍스트는 콜스택에서 사라진다.
콜스택에 사라졌지만 inner함수의 함수 객체 Environment 슬롯에 outer함수의 렉시컬 환경을 참조하고 있으므로 outer 함수의 렉시컬 환경은 사라지지 않는다. 따라서 inner함수를 실행했을 때, 렉시컬 환경의 외부 렉시컬 환경에 대한 참조값으로 outer함수의 렉시컬 환경이 들어간다.
이렇게 이미 생명 주기가 종료한 외부 함수의 변수를 참조할 수 있는 중첩 함수를 클로저라고 부른다.
하지만 모든 함수를 클로저라고 부르지 않는다.
클로저는 중첩 함수가 상위 스코프의 식별자를 참조하고 있고 중첩 함수가 외부 함수보다 더 오래 유지되는 경우에 한정하는 것이 일반적이다.