클로저
클로저란?
클로저는 함수와 그 함수가 선언됐을 때의 렉시컬 환경(Lexical environment)과의 조합이다.
MDN 문서
위의 말로만 듣고는 이해하기 힘들다. 간단한 예제 코드를 살펴보고 다시 알아보도록 하자.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function outerFunc() {
var x = 10;
var innerFunc = function () {
console.log(x);
};
return innerFunc;
}
/**
* 함수 outerFunc를 호출하면 내부 함수 innerFunc가 반환된다.
* 그리고 함수 outerFunc의 실행 컨텍스트는 소멸한다.
*/
var inner = outerFunc();
inner(); // 10
함수 outerFunc는 내부함수 innerFunc를 반환하고 생을 마감했다.
즉, 함수 outerFunc는 실행된 이후 콜스택(실행 컨텍스트 스택)에서 제거되었으므로 함수 outerFunc의 변수 x 또한 더이상 유효하지 않게 되어 변수 x에 접근할 수 있는 방법은 달리 없어 보인다.
그러나 위 코드의 실행 결과는 변수 x의 값인 10이다. 이미 life-cycle이 종료되어 실행 컨텍스트 스택에서 제거된 함수 outerFunc의 지역변수 x가 다시 부활이라도 한 듯이 동작하고 있다.
이처럼 자신을 포함하고 있는 외부함수보다 내부함수가 더 오래 유지되는 경우, 외부 함수 밖에서 내부함수가 호출되더라도 외부함수의 지역 변수에 접근할 수 있는데 이러한 함수를 클로저(Closure)라고 부른다.
클로저는 반환된 내부함수가 자신이 선언됐을 때의 환경(Lexical environment)인 스코프를 기억하여 자신이 선언됐을 때의 환경(스코프) 밖에서 호출되어도 그 환경(스코프)에 접근할 수 있는 함수를 말한다.
이를 조금 더 간단히 말하면 클로저는 자신이 생성될 때의 환경(Lexical environment)을 기억하는 함수다라고 말할 수 있겠다.
클로저의 활용
클로저는 자신이 생성될 때의 환경(Lexical environment)을 기억해야 하므로 메모리 차원에서 손해를 볼 수 있다.
하지만 클로저는 자바스크립트의 강력한 기능으로 이를 적극적으로 사용해야 한다. 클로저가 유용하게 사용되는 상황에 대해 살펴보자.
1. 상태 유지
클로저가 가장 유용하게 사용되는 상황은 현재 상태를 기억하고 변경된 최신 상태를 유지하는 것이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<button class="toggle">toggle</button>
<div class="box" style="width: 100px; height: 100px; background: red;"></div>
var box = document.querySelector(".box");
var toggleBtn = document.querySelector(".toggle");
var toggle = (function () {
var isShow = false;
// ① 클로저를 반환
return function () {
box.style.display = isShow ? "block" : "none";
// ③ 상태 변경
isShow = !isShow;
};
})();
// ② 이벤트 프로퍼티에 클로저를 할당
toggleBtn.onclick = toggle;
toggle()은 즉시실행함수로 함수를 반환하고 즉시 소멸한다. 즉시실행함수가 반환한 함수는 자신이 생성됐을 때의 렉시컬 환경(Lexical environment)에 속한 변수 isShow를 기억하는 클로저다.
클로저를 toggleBtn의 이벤트 핸들러로서 이벤트 프로퍼티에 할당했다. 이벤트 프로퍼티에서 이벤트 핸들러인 클로저를 제거하지 않는 한 클로저가 기억하는 렉시컬 환경의 변수 isShow는 소멸하지 않는다. 다시 말해 현재 상태를 기억한다.
버튼을 클릭하면 이벤트 프로퍼티에 할당한 이벤트 핸들러인 클로저가 호출된다. 이때 .box 요소의 표시 상태를 나타내는 변수 isShow의 값이 변경된다. 변수 isShow는 클로저에 의해 참조되고 있기 때문에 유효하며 자신의 변경된 최신 상태를 게속해서 유지한다.
이처럼 클로저는 현재 상태(위 예제의 경우 .box 요소의 표시 상태를 나타내는 isShow 변수)를 기억하고 이 상태가 변경되어도 최신 상태를 유지해야 하는 상황에 매우 유용하다.
만약 자바스크립트에 클로저라는 기능이 없다면 상태를 유지하기 위해 전역 변수를 사용할 수 밖에 없다. 전역 변수는 언제든지 누구나 접근할 수 있고 변경할 수 있기 때문에 많은 부작용을 유발해 오류의 원인이 되므로 사용을 억제해야 한다.
2. 전역 변수의 사용 억제
카운터 예제 1) 전역 변수
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<button id="inclease">+</button>
<p id="count">0</p>
var incleaseBtn = document.getElementById("inclease");
var count = document.getElementById("count");
// 카운트 상태를 유지하기 위한 전역 변수
var counter = 0;
function increase() {
return ++counter;
}
incleaseBtn.onclick = function () {
count.innerHTML = increase();
};
위 코드는 잘 동작하지만 오류를 발생시킬 가능성을 내포하고 있는 좋지 않은 코드다.
increase 함수는 호출되기 직전에 전역변수 counter의 값이 반드시 0이여야 제대로 동작한다.
하지만 변수 counter는 전역 변수이기 때문에 언제든지 누구나 접근할 수 있고 변경할 수 있다. 이는 의도치 않게 값이 변경될 수 있다는 것을 의미한다.
변수 counter는 카운터를 관리하는 increase 함수가 관리하는 것이 바람직하다. 전역 변수 counter를 increase 함수의 지역 변수로 바꾸어 의도치 않은 상태 변경을 방지해보자.
카운터 예제 2) 지역 변수
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<button id="inclease">+</button>
<p id="count">0</p>
var incleaseBtn = document.getElementById("inclease");
var count = document.getElementById("count");
function increase() {
// 카운트 상태를 유지하기 위한 지역 변수
var counter = 0;
return ++counter;
}
incleaseBtn.onclick = function () {
count.innerHTML = increase();
};
전역변수를 지역변수로 변경하여 의도치 않은 상태 변경은 방지했다.
하지만 increase 함수가 호출될 때마다 지역변수 counter를 0으로 초기화하기 때문에 언제나 1이 표시된다. 다시 말해 변경된 이전 상태를 기억하지 못한다.
이전 상태를 기억하도록 클로저를 사용하여 이 문제를 해결해보자.
카운터 예제 3) 클로저 활용
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<button id="inclease">+</button>
<p id="count">0</p>
var incleaseBtn = document.getElementById("inclease");
var count = document.getElementById("count");
var increase = (function () {
// 카운트 상태를 유지하기 위한 자유 변수
var counter = 0;
// 클로저를 반환
return function () {
return ++counter;
};
})();
incleaseBtn.onclick = function () {
count.innerHTML = increase();
};
increase()는 즉시실행함수로 함수 function () { return ++counter; }를 반환하고 즉시 소멸한다. 즉시실행함수가 반환한 함수는 자신이 생성됐을 때의 렉시컬 환경(Lexical environment)에 속한 변수 counter를 기억하는 클로저다.
클로저는 변수 increase에 할당되어 inclease 버튼을 클릭하면 클릭 이벤트 핸들러 내부에서 호출된다. 이때 클로저는 자신이 선언됐을 때의 렉시컬 환경인 즉시실행함수의 스코프에 속한 지역변수 counter를 기억한다.
#counter 요소의 innerHTML을 클로저에서 return된 값(++counter)으로 변경해준다.
즉시실행함수는 한번만 실행되므로 increase가 호출될 때마다 변수 counter가 재차 초기화될 일은 없을 것이다.
변수 counter는 외부에서 직접 접근할 수 없는 private 변수이므로 전역 변수를 사용했을 때와 같이 의도되지 않은 변경을 걱정할 필요도 없기 때문이 보다 안정적인 프로그래밍이 가능하다.
3. 정보의 은닉
이번에는 생성자 함수 Counter를 생성하고 이를 통해 counter 객체를 만들어보자.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function Counter() {
// 카운트를 유지하기 위한 자유 변수
var counter = 0;
// 클로저
this.increase = function () {
return ++counter;
};
// 클로저
this.decrease = function () {
return --counter;
};
}
const counter = new Counter();
console.log(counter.increase()); // 1
console.log(counter.decrease()); // 0
생성자 함수 Counter는 increase, decrease 메소드를 갖는 인스턴스를 생성한다.
increase, decrease 메소드는 모두 자신이 생성됐을 때의 렉시컬 환경인 생성자 함수 Counter의 스코프에 속한 변수 counter를 기억하는 클로저이며 렉시컬 환경을 공유한다.
생성자 함수가 생성한 객체의 메소드는 객체의 프로퍼티에만 접근할 수 있는 것이 아니며 자신이 기억하는 렉시컬 환경의 변수(counter)에도 접근할 수 있다.
생성자 함수 Counter 내에서 선언된 변수 counter는 생성자 함수 Counter 외부에서 접근할 수 없다.
하지만 생성자 함수 Counter가 생성한 인스턴스의 메소드인 increase, decrease는 클로저이기 때문에 자신이 생성됐을 때의 렉시컬 환경인 생성자 함수 Counter의 변수 counter에 접근할 수 있다.
이러한 클로저의 특징을 사용해 클래스 기반 언어의 private 키워드를 흉내낼 수 있다.
클로저 유의해야할 점
1
2
3
4
5
6
7
function a() {
for (var i = 0; i < 5; i++) {
setTimeout(function () {
console.log(`${i}`);
}, i * 1000);
}
}
위의 console에서 0,1,2,3,4가 출력될 것이라고 생각하겠지만 5, 5, 5, 5, 5가 출력된다.
이는 var는 함수형 스코프이기 때문이다.
변수 i는 함수형 스코프를 가지기 때문에 함수 a()에 속해 있고 for문은 반복을 실행할 때마다 새로운 스코프를 가진다.
for문의 5개의 스코프는 함수 a()에 선언된 i 하나를 바라보기 때문에 모두 5가 출력된 것이다.
- 이때 i는 setTimeout()함수가 call stack에 들어와서 실행 할 때 이미 반복문이 끝난 상태이기 때문에 5를 가르키게 된다.
이를 해결하기 위해서는 let, 즉시 실행 함수를 활용할 수 있다.
- let
1
2
3
4
5
6
7
function a() {
for (let i = 0; i < 5; i++) {
setTimeout(function () {
console.log(`${i}`);
}, i * 1000);
}
}
let는 블록형 스코프로 for문 내에 새로운 scope를 가지기 때문에 반복을 실행 할 때마다 새로운 let이 선언이 된다.
setTimeout()함수가 call stack에 들어와서 실행 될 때 상위 스코프를 검색할 때 매 반복마다 선언 및 초기화된 i값을 참조하여 0 1 2 3 4값을 가지게 된다.
- 즉시 실행 함수
1
2
3
4
5
6
7
8
9
function a() {
for (let i = 0; i < 5; i++) {
function(j){
setTimeout(function(){
console.log(j)
}, i * 1000);
}(i);
}
}
for 반복문을 실행 할 때마다 i의 값이 실행되는 익명함수의 j로 전달이 된다.
그렇게 새로운 함수 5개가 생기고 각자의 함수 j는 for문의 i를 차례차례 갖게된다.(i = 0, 1, 2, 3, 4)
📑 참고자료