목표
24. 클로저(Closure)
const x = 1;
function outer() {
const x = 10;
function inner() {
console.log(x);
}
inner();
}
outer();
- 위 코드의 결과는 무엇일까?
- undefined 또는 10 또는 1 셋 중에 하나가 출력될 것이다.
- 답은 10이 출력되는데 이는 중첩 함수인 inner 내부에서 외부 함수인 outer의 변수에 접근이 가능하기 때문이다.
- 이는 클로저(Closure) 와 관련이 있다.
- MDN에서 클로저를 다음과 같이 정의하고 있다.
- 클로저는 함수와 함수가 선언된 어휘적 환경의 조합이다. 클로저를 이해하려면 자바스크립트가 어떻게 변수의 유효범위를 지정하는지(Lexical scoping) 를 먼저 이해해야 한다.
24-1. 렉시컬 스코프
- 렉시컬 스코프는 함수를 어디서 정의했는지에 따라 상위 스코프를 결정한다는 것이다. 다른 말로는 정적 스코프 라고도 한다.
const x = 1;
function scope1() {
const x = 10;
scope2();
}
function scope2() {
console.log(x);
}
scope1();
scope2();
- 위 코드에서 scope1 함수와 scope2 함수는 모두 전역에서 정의된 전역 함수이다.
- 자바스크립트는 정적 스코프를 따르므로 scope1, scope2 함수 모두 상위 스코프는 전역 스코프이다.
- 따라서 결과는 1이 두 번 출력된다.
- 스코프의 실체는 실행 컨텍스트 의 렉시컬 환경이고 렉시컬 환경은 외부 렉시컬 환경에 대한 참조 컴포넌트를 통해 상위 렉시컬 환경이 연결된다. 이를 스코프 체인(scope chain) 이라고 한다.
- 따라서, "함수의 상위 스코프를 결정한다" 는 것은 "렉시컬 환경의 외부 렉시컬 환경에 대한 참조에 저장할 참조값을 결정한다" 는 것과 같다.
- 렉시컬 환경의 "외부 렉시컬 환경에 대한 참조"에 저장할 참조값 즉, 상위 스코프에 대한 참조는 함수 정의가 평가되는 시점에 함수가 정의된 위치(정적 스코프)에 의해 결정되고 이를 렉시컬 스코프 라고 한다.
24-2. 함수 객체의 내부 슬롯 [[Environment]]
- 함수는 [[Environment]]라는 내부 슬롯에 상위 스코프의 참조를 저장한다.
- 위 코드에서 scope1과 scope2는 함수를 정의한 스코프의 상위 스코프인 전역 렉시컬 환경을 [[Environment]]에 저장하여 기억한다.
24-3. 클로저
const x = 1;
function outer() {
const x = 10;
const inner = function () { console.log(x); };
return inner;
}
const func = outer();
func();
- 위 코드에서 outer 함수를 호출하면 중첩 함수 inner를 반환하고 생명 주기를 마감하면서 실행 컨텍스트 스택에서 제거된다.
- 하지만, 위 코드의 결과 값은 10으로 실행 컨텍스트에서 제거될 것으로 예상되는 outer 함수의 지역 변수 x의 값이다.
- 이처럼, 외부 함수보다 중첩 함수가 더 오래 유지되는 경우 중첩 함수는 이미 생명 주기가 종료된 외부 함수의 변수를 참조할 수 있다. 이 중첩 함수를 클로저 라고 부른다.
24-3-1. 클로저와 렉시컬 환경
- 위 코드에서 outer 함수를 호출하면 outer 함수의 렉시컬 환경이 생성되고 outer 함수 [[Environment]] 내부 슬롯에 저장된 전역 렉시컬 환경을 outer 함수 렉시컬 환경의 구성요소인 외부 렉시컬 환경에 대한 참조에 할당한다.
- 그리고 inner 함수를 호출하면 inner 함수 [[Environment]] 내부 슬롯에 현재 실행 중인 실행 컨텍스트의 렉시컬 환경, 즉 outer 함수의 렉시컬 환경을 상위 스코프로서 저장한다.
- outer 함수의 실행이 종료되면 inner 함수를 반환하고 outer 함수의 실행 컨텍스트가 실행 컨텍스트 스택에서 제거된다.
- 하지만, outer 함수의 실행 컨텍스트는 실행 컨텍스트 스택에서 제거되지만 렉시컬 환경까지 소멸하는 것이 아니다.
- 이는 outer 함수의 렉시컬 환경이 inner 함수의 [[Environment]] 내부 슬롯에 의해 참조되고 있기 때문에 가비지 컬렉션의 대상이 되지 않기 때문에 가능하다.
24-3-2. 예외
- 자바스크립트의 모든 함수는 상위 스코프를 기억하므로 이론적으로 모든 함수는 클로저이다.
- 하지만, 다음과 같은 경우는 클로저가 아니다.
function outer() {
const x = 1;
const y = 2;
function inner() {
const z = 3;
console.log(z);
}
return inner;
}
const func = outer();
func();
- 위 코드에서 중첩 함수 inner는 외부 함수 outer보다 더 오래 유지되지만 상위 스코프의 어떤 식별자도 참조하지 않는다.
- 대부분의 모던 브라우저는 상위 스코프의 어떤 식별자도 참조하지 않는 경우 최적화 작업을 통해 상위 스코프를 기억하지 않는다.
- 따라서, 가비지 컬렉터에 의해 없어지므로 inner 함수는 클로저라고 할 수 없다.
- 클로저는 중첩 함수가 상위 스코프의 식별자를 참조하고 있고 중첩 함수가 외부 함수보다 더 오래 유지되는 경우에 한정하는 것이 일반적이다.
- 대부분의 모던 자바스크립트 엔진은 최적화가 잘 되어 있어 클로저가 참조하고 있지 않는 식별자를 기억하지 않아 불필요한 메모리 낭비를 방지할 수 있다.
24-4. 클로저는 왜 사용할까?
- 클로저는 상태를 안전하게 변경하고 유지하기 위해 사용한다.
- 구체적으로 상태가 의도치 않게 변경되지 않도록 상태를 안전하게 은닉(information hiding)하고 특정 함수에게만 상태 변경을 허용하기 위해 사용한다.
let num = 0;
const increase = function() {
return ++num;
};
console.log(increase());
console.log(increase());
console.log(increase());
- 위 코드는 작동하는데에는 문제가 없지만 오류를 발생시킬 가능성이 높다.
- 카운트 상태 즉, num은 전역 변수이기 때문에 언제든지 누구나 접근할 수 있고 변경할 수 있다. 이는 의도치 않은 오류를 발생시킬 수 있다.
const increase = function () {
let num = 0;
return ++num;
};
console.log(increase());
console.log(increase());
console.log(increase());
- 위 코드는 카운트 상태를 안전하게 변경하고 유지하기 위한 전역 변수 num을 increase 함수의 지역 변수로 변경하여 의도치 않은 상태 변경은 방지했다.
- 하지만, increase 함수가 호출될 때마다 num은 0으로 초기화되기 때문에 이전 상태를 유지하지 못한다.
const increase = (function () {
let num = 0;
return function () {
return ++ num;
};
}());
console.log(increase());
console.log(increase());
console.log(increase());
- 위 코드가 실행되면 즉시 실행 함수 가 호출되는데 이 함수가 반환한 함수가 increase 변수에 할당된다.
- increase 변수에 할당된 함수는 자신이 정의된 위치에 의해 결정된 상위 스코프인 즉시 실행 함수의 렉시컬 환경을 기억하는 클로저다.
- 따라서, 즉시 실행 함수가 반환한 클로저는 자신의 렉시컬 환경인 즉시 실행 함수의 스코프에 속한 지역 변수 num을 기억하게 되고 언제 어디서 호출하든지 참조하고 변경할 수 있다.
- 클로저는 상태가 의도치 않게 변경되지 않도록 안전하게 은닉하고 특정 함수에게만 상태 변경을 허용하여 상태를 안전하게 변경하고 유지하기 위해 사용한다.
24-5. 캡슐화와 정보 은닉
- 캡슐화(encapsulation) 는 객체의 상태를 나타내는 프로퍼티와 프로퍼티를 참조하고 조작할 수 있는 동작인 메서드를 하나로 묶는 것을 말한다.
- 캡슐화는 특정 프로퍼티나 메서드를 감출 목적으로 사용하기도 하는데 이를 정보 은닉(information hiding) 이라고 한다.
- 정보 은닉은 외부에 공개할 필요가 없는 구현의 일부를 외부에 공개되지 않도록 감추어 적절치 못한 접근으로부터 객체의 상태가 변경되는 것을 방지해 정보를 보호하고, 결합도(객체 간의 상호 의존성)를 낮추는 효과가 있다.
- 대부분의 객체지향 프로그래밍 언어는 클래스를 구성하는 멤버(프로퍼티, 메서드)에 대하여 public, private, protected 같은 접근 제한자(access modifier)를 선언하여 공개 범위를 한정할 수 있다.
- 하지만, 자바스크립트는 접근 제한자를 제공하지 않기 때문에 모든 프로퍼티와 메서드는 기본적으로 외부에 공개되어 있다.
24-6. 자주 발생하는 실수
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]());
}
- 위 코드의 결과 값은 무엇일까?
- 1 2 3을 기대하지만 3 3 3이 출력된다.
- 왜냐하면 var 키워드로 선언한 i 변수는 함수 레벨 스코프이기 때문에 전역 변수이기 때문이다.
const funcs = [];
for (let i = 0; i < 3; i++) {
funcs[i] = function () { return i; };
}
for (let j = 0; j < funcs.length; j++) {
console.log(funcs[j]());
}
- 그렇다면 위 코드의 결과 값은 무엇일까?
- 기대했던 1 2 3이 출력된다.
- let 키워드로 선언한 i 변수는 블록 레벨 스코프이기 때문에 for 문의 코드 블록이 반복 실행 될 때마다 독립적인 렉시컬 환경을 생성하기 때문에 반복문 코드 블록 내부에 함수 정의가 있다면 식별자의 값을 유지한다.
- 하지만, 함수 정의가 없다면 아무도 참조하지 않기 때문에 가비지 컬렉션의 대상이 되어 참조할 수 없다.
24-6-1. 고차함수
- 위와 같은 실수를 줄이는 방법으로는 고차함수를 사용하는 방법도 있다.
const funcs = Array.from(new Array(3), (_, i) => () => i);
funcs.forEach(func => console.log(func()));
0
1
2