우당탕탕 개발일기
모던 자바스크립트 24. 클로저 본문
MDN에서는 클로저를 “함수와 그 함수가 선언된 렉시컬 환경과의 조합”이라고 정의하고 있다. “함수가 선언된 렉시컬 환경”이 클로저를 이해하기 위한 핵심 키워드이므로 먼저 알아봐야한다.
렉시컬 스코프
자바스크립트 엔진은 함수를 어디에 정의했는지에 따라 상위 스코프를 결정한다. 이를 렉시컬 스코프(정적 스코프)라고 한다.
실행 컨텍스트 파트에서 공부했듯 ‘스코프’란 실행 컨텍스트의 렉시컬 환경이다.
이 렉시컬 환경은 자신의 “외부 렉시컬 환경에 대한 참조”를 통해 상위 렉시컬 환경과 연결된다.
이 원리로 이루어지는 것이 바로 스코프 체인인 것.
따라서 함수의 상위 스코프를 결정한다는 것은 렉시컬 환경의 외부 렉시컬 환경에 대한 참조에 저장할 참조값을 결정한다는 것과 같다.
렉시컬 환경의 외부 렉시컬 환경에 대한 참조에 저장할 참조값이 바로 상위 렉시컬 환경에 대한 참조이며, 이것이 곧 상위 스코프이기 때문이다.
따라서 “외부 렉시컬 환경에 대한 참조”에 저장할 참조값, 즉 상위 스코프에 대한 참조는 함수 정의가 평가되는 시점에 함수가 정의된 환경(위치)에 의해 결정된다. 이것이 바로 렉시컬 스코프의 원리이다.
함수가 정의된 위치와 호출된 위치는 (당연하게도) 다를 수 있다.
따라서 렉시컬 스코프가 가능하려면 자신이 호출되는 위치와 상관없이 자신이 정의 된 환경, 즉 상위 스코프를 기억해야 한다.
상위스코프를 기억하기 위해 함수는 자신의 내부 슬롯 [[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 함수의 지역 변수인 x(10)도 생명 주기를 마감한다.
지역 변수 x가 유효하지 않은 것 같지만, 위 코드를 실행하면 outer 함수의 지역 변수인 x(10)가 출력된다.
이렇게 외부 함수보다 중첩 함수가 더 오래 유지되는 경우 중첩 함수는 이미 생명 주기가 종료한 외부 함수의 변수를 참조할 수 있다.
이러한 중첩 함수를 바로 클로저라고 한다.
outer 함수가 평가되어 함수 객체를 생성할 때(ⓐ) 현재 실행 중인 실행 컨텍스트의 렉시컬 환경, 즉 전역 렉시컬 환경을 outer 함수 객체의 내부 슬롯에 상위 스코프로서 저장한다.
outer 함수를 호출하면 outer 함수의 렉시컬 환경이 생성되고 outer 함수 객체의 내부 슬롯에 저장된 전역 렉시컬 환경을 outer 함수 렉시컬 환경의 “외부 렉시컬 환경에 대한 참조”에 할당한다.
그 이후, 중첩 함수 inner가 평가된다.(ⓑ)
중첩 함수 inner는 자신의 내부 슬롯에 현재 실행 중인 실행 컨텍스트의 렉시컬 환경, 즉 outer 함수의 렉시컬 환경을 상위 스코프로서 저장한다.
outer 함수의 실행이 종료되면 inner 함수를 반환하며 outer 함수의 생명 주기가 종료된다(ⓒ).
즉, outer 함수의 실행 컨텍스트가 실행 컨텍스트 스택에서 제거된다.
단, outer 함수의 실행 컨텍스트는 실행 컨텍스트 스택에서 제거되지만 outer 함수의 렉시컬 환경까지 사라지는 것은 아니다.
outer 함수의 렉시컬 환경은 중첩함수 inner 함수의 내부 슬롯에 의해 참조되고 있고
inner 함수는 전역 변수 innerFunc에 의해 참조되고 있으므로 가비지 컬렉션의 대상이 되지 않기 때문이다.
outer 함수가 반환한 inner함수를 호출(ⓓ)하면 inner 함수의 실행 컨텍스트가 생성되고 실행 컨텍스트 스택에 푸시된다.
그리고 렉시컬 환경의 외부 렉시컬 환경에 대한 참조에는 inner 함수 객체의 내부 슬롯에 저장되어 있는 참조값(outer의 렉시컬 환경)이 할당된다.
이렇게 중첩 함수 inner는 외부 함수 outer보다 더 오래 ‘생존’했다. 이를 클로저라고 하는 것이다.
클로저는 외부 함수의 생존 여부와 상관없이 자신이 정의된 위치에 의해 결정된 상위 스코프를 기억한다.
하지만 클로저는 단순히 오래 생존하는 것만 중요한 것이 아니다.
클로저가 되려면 외부 함수보다 오래 생존해야 하며, 그리고 상위 스코프의 식별자를 참조해야만 한다.
따라서 클로저는 중첩 함수가 상위 스코프의 식별자를 참조하고 있고 중첩 함수가 외부 함수보다 더 오래 유지되는 경우에 한정하는 것이 일반적이다. 이 때, 클로저에 의해 참조되는 상위 스코프의 변수를 자유변수라고 한다.
클로저의 활용
클로저는 상태를 안전하게 변경하고 유지하기 위해 사용한다. 즉, 상태가 의도치 않게 변경되지 않도록 상태를 안전하게 은닉하고 특정 함수에게만 상태 변경을 허용하기 위해 사용한다.
전역 변수로 둔다던지, 지역 변수로 두는 경우 제대로 동작하지 않거나 다른 사람에 의해 변수값이 변경 될 위험이 있다. 이런 상황을 방지하기 위해 클로저를 사용한다.
const increase = (function() {
let num = 0;
// 클로저
return function() {
return ++num;
}
}());
console.log(increase()); // 1
console.log(increase()); // 2
console.log(increase()); // 3
위 코드가 실행되면 즉시 실행 함수가 호출되고, 즉시 실행 함수가 반환한 함수(클로저)가 increase 변수에 할당된다.
increase에 할당 된 함수(클로저)는 자신이 정의된 위치에 의해 결정된 상위 스코프인 즉시 실행 함수의 렉시컬 환경을 기억하게 된다.
상위 스코프의 변수인 num이 자유 변수가 되는 것이고, 한 번만 실행되는 즉시 실행 함수가 호출 이후 소멸되더라도 즉시 실행 함수가 반환한 클로저는 increase에 할당되어 호출할 수 있다.
이처럼 클로저는 상태가 의도치 않게 변경되지 않도록 안전하게 은닉하고 특정 함수에게만 상태 변경을 허용하여 상태를 안전하게 변경하고 유지하기 위해 사용한다.
자주 발생하는 실수
var funcs = [];
for(var i = 0; i < 3; i++) {
funcs[i] = function() { return i; };
}
for(var j = 0; j < funcs.length; j++) {
console.log(funcs[j]()); // 3, 3, 3
}
위 코드에서는 var를 사용해 변수를 선언한다. var는 함수 레벨 스코프를 가지게 되어 전역 변수가 된다.
따라서 모든 함수는 동일한 ‘i’ 값을 참조하게 된다.
첫 번째 for문이 종료된 이후 i는 3이 된다.
따라서 배열 funcs에 저장된 모든 함수(i를 반환하는)는 i의 최종 값인 3을 반환하게 된다.
위를 고치는 방법은 아래 두 가지 방법이 있다.
1. 블록 스코프인 let을 사용하기
var funcs = [];
// let 사용
for(let i = 0; i < 3; i++) {
funcs[i] = function() { return i; };
}
for(let j = 0; j < funcs.length; j++) {
console.log(funcs[j]()); // 0, 1, 2
}
2. 즉시 실행 함수 사용
var funcs = [];
for (var i = 0; i < 3; i++) {
// 즉시실행함수
funcs[i] = (function(value) {
return function() { return value; };
})(i);
}
for (var j = 0; j < funcs.length; j++) {
console.log(funcs[j]()); // 0, 1, 2
}
'What I Learned > JavsScript' 카테고리의 다른 글
모던 자바스크립트 30. Date (0) | 2024.07.02 |
---|---|
모던 자바스크립트 26. ES6 함수의 추가 기능 (1) | 2024.06.17 |
모던 자바스크립트 22. this (0) | 2024.05.20 |
모던 자바스크립트 19. 프로토타입 (0) | 2024.05.13 |
모던 자바스크립트 16. 프로퍼티 어트리뷰트 (0) | 2024.05.02 |