사실 클로져는 이제까지 쓴 블로그 내용 가운데 한번이라도 언급이 됬을 가능성이 높다. (그만큼 워낙 중요한 특성이니)
근데, 다른 사람에게 해당 내용을 설명하려는 상황이 있었는데 명확하게 용어로 설명하지 못하는 나를 바라보며 반성하고,
남에게 설명할 수 없다면 그것은 알지 못하는 것과 같다는 절치부심의 마음으로 재정리한다.
클로져의 정의는 MDN 상에서 아주 명확하게 말해주고 있다. ( 다만 해석상 핵심만 적어놓는다 )
A closure is the combination of a function and the lexical environment within which that function was declared
클로져란, 함수입니다. 이 함수는 자신이 정의된 외부 환경 (즉 상위 컨텍스트와 바인딩된 렉시컬 환경) 과 연결되어 있습니다.
사실 실행 컨텍스트의 개념을 이해한다면 클로져가 무슨 의미인지 아주 이해하기 쉽다.
실행 컨텍스트란 자바스크립트 엔진이 식별자를 스코프 단위로 관리함과 동시에 실행 순서를 조정하기 위한 자바스크립트 내부 실행 메커니즘이다
말로만 설명하면 어려우니 간단하게 일반함수를 기반으로 실행 컨텍스트를 설명하자면 (전역 실행 컨텍스트는 설명에서 제외)
function ctx (arg) {
let name = "hi";
console.log(name);
}
ctx();
위 코드는 전역 실행 컨텍스트 내부에 정의된 함수 ctx를 호출하는 상황을 보여주고 있다.
함수 선언문 "ctx" 는 컴파일 시점에서 암묵적으로 함수 이름을 가진 식별자를 생성한 뒤, 몸체에 해당하는 내용이 평가가 완료되어 만들어지는 "함수 객체" 를 할당한다.
이때, 함수 객체에서 중요한 내부 구조는 다음과 같다.
여기서 클로져에 의미있는 부분이 바로 6번인 [[scope]] 슬롯이다.
아까 다시한번 MDN에서 말했던 정의를 살펴보자
클로져란, 함수입니다. 이 함수는 자신이 정의된 외부 환경 (즉 상위 컨텍스트와 바인딩된 렉시컬 환경) 과 연결되어 있습니다.
함수는 자바스크립트 엔진에 의해서 컴파일 시점에 평가가 되어 함수 객체로 변환되면서 자신이 정의되어 있는 환경인 상위 실행컨텍스트의 렉시컬 환경을 기억한다.
단 여기서 주의할 점이 있다.
위에서도 언급했지만, 해당 스코프 슬롯은 첫 함수객체의 평가 시점에서 업데이트가 되지 않으며, 노출되지 않은 내부 슬롯 [[Environment]] 라는 장소에 스코프 체인으로 활용되야 하는 상위 컨텍스트들의 렉시컬 환경들이 배열형태로 등록된다고 한다.
예를 들어,
위의 내용은 함수 parent 내부에서 함수 child가 리턴이 되고 있고, 이 리턴되는 객체값을 console.dir로 확인하는 내용을 담고 있다.
만약 parent 함수가 호출될 때 return문의 왼편에서 child가 평가되며 함수객체로 전환되며 리턴될 경우, 해당 시점에서 이미 scopes가 업데이트되었다면 부모 parent의 실행컨텍스트와 연결되어 있는 렉시컬 환경이 스코프슬롯의 배열에 들어가있어야 정상인 것처럼 보인다. 하지만
업데이트가 초기값인 0번째 인덱스, global 스코프 이후로는 되어있지 않음을 확인할 수 있다. 하지만,
함수가 호출되면 자동으로 실행 컨텍스트가 만들어지고, 1차적으로 식별자 정보만 등록된 렉시컬 환경과 결합된 뒤 콜스텍에 들어간다.
그리고 나서 런타임이 되면 해당 소스코드 내용에서 할당문이나, 함수 호출문과 같은 실행 문들을 실행하여 이 렉시컬 환경을 업데이트한다.
이때, 렉시컬 환경에는 크게 3가지로 분류할 수 있는데
1. environment record : 환경 변수, 위에서 이야기한 식별자인 키와 값에 해당하는 프로퍼티 쌍이 모여있는 장소를 의미한다.
2. this : 일반함수로 호출되었기에 window가 할당된다.
3. outer lexical environment reference : 스코프 체인에 해당되는 부분이다. 자바스크립트 엔진은 실행단계에서 필요한 식별자를 해당 스코프 내에서 찾지 못하면 이 장소를 참조하며 스코프 체인을 통해 식별자를 확인한다.
child가 return문의 왼쪽에 피연산자로 존재하면서 함수 객체로 평가가 되는 순간, [[Scopes]]가 아닌 내부 슬롯 [[Environment]] 라는 장소에는 실행 컨텍스트 스텍들이 가지고 있는 렉시컬 환경값들이 리스트 형태로 저장되었다.
만약, child가 호출이 되는 순간이 온다면 역시 마찬가지로 지역 실행 컨텍스트를 형성하고 렉시컬 환경을 조성하여 연결되는데, 그 순간 3번에 해당하는 outer lexical environmet reference부분에 들어가는 것이 바로 [[Environment]] 슬롯에 저장되어 있던 상위 실행 컨텍스트들의 렉시컬 환경들 리스트가 되는 것이다.
자바스크립트 엔진은 만약 child의 body에 있는 소스코드들을 실행하게 된다면 필요한 식별자 탐색을 해당 지역 스코프에서 식별자를 못찾으면 스코프 체인을 참조해가면서 행하게 된다.
그러면 여기서 의문, 저 scopes는 도대체 뭐란말인가
나도 이 글을 쓸 당시까지도 정체를 몰랐다가, 최근에 알게 되었다.
저 개발자가 확인 가능한 scopes 슬롯의 정체는 바로, 해당 코드가 돌아가게 되는 파일을 기준으로 설정되는 배열로 확인되었다.
그것에 대한 확신이 들었던 것이, 지금 위 예시의 내용은 chrome에서 개발자 탭을 열고 console 탭에서 작성하여보았던 것이다.
하지만 동일한 내용을 리엑트에서 작성하면 어떻게 될까?
보이는 것처럼 [[scopes]] 슬롯 내부에 뭔가가 추가되어 있는 것을 알 수 있다.
현재 저 코드가 돌아가는 위치는 index.js이다.
그래서 스코프의 내용을 보면 첫째로 index.js의 모듈스코프가 정의되어 있다.
React가 돌아갈 때, index.js는 webpack의 모듈 스코프 내부에서 정의되는 것으로 보이고,
webpack은 가장 최상단인 전역 실행컨텍스트에서 정의되어 실행되는 것으로 확인된다.
따라서 scopes에 저렇게 업데이트되는 것으로 보인다.
정리하자면
클로져는 함수이다. 이 함수는 컴파일에 평가되는 시점에서 자신이 정의된 실행 컨텍스트의 렉시컬 환경을 [[Environment]] 내부슬롯에 등록하였다가 호출이 되어 자신의 지역 실행컨텍스트를 만들고 렉시컬 환경을 만들면서 해당 내부슬롯의 내용을 스코프 체인형태로 등록해 사용한다.
위에 내용이 상당히 길어졌는데, 일반적으로 모든 함수는 자신의 상위 실행 컨텍스트의 렉시컬 환경을 기억하지만 모든 함수를 클로져라고 부르지 않는다.
클로져라는 명칭이 붙기 위한 조건은 아래와 같다
// a. 클로져는 어떤 함수에서부터 리턴되어야 한다
// 리턴되지 않고 함수 내부에서 호출이 완료되어 콜스텍에서 빠져버리는 중첩함수는 클로져라고 부르지 않는다.
function parent (){
function child(){};
return child; // 이렇게 되면 클로져이지만
child(); // 이렇게 되었으면 클로져가 아니다
}
// b. 클로져는 외부 실행 컨텍스트의 식별자를 참조하고 있어야 한다.
// 만약 외부 실행 컨텍스트의 식별자를 참조하지 않는 함수라면 이것은 클로져라고 부르지 않는다.
function parent (){
const x = 1;
function child(){
return x; // 이렇게 되면 클로져라고 할 수 있다
return "hello"
// 이렇게 되면 상위 실행 컨텍스트의 렉시컬 환경의 환경레코드 내부에 있는 값중 아무것도 참조하고 있지 않기 때문에
//클로져라고 부르지 않는다
}
}
참고로 예전에는 자바스크립트의 클로져가 모든 상위 실행 컨텍스트의 렉시컬 환경이 담긴 레퍼런스 주소 자체를 참조해버려서 불필요한 값들이 계속 유지되어 메모리적으로 손해를 보고 있었지만
최근에는 최적화가 아주 잘되어서 위에와 같이 상위 실행 컨텍스트의 렉시컬 환경에 환경레코드 내부에 있는 값중 참조하는 값만을 남기고 나머지는 사용하지 않는다고 하니, 적극 활용해야 한다.