Javascript에서 가장 중요한 개념 중 하나인 클로저(closure) 에 대해 깊게 알아보자.
면접관의 꼬리질문에도 당황하지 않을 수 있도록!
면접관: 클로저에 대해 설명해 주시겠어요?
???: 내부 함수가 외부 함수의.... 이렇고 저렇고... 입니다
면접관: 그러면 어떠한 원리로 클로저가 동작하는지 말씀해 주시겠어요?
???: ....
클로저는 자신이 선언 됐을 당시의 렉시컬 스코프를 기억하는 함수이다
쉽게 말하면, 클로저는 함수가 자신이 생성될 때의 스코프를 기억하고 있다가, 해당 스코프가 외부 환경에서 접근할 수 없게 된 후에도 그 기억을 유지하는 현상이다.
let value = 10;
const outer = () => {
let value = 1;
const inner = () => {
console.log(value);
};
return inner;
};
const test = outer();
test(); // 1
inner()함수는 선언될 당시의 상위 스코프인 outer()함수의 스코프를 참조하고 있다.
즉 inner()함수는 선언됐을 당시의 렉시컬 스코프(=outer()함수의 렉시컬 환경)을 참조하고 있는 것이다.
이게 클로저가 된다.
return 문을 통해 함수를 반환하는 외부 함수(outer) 를 만들고
return 문을 통해 반환되는 내부 함수(inner)는 외부 함수(outer)의
변수를 참조하는 것이다. ⇒ 반환된 내부 함수(inner)는 클로저가 된다.
외부 함수에서, 내부 함수를 만나게 되면
내부 함수 선언 당시의 렉시컬 환경(=외부 함수의 환경 레코드)를 기억해 두었다가
나중에 내부 함수를 실행할 때 ,기억해둔 외부 함수의 환경 레코드를
내부 함수의 실행 컨텍스트의 외부 환경 참조로 넣어 줘야 하는 것이다
앞선 글에 따르면 ,
나중에 내부 함수를 실행할 때 내부 함수의 실행 컨텍스트에
외부 함수의 환경 레코드를 참조해야 하지만 '이미 외부 함수는 종료되어버린 상태'이다.
=변수 선언 단계
키
로 등록흔히들 말하는 런타임이다. 각 줄을 읽어가며 변수 초기화와 할당단계가 일어나면서 환경 레코드의 키에 값들이 채워지게 된다=변수 초기화, 할당 단계
변수의 경우 키: 변수 값
형태로 저장
함수의 경우 키: 함수객체의 참조
형태로 저장
(var로 선언된 변수나 함수 선언문의 경우는 예외 적으로 동작한다.)
여기서 말하는 ‘함수 객체’가 핵심이다.
함수 객체는 함수가 선언될 때 생성되는 특별한 객체이다.
이 객체는 함수의 동작과 관련된 속성들을 가지고 있으며, JS 런타임에서 함수가 호출될 때 이를 참조하여 실행된다.
함수 객체는 일반 객체와 달리 실행 가능한 코드와 스코프 정보를 포함하고 있어 함수로서의 역할을 수행하게 된다.
함수객체 역시 환경레코드와 마찬가지로 Heap메모리에 저장된다. 실제로 환경 레코드는 해당 함수 객체의 '참조'를 저장해 놓고 사용하는 것이다.
💡 함수 객체에는 함수와 관련된 모든 정보를 갖고 있다
name
— 함수명
length
— 매개변수의 개수
prototype
— 생성자의 프로토타입 객체
[[Environment]]
— 함수 객체가 생성된 렉시컬 환경에 대한 참조
그 외…
예시를 통해 알아보자
function outer() {
let x = 10;
function inner() {
console.log(x);
}
return inner;
}
const innerFunc = outer();
innerFunc(); // 10
outer함수가 외부에서 호출
outer함수 평가 단계
outer 함수의 렉시컬 환경에는 x 변수, inner함수가 포함되어 있다.
inner의 함수 객체를 Heap메모리에 생성하고
생성된 함수 객체의 [[Enviroment]] 슬롯에 inner 함수가 선언됐을 때의 렉시컬 스코프 (=outer 컨텍스트의 환경레코드) 를 바인딩한다.
outer함수의 환경레코드의 키로 inner라는 함수 이름을,
값으로는 inner함수 객체의 메모리 참조값
을 저장하는 것이다.
outer함수가 실행되면서 x=10 이라는 ‘값’을 outer컨텍스트의 환경 레코드에 갱신
이후 outer 함수가 inner 함수를 return
outer 함수의 실행 컨텍스트는 콜 스택에서 pop
inner함수 호출
inner함수의 실행 컨텍스트가 생성
이때 inner함수 컨텍스트의
외부 환경 참조
슬롯에다가 기존 inner 함수 객체의 [[Enviroment]]슬롯에 바인딩해 두었던 외부 함수의 환경 레코드(outer의 환경레코드)를 바인딩 한다.
❗❗이게 바로 클로저의 핵심 동작 원리가 된다
결과적으로, 내부 함수가 선언된 순간의 렉시컬 스코프(= outer 함수의 렉시컬 환경)이 inner함수 객체의 외부 환경 참조슬롯에 저장되는 것이다.
inner함수 선언당시의 함수 객체와 inner함수가 실행될 때의 실행 컨텍스트
이러한 원리로 인해 이후 외부 함수 outer의 실행컨택스트가 pop돼서 없어져도,
여전히 내부 함수의 외부 환경 참조 슬롯에서
Heap에 존재하는 outer의 환경 레코드를 참조하고 있는 것이다
이게 바로 닫힌 상태( Closure )인 클로저이다.
외부 스코프의 환경 레코드를 (그 어떤 실행 컨텍스트도 접근하지 못하며) 내부 함수만이 참조할수 있는 나만의 식별자 공간을 만드는 것이다.
이렇듯, 클로저는 생명주기가 종료된 외부함수의 변수를 계속해서
참조할 수 있게 된다.
let value = 10; // 전역 변수
// 클로저 함수를 반환하는 외부 함수
const outer = () => {
let value = 1;
// 내부 함수 선언. 외부 함수의 변수를 참조
const inner = () => {
console.log(value);
};
return inner;
// return문을 통해 outer함수는 실행컨텍스트에서 pop되며
// 생명주기가 종료됨**
};
const test = outer();
// 생명 주기가 종료된 outer함수의 변수를 여전히 참조
test(); // 1
자바스크립트에는 가비지 컬렉터(GC)라는 것이 존재한다.
이미 제 역할을 수행하고 더이상 필요없어진 데이터들을 계속 메모리에
냅둔다면 결국 메모리가 부족해질 수 밖에 없다.
그러므로 가비지컬렉터는 변수 또는 데이터가 더이상 필요하지 않을 때
그것들을 버려주는 역할을 수행을 한다.
그렇지만 Javascript는
JS에서 가비지 컬렉터는 ‘도달 가능성’을 기준으로 수행한다.
도달 가능한 값은 메모리에서 삭제하지 않는 것이다.
그렇다면 도달 가능하다는 것은 어느 것들을 의미할까?
이건 동시에 지역 스코프를 떠난 후 해당 스코프의 변수가 외부 스코프에서
참조 되지 않으면
가비지 콜렉션의 대상이 된다는 것이다.
그렇다면 도달 가능한 조건의 마지막 조건이었던
중첩 함수의 체인에 있는 함수에서 "사용되고 있는" 변수, 매개변수
이 부분이 바로 클로저의 원리이다.
outer 함수의 실행이 끝났지만
앞서 설명했던 "기존 inner 함수 객체의 [[Enviroment]]슬롯에 바인딩해 두었던 외부 함수의 환경 레코드(outer의 환경레코드)를 바인딩" 하는 과정 덕분에 아직 outer 함수 안에 있는 변수들을 참조하고 있다.
그러므로 가비지 컬렉션의 대상에서 벗어나게 된다.
콜스택 (stack에 저장) 에서 어떤 함수의 실행컨텍스트가 제거되었다고해서
해당 함수 실행컨텍스트에 바인딩 된 렉시컬 환경(Heap에 존재) 까지 소멸하는 것은 아니기 때문이다.
외부 함수의 실행 컨텍스트가 폐기되어도, 외부 함수의 (환경레코드, 외부 환경 참조)를 보존하기 위해, 실행 컨텍스트 내부에 포함시키지 않고 따로 Heap에 분리시킨 것이다.
동시에, 다른 실행 컨텍스트에서도 해당 (환경레코드, 외부 환경 참조)을 참조 할 수도 있게 된다.
이러한 구조의 언어를 heap 기반 언어
라 하며, 환경 레코드를 실행컨텍스트 내부에 포함하는 언어를 stack 기반 언어
라 한다