

안녕하신가!
오늘은 좀 어려운 자바스크립트 개념 closure 배워보자.
우선 알고 가야 할 부분이 있다.
const x = 1;
function outerFunc() {
const x = 10;
function innerFunc() {
console.log(x); // 10
}
innerFunc();
}
outerFunc();
위의 예제에서 outerFunc 함수 내부에는 중첩 함수인 innerFunc가 정의되고 호출 되었다.
이때 innerFunc의 상위 스코프는 외부 함수 outerFunc의 스코프이다. 따라서 중첩 함수인 innerFunc 내부에서 자신을 포함하고 있는 외부함수 outerFunc의 x 변수에 접근 가능하다.
만약 innerFunc 함수가 outerFunc함수의 내부에 정의된 중첩함수가 아니라면 innerFunc 함수를 outerFunc 함수 내부에서 호출하여도 innerFunc 함수에서 outerFunc 함수의 변수에 접근 불가능하다.
const x = 1;
function outerFunc() {
const x = 10;
innerFunc();
}
function innerFunc() {
console.log(x); // 1
}
outerFunc();
위와 같이 동작하는 이유는 JavaScript가 Lexical Scope를 따르는 프로그래밍 언어이기 때문이다.
여기서 Lexical Scope란?
JavaScript 엔진이 함수를 어디서 호출했는지가 아니라 함수를 어디에 정의했는지에 따라 상위 스코프를 결정하게 되는 것을 Lexical Scope라고 한다.
렉시컬 환경은 Outer Lexical Environment Reference를 통해 상위 렉시컬 환경과 연결된다. 이것이 Scope Chain이다.
따라서 함수의 상위 스코프를 결정한다라는 것은 렉시컬 환경의 Outer Lexical Environment Reference에 저장할 참조값을 결정한다는 것과 같은 것이다.
함수는 정의된 환경과 호출되는 환경이 다를 수 있기 때문에 Lexical Scope가 가능하려면 정의된 환경을 기억할 수 있는 무언가가 필요하다. 이를 위해 함수 내부 슬롯 [[Environment]]에 자신이 정의된 환경인 상위 스코프의 참조를 저장한다.
전역에서 정의된 함수 선언문은 전역 코드가 평가되는 시점에 평가되어 함수 객체를 생성한다. 이때 생성된 함수 객체의 내부 슬롯 [[Environment]]에는 함수 정의가 평가 되는 시점, 즉 전역 코드 평가 시점에 실행 중인 컨텍스트의 렉시컬 환경인 전역 렉시컬 환경의 참조가 저장된다.
함수 내부에 정의된 함수 표현식은 외부 함수 코드가 실행되는 시점에 평가되어 함수 객체를 생성한다. 이때 생성된 함수 객체의 내부 슬롯 [[Environment]]에는 함수 정의가 평가되는 시점, 즉 외부 함수 코드 실행 시점에 실행 중인 실행 컨텍스트의 렉시컬 환경인 외부 함수 렉시컬 환경의 참조가 저장된다.
따라서 함수 객체의 내부 슬롯 [[Environment]]에 저장된 현재 실행 중인 실행 컨텍스트의 렉시컬 환경의 참조가 바로 상위 스코프이다. 또한 자신이 호출되었을 때 생성될 함수 렉시컬 환경의 Outer Lexical Environment Reference에 저장될 참조 값이다. 함수 객체는 내부 슬롯 [[Environment]]에 저장한 렉시컬 환경의 참조, 즉 상위스코프를 자신이 존재하는 한 기억한다.
const x = 1;
function outer() {
const x = 10;
const inner = function () {
console.log(x);
};
return inner;
}
const innerFunc = outer();
innerFunc() // 10
위의 예제에서 outer 함수를 호출하면 outer 함수는 중첩 함수 inner를 반환하고 생명 주기를 마감한다. 즉 outer 함수의 실행이 종료되면 outer 함수의 실행 컨텍스트는 실행 컨텍스트 스택에서 제거된다. 이때 outer 함수가 가지고 있던 지역 변수 x와 변수 값 10을 저장하고 있던 outer 함수의 실행 컨텍스트가 제거되었으므로 outer 함수의 지역 변수 x 또한 생명 주기를 마감한다. 따라서 outer 함수의 지역 변수 x는 더 이상 유효하지 않을 되어 x 변수에 접근할 방법이 없어보인다.
하지만 코드 실행 결과 outer 함수의 지역 변수 x의 값인 10을 console에 출력 해주고 있다. 이미 생명 주기가 종료되어 실행 컨텍스트에서 제거된 outer 함수의 지역 변수 x가 다시 살아난 듯이 동작하고있다.
이처럼 외부 함수보다 중첩 함수가 더 오래 유지되는 경우 중첩 함수는 이미 생명주기가 종료한 외부 함수의 변수를 참조 할 수 있다. 이러한 중첩 함수를 closure라고 부른다.
렉시컬 환경을 통해 조금 더 자세히 알아보자면, outer 함수 내부 슬롯 [[Environment]]의 Outer Lexical Environment Reference에 전역 렉시컬 환경을 할당하게되고 inner 함수 객체 내부 슬롯 [[Environment]]의 Outer Lexical Environment Reference에 outer 함수의 렉시컬 환경을 할당하게된다.
outer 함수의 실행이 종료되면 inner 함수를 반환하면서 outer 함수의 생명주기가 종료되는데, 이때 outer 함수의 실행 컨텍스트는 실행 컨텍스트 스택에서 제거되지만 outer 함수의 렉시컬 환경까지 소멸되는 것은 아니다. 그래서 중첩 함수 inner는 이미 생명주기가 종료한 외부 함수의 변수를 참조할 수 있게된다.
자바스크립트의 모든 함수는 상위 스코프를 기억하므로 이론적으로는 모든 함수는 closure다. 하지만 일반적으로 모든 함수를 closure라고 부르지 않는다. 왜냐하면 중첩 함수에서 상위 스코프의 식별자를 참조하지 않거나 외부 함수로 중첩 함수가 반환되지않아 중첩함수가 외부함수 보다 생명 주기가 짧은 경우는 closure라고 부르지 않는다.
closure는 중첩 함수가 상위스코프의 식별자를 참조하고 있고, 중첩 함수가 외부 함수보다 더 오래 유지되는 경우에 한정하는 것이 일반적이기 때문이다.
그럼 closure는 이제 이런거구나 알겠는데, closure를 활용하면 좋은 상황은 어떤 것들이 있을까? 다음 시간에 알아보도록 하자.