클로저는 JavaScript 특정 개념이 아닙니다. 함수를 일급 객체로 취급하는 함수형 프로그래밍 언어(Haskell, Lisp, Scala 등)에서 사용되는 중요한 특성입니다.
MDN에서는 클로저에 대해 다음과 같이 정의하고 있다.
A closure is the combination of a function and the lexical environment within which that function was declared
(클로저는 함수와 함수가 선언된 렉시컬 환경의 조합입니다.)
클로저에 대해 자세히 알아보기 전에 위의 정의에서 먼저 이해해야 하는 중요한 키워드인 ‘함수가 선언된 렉시컬 환경’에 대해 먼저 알아보겠습니다.
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 함수 내부에서 호출하면 outerFunc 함수의 변수에 액세스할 수 없습니다.
이러한 현상이 발생하는 이유는 JavaScript가 렉시컬 스코프를 따르는 프로그래밍 언어이기 때문입니다.
24.1 렉시컬 스코프
JavaScript 엔진은 함수를 어디에서 호출했는지가 아니라 함수를 어디에서 정의했는지에 따라 부모 범위를 결정합니다. 즉, 렉시컬 환경의 “외부 렉시컬 환경에 대한 참조”에 저장된 참조 값, 즉 상위 범위에 대한 참조는 함수 정의가 평가될 때 함수가 정의된 환경(위치)에 의해 결정됩니다. . 이것을 렉시컬 스코프(정적 스코프)라고 한다.
24.2 함수 객체의 내부 슬롯 ((Environment))
함수가 정의된 환경과 호출되는 환경은 다를 수 있습니다. 그러나 렉시컬 스코프를 가능하게 하려면 함수가 정의된 환경, 즉 상위 스코프(함수 정의가 위치하는 스코프가 바로 상위 스코프)를 기억해야 합니다.
이를 위해 함수는 자신의 내부 슬롯 (Envorinment)에 자신이 정의 된 환경, 즉 상위 범위 참조를 저장합니다.
따라서 함수 객체의 내부 슬록((Environemnt))에 저장된 현재 실행 중인 실행 컨텍스트의 렉시컬 환경에 대한 참조는 바로 상위 범위입니다. 또, 자신이 불려 갔을 때에 생성되는 함수 렉시컬 환경의 「외부 렉시컬 환경에의 참조」에 포함되는 참조치입니다. 함수 객체는 내부 슬롯 ((Environment))에 저장된 렉시컬 환경에 대한 참조, 즉 부모 범위를 자신이 존재하는 한 저장합니다.
24.3 클로저 및 렉시커 환경
const x = 1;
// 1.
function outer() {
const x = 10;
const inner = function () { console.log(x); }; // 2.
return inner
}
const innerFunc = outer(); // 3.
innerFunc(); // 4. 10
위의 예를 보면, outer 함수가 3번으로 호출이 되면 inner 함수를 돌려주고, 생명주기를 마친다. 즉, outer 함수의 실행 컨텍스트는 실행 컨텍스트 스택에서 삭제되고, 그 안에 저장된 변수 x와 x에 할당된 10이라는 값도 생명주기를 마친다. 2번에서는 inner 함수가 outer 함수의 로컬 변수인 x에 액세스하는 방법이 없고, 글로벌 변수인 x의 값 1이 출력이 되지만, 4번에서는 10이라는 값이 출력됩니다. 했다.
이렇게 외부 함수보다 중첩 함수가 오래 유지되면, 중첩 함수는 이미 수명주기가 종료된 외부 함수의 변수를 참조할 수 있습니다.이러한 중첩 함수를 닫습니다.라고 부른다.
위의 예에서는 클로저의 정의를 이해할 수 있습니다. 그러나 10이라는 값이 출력되는 것은 아직 이해할 수 없다. 10이라는 값이 출력되는 이유를 살펴 보겠습니다.
1번. outer 함수를 평가하여 함수 객체를 만들 때
- 현재 실행 중인 실행 컨텍스트의 렉시컬 환경, 즉 글로벌 렉시컬 환경을 outer 함수 객체의 ((Environment)) 내부 슬롯에 상위 범위로 저장합니다. outer 함수를 호출하면 outer 함수의 렉시컬 환경이 만들어지고 이전에 outer 함수 객체의 ((Environment)) 내부 슬롯에 저장된 글로벌 렉시컬 환경을 outer 함수의 렉시컬 환경에 대한 “외부 렉시컬 환경에 대한 참조” 에 할당합니다. 한다.
2번. 중첩 된 함수 내부가 평가되면 (여기서 내부 함수는 함수 표현식으로 정의되므로 런타임에 평가됩니다.)
- 이때, 중첩 함수 inner 는 자신의 (( Environment )) 내부 슬롯에 현재 실행중의 실행 문맥의 렉시컬 환경, 즉 outer 함수의 렉시컬 환경의 상위 스코프로서 격납한다.
3번. outer 함수의 실행이 끝나면 inner 함수를 반환하고 outer 함수의 수명주기를 종료합니다.
- outer 함수의 실행 컨텍스트는 실행 컨텍스트 스택에서 제거됩니다.이때 outer 함수의 실행 컨텍스트는 실행 컨텍스트 스택에서 제거되지만 outer 함수의 렉시컬 환경까지 파기되는 것은 아닙니다.
- outer 함수의 렉시컬 환경은 inner 함수의 (( Environment )) 내부 슬롯에 의해 참조되고 있어 inner 함수는 글로벌 변수 innerFunc 에 의해 참조되고 있으므로 가비지 콜렉션의 대상이 되지 않기 때문입니다.
4번. outer 함수가 반환한 inner 함수를 호출할 때
- inner 함수의 실행 컨텍스트가 생성되어 실행 컨텍스트 스택으로 푸시됩니다. 그리고, 렉시컬 환경의 외부 렉시컬 환경에의 참조에는, inner 함수 오브젝트의 ((Environment)) 내부 슬롯에 격납되어 있는 참조치가 할당됩니다.
중첩 함수 inner는 외부 함수 outer보다 오래 살아남았습니다. 이때, 외부 함수보다 오래 살아남은 중첩 함수는 외부 함수가 생존하는지(실행 문맥이 생존할지 여부에 관계없이) 자신이 정의된 위치에 의해 결정된 상위 범위를 저장한다. 이와 같이, 네스트 함수 이너 내부에서는 상위 스코프를 참조할 수 있기 때문에, 상위 스코프의 식별자를 참조할 수 있고, 식별자의 값을 변경할 수도 있다.
JavaScript의 모든 함수는 부모 범위를 기억하므로 이론적으로 모든 함수는 닫힙니다. 그러나 모든 함수를 일반적으로 클로저라고 부르지는 않습니다. 예를 들어, 다음과 같은 상황의 중첩 함수는 클로저가 아닌 것으로 간주할 수 있습니다.
1. 중첩 함수가 외부 함수보다 오래 살아남지만 부모 범위의 식별자를 참조하지 않는 경우
function foo() {
const x = 1;
const y = 2;
function bar() {
const z = 3;
console.log(z);
}
return bar;
}
const bar = foo();
bar();
2. 중첩 함수가 부모 범위의 음식을 참조하지만 외부 함수가 중첩 함수를 반환하지 않으므로 중첩 함수의 수명주기가 외부 함수보다 짧습니다.
function foo() {
const x = 1;
function bar() {
console.log(x);
}
bar();
}
foo();
그러므로 클로저는 일반적으로 중첩 함수가 부모 범위의 식별자를 참조하고 중첩 함수가 외부 함수보다 오래 유지되는 경우에만 제한합니다.
24.4 클로저 활용
클로저는 상태를 안전하게 변경하고 유지하는 데 사용됩니다. 즉, 상태가 의도하지 않게 변경되지 않도록 상태를 안전하게 숨기고 특정 함수에서만 상태 변경을 허용하는 데 사용됩니다.
예를 들어 함수가 호출될 때마다 호출된 횟수를 누적하여 출력하는 카운터를 만듭니다.
let num = 0;
const increase = function () {
return ++num;
}
console.log(increase()); // 1
console.log(increase()); // 2
console.log(increase()); // 3
위의 코드는 잘 작동하지만 오류를 일으킬 수 있는 나쁜 코드입니다. 위의 예가 올바르게 작동하려면 두 가지 전제 조건을 준수해야 합니다.
- 카운트 상태(num 변수의 값)는 increase 함수가 호출될 때까지 변경되지 않고 유지되어야 한다.
- 이를 위해서는 카운트 상태(num 변수의 값)는 increase 함수만 변경할 수 있어야 합니다.
그러나 카운트 상태는 전역 변수를 통해 관리되므로 언제든지 누구나 액세스하고 변경할 수 있습니다. 따라서, 카운트 상태를 안전하게 변경하고 유지하기 위해서는, increase 함수만이 num 변수를 참조 및 변경할 수 있도록 하는 것이 바람직하다. 이렇게 하려면 전역 변수 num을 increase 함수의 로컬 변수로 바꾸고 의도하지 않은 상태 변수를 피하십시오.
const increase = function() {
let num = 0;
return ++num;
}
console.log(increase()); // 1
console.log(increase()); // 1
console.log(increase()); // 1
num 변수의 상태는 increase 함수만 변경 가능합니다. 그러나 increase 함수가 호출될 때마다 로컬 변수 num이 재선언되고 0으로 초기화되므로 출력 결과는 항상 1입니다. 이제 이전 상태를 유지하기 위해 클로저를 사용해 봅시다.
const increase = (function() {
let num = 0;
return function() {
return ++num;
};
}());
console.log(increase()); // 1
console.log(increase()); // 2
console.log(increase()); // 3
위의 코드가 실행되면 즉시 실행 함수가 호출되고 즉시 실행 함수가 반환하는 함수가 increase 변수에 할당됩니다. increase 변수에 할당된 함수는 즉시 실행 함수의 렉시컬 환경을 기억하는 클로저입니다.
즉석 실행 함수는 호출된 후에 버려지지만, 즉시 실행 함수에 의해 리턴된 클로저는 increase 변수에 지정되어 호출됩니다. 클로저는 자신의 상위 스코프인 즉시 실행 함수의 렉시컬 환경을 기억하기 때문에, 자유 변수 num을 참조하여 변경할 수 있다.
이와 같이 클로저를 사용하면 num 변수가 호출될 때마다 초기화되지 않고 외부에서 직접 액세스할 수 없는 숨겨진 private 변수이므로 의도하지 않은 변경에 대해 걱정할 필요가 없는 코드가 된다.
24.5 캡슐화와 정보 은폐
캡슐화: 오브젝트의 상태를 나타내는 프로퍼티와 프로퍼티를 참조해 조작할 수 있는 동작인 메소드를 하나로 정리하는 것.
정보 숨김: 개체의 특정 속성과 메서드를 숨기기 위해 캡슐화를 사용합니다.
정보 은닉은 외부에 공개할 필요가 없는 구현의 일부를 외부에 공개되지 않게 숨겨, 부적절한 액세스로부터 오브젝트의 상태가 변화하는 것을 막아, 정보를 보호해, 오브젝트간의 상호 의존 성, 즉 결합도를 낮추는 효과가 있다. 있다.
JavaScript는 public, private, protected 등의 액세스 제한을 제공하지 않습니다. JavaScript 객체의 모든 속성과 메서드는 기본적으로 공용입니다.
const Person = (function () {
let _age = 0
// 생성자 함수
function Person(name, age) {
this.name = name
_age = age
}
// 프로토타입 메서드
Person.prototype.sayHi = function () {
console.log(`Hi! My name is ${this.name}. I am ${_age}`)
}
// 생성자 함수를 반환
return Person
})()
const me = new Person("Seo", 20)
me.sayHi()
console.log(me.name)
console.log(me._age)
const you = new Person("Daewon", 30)
you.sayHi()
console.log(you.name)
console.log(you.age)
위의 패턴을 사용하면 액세스 제한을 제공하지 않는 JavaScript에서도 정보를 숨길 수 있습니다. 즉석 실행 함수에 반하는 Person 생성자 함수와 Person 생성자 함수의 인스턴스가 상속하고 호출하는 Person.prototype.sayHi 메서드는 즉시 실행 함수가 종료된 후에 호출됩니다. 그러나 Person 생성자 함수와 sayHi 메서드는 이미 종료되고 사라진 직후 실행 함수의 로컬 변수 _age를 참조할 수 있는 클로저입니다.
그러나 위의 코드도 문제가 있습니다. Person 생성자 함수가 여러 인스턴스를 작성해도 _age 변수의 상태는 유지되지 않습니다. 이것은 Person.prototype.sayHi 메서드가 한 번만 생성되는 클로저이기 때문에 발생하는 현상입니다. Perosn.prototype.sayHi 메소드는, 자신의 부모 스코프인 즉시 실행 함수의 실행 문맥의 렉시컬 환경의 참조를 ((Environment)) 에 격납해 기억한다. 따라서 Person 생성자 함수의 모든 인스턴스가 상속을 통해 호출할 수 있는 Person.prototype.sayHi 메서드의 부모 범위는 모든 인스턴스에서 호출될 때 동일한 부모 범위를 사용합니다. 그래서 _age 변수는 상태가 유지되지 않습니다.
이런 식으로 JavaScript는 정보 숨김을 완전히 지원하지 않습니다. 인스턴스 메소드를 사용하면 비공개를 모방할 수 있지만 프로토타입 메소드를 사용하면 이것도 불가능합니다. 그러나 2021년 1월 TC39 프로세서의 3단계에서는 클래스의 개인 필드를 정의할 수 있는 새로운 표준 사양이 제안되었습니다.
24.6 자주 발생하는 실수
var funcs = ()
for (var i = 0; i < 3; i++) { // i는 var키워드로 선언했기 때문에 전역 변수
funcs(i) = function () {
return i
}
}
for (var j = 0; j < funcs.length; j++) {
console.log(funcs(j)())
}
위의 예는 클로저를 사용할 때 자주 발생할 수 있는 실수를 보여주는 예입니다.
0, 1, 2의 순서로 출력될 것으로 기대했지만, 본래의 출력값은 3이다. 이 값이 나오는 가장 큰 이유는 for문의 변수 선언문에서 var 키워드로 선언한 변수가 전역 변수이기 때문에 발생하는 현상입니다. var 키워드 대신 let 키워드를 사용하면 원하는 결과 값을 얻을 수 있습니다.
클로저를 사용하여 위의 예제를 올바르게 작동하는 코드로 만듭니다.
var funcs = ();
for (var i = 0; i < 3; i++) {
funcs(i) = (function (id) {
return function () {
return id
}
})(i)
}
for (var j = 0; j < funcs.length; j++) {
console.log(funcs(j)())
}


