클로저는 내부함수가 외부함수의 맥락(context)에 접근할 수 있는 것을 가르킨다.
자바스크립트가 어떻게 변수의 유효범위를 지정하는지(Lexical scoping)를 먼저 이해하자.
자바스크립트는 함수가 선언된 시점에서의 유효범위를 갖는다. 이러한 유효범위의 방식을 정적 유효범위(static scoping), 혹은 렉시컬(lexical scoping)이라고 한다.
var i = 5;
function a(){
var i = 10;
b();
}
function b(){
document.write(i);
}
a();
변수 i
는 전역변수이다.
함수 a
는 변수 i
를 지역변수로 가지고 있으며 다시 함수 b
를 호출한다.
함수 b
는 변수 i
를 출력한다.
1) 함수 a
를 호출한다. → 함수 내부 i
값=10 이되며, 그 후 함수 b
를 출력한다.
2 ) i
는 먼저 함수 b
내부에 i
지역변수를 찾는다.
3 ) 지역변수가 없다면 전역 변수를 찾는다.
이때, b
를 호출하고 있는 함수 a
의 지역변수 VS 함수b
가 현재 정의된 시점에서의 전역변수 중 어느것을 따라갈까?
→ 답은 i = 5 이다.
함수 b
가 선언된 시점에서 i
의 전역변수가 사용된다. 함수b
가 호출된 시점은 영향을 주지 못한다.
즉 '사용될 때' 가 아닌 '정의될 때' 사용된다. 이것을 정적 유효범위, 렉시컬 스코핑 이라고 한다.
함수가 호출된 위치는 스코프 결정에 아무런 의미를 주지 않는다.
function outter(){
function inner(){
const title = 'something';
alert(title);
}
inner();
}
funtion inner(){...}
를 내부 함수라고 한다.✍️ 예제를 통해 클로저의 특징에 대해서 알아보자.
내부함수에서 외부함수의 지역변수에 접근할 수 있다.
function outter(){
const title = 'Hello World!';
function inner(){
alert(title);
}
inner();
}
outter();
클로저는 내부함수와 밀접한 관계를 가지고 있다.
함수 inner
가 함수 outter
의 내부에 선언된 내부함수이므로 함수 inner
는 자신이 속한 렉시컬 스코프(전역, 함수 outter
, 자신의 스코프)를 참조할 수 있다.
즉, 내부함수inner
에서 외부함수outter
의 지역변수title
에 접근할 수 있다. 이러한 것을 클로저 라고한다.
외부함수가 실행이 종료되어 소멸된 이후에도, 내부함수가 외부함수의 변수에 접근 할 수 있다.
// ex.1)
function outter(){
const title = 'Hello World!';
return function(){
alert(title);
}
}
inner = outter();
inner();
흔히 함수 내에서 함수를 정의하고 사용하면 클로저라고 한다. 하지만 대개는 정의한 함수를 리턴하고 사용은 바깥에서 하게된다.
위에서 정의한 otter
은 함수를 반환하고, 반환된 함수는 otter
내부에서 선언된 변수 title
을 참조하고 있다.
참조된 변수는 함수 실행이 끝났다고 해서 사라지지 않았고, 여전히 값을 반환하고 있다.
즉, 내부함수가 외부함수의 지역변수에 접근 할 수 있고, 외부함수는 외부함수의 지역변수를 사용하는 내부함수가 소멸될 때까지 소멸되지 않는 클로저의 특성을 볼 수 있다.
// ex.2)
const base = 'hello! ';
function sayHelloTo(name) {
const result = base + name;
return function(){
console.log(result);
}
}
const hello1 = sayHelloTo('kim');
const hello2 = sayHelloTo('lee');
hello1(); // hello! kim
hello2(); // hello! lee
sayHelloTo
는 함수를 반환하고, 반환된 함수는 sayHelloTo
내부에서 선언된 변수 result
를 참조하고 있다.result
가 동적으로 변화한 것 처럼 보이지만, 실제로는 result
라는 변수 자체가 여러 번 생성된 것이다. hello1()
, hello2()
는 서로 다른 환경을 가지고 있다.클로저는 객체의 메소드에서도 사용할 수 있으며, 클로저를 통해 Private한 속성을 사용할 수 있다.
// ex.1)
function movie(title){
return {
get_title : function(){
return title;
},
set_title : function(_title){
title = _title
}
}
}
const myMovie1 = movie('a');
const myMovie2 = movie('b');
console.log(myMovie1.get_title()); // a
console.log(myMovie2.get_title()); // b
myMovie1.set_title('aa');
myMovie2.set_title('bb');
console.log(myMovie1.get_title()); // aa 값이 변경되었다.
console.log(myMovie2.get_title()); // bb 값이 변경되었다.
클로저는 객체의 메소드에서도 사용할 수 있다.
메소드 get_title
과 set_title
은 외부 함수 movie
의 인자값으로 전달된 지역변수title
을 사용하고 있다.
동일한 외부함수 안에서 만들어진 내부함수나 메소드는 외부함수의 지역변수를 공유한다.
1) myMovie1
과 myMovie2
의 set_tile
에서 외부함수의 지역변수 title
을 변경했다.
2) 이후 get_title
의 값이 변경 되었다.
JavaScript는 기본적으로 Private한 속성을 지원하지 않지만, 클로저의 특성을 이용해서 Private한 속성을 사용할 수 있다.
1) movie
의 지역변수 title
은 2행에서 정의된 객체의 메소드에서만 접근 할 수 있는 값이다.
2) 이 말은 title
의 값을 읽고 수정 할 수 있는 것은 movie
메소드를 통해서 만들어진 객체 뿐이라는 의미다.
// ex.2) 클로저를 통한 은닉화 추가예제
function music(title) {
const _title = title;
return function(){
console.log('My favorite music is ' + _title);
}
}
const music1 = music('a');
const music2 = music('b');
music1(); // My favorite music is a
music2(); // My favorite music is b
_title
에 접근할 방법이 없다.클로저와 반복문
var arr = []
for(var i = 0; i < 3; i++){
arr[i] = function(){
return i; // i를 리턴한다.
}
}
for(var index in arr){
console.log(arr[index]()); // return i의 값으로 치환된다.
}
// 3
// 3
// 3
클로저를 사용할 때 자주 발생할 수 있는 실수에 관련한 예제이다. 예상한 0, 1, 2 출력이 되지 않는다.
이유는, 3행
에 정의된 함수의 외부 변수에 정의된 i
의 값이 아니므로, 즉 for문에서 사용한 변수 i
는 전역 변수이기 때문이다.
❓ 그렇다면, 어떻게 만들어야 할까?
1)3행
에 정의된 함수를 내부 함수로 하는 새로운 외부 함수를 정의한다.
2) 그 외부함수에 지역변수의 값을 준다.
3) 내부함수가 외부함수의 지역변수를 참조할 수 있도록 만든다.
▼ 수정된 예제를 참고하자.
var arr = []
for(var i = 0; i < 3; i++){
arr[i] = function(a) { // 2)
return function(){
return a; // 3)
}
}(i); // 1) 외부함수를 즉석으로 호출 -> 외부함수를 리턴 -> 내부함수를 리턴
}
for(var index in arr) {
console.log(arr[index]());
}
// 0
// 1
// 2
이전 3행
의 함수를 내부 함수로 하는 새로운 외부 함수를 정의한다.
1) 즉시실행 함수를 통해 i
값을 인자로 넣어준다.
2) a
에 인자값 i
가 들어온다. (a
는 일종의 지역변수와 같은 효과를 낸다.)
3) 들어온 인자값을 함수내의 지역변수처럼 사용할 수 있게 내부 함수를 만들어 줌으로써, 리턴 값으로 들어온 인자값을 가리킨다.
4) 리턴된 내부 함수는 arr[i]
에 담기게 된다.
📌 위 예제는 자바스크립트의 함수 레벨 스코프 특성으로 인해 for
루프의 초기문에서 사용된 변수의 스코프가 전역이 되기 때문에 발생하는 현상이다.
▼ ES6의 let
키워드를 사용하면 위와 같은 문제는 나타나지 않는다. 😀
const arr = [];
for (let i = 0; i < 3; i++) {
arr[i] = function () {
return i;
};
}
for(var index in arr) {
console.log(arr[index]());
}
// 0
// 1
// 2
✍️ 앞으로 var
대신 let
을 많이 사용한다고 하더라도 기존의 var
와 클로저에 대한 지식이 있어야, 앞으로의 문제 상황에서 유연하게 대처할 수 있을 것 같다고 생각했다.
클로저는 자신이 생성될 때의 환경(Lexical environment)을 기억해야 하므로 메모리 차원에서 손해를 볼 수 있지만, 자바스크립트를 더욱 강력하게 해주는 기능이기도 하다.
정보의 은닉
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
▶ 앞에서 살펴본 바와 같이 클로저의 특성을 이용해서 Private한 속성을 사용할 수 있다.
1) 생성자 함수 Counter
는 increase
, decrease
메소드를 갖는 인스턴스를 생성한다.
→ 이 메소드들은 자신이 생성됐을 때의 렉시컬 환경인 생성자 함수 Counter
의 스코프에 속한 변수 counter
를 기억하는 클로저이며 렉시컬 환경을 공유한다.
→ 생성자 함수가 생성한 객체의 메소드는 객체의 프로퍼티에만 접근할 수 있는 것이 아니며 자신이 기억하는 렉시컬 환경의 변수에도 접근할 수 있다.
2) 생성자 함수 Counter
의 변수 counter
는 this
에 바인딩된 프로퍼티가 아니라 변수이다.
→ 생성자 함수 Counter
내에서 선언된 변수 counter
는 생성자 함수 Counter
외부에서 접근할 수 없다.
3) 생성자 함수 Counter
가 생성한 인스턴스의 메소드인 increase
, decrease
는 클로저이기 때문에 자신이 생성됐을 때의 렉시컬 환경인 생성자 함수 Counter
의 변수 counter
에 접근할 수 있다.
📌 이러한 클로저의 특징을 사용해 클래스 기반 언어의 private
키워드를 흉내낼 수 있다.
상태유지
<!-- html ▼ -->
<button class="toggle">toggle</button>
<div class="box" style="width: 80px; height: 80px; background: blue;"></div>
// javaScript ▼
var box = document.querySelector('.box');
var toggleBtn = document.querySelector('.toggle');
var toggle = (function () {
var isShow = false;
// 1) 클로저를 반환
return function () {
box.style.display = isShow ? 'block' : 'none';
// 3) 상태 변경
isShow = !isShow;
};
})();
// 2) 이벤트 프로퍼티에 클로저를 할당
toggleBtn.onclick = toggle;
▶ toggle 버튼을 누르면 나타나거나 사라지는 box를 만든다.
1) 즉시 실행 함수를 반환하고 즉시 소멸한다.
→ 즉시실행함수가 반환한 함수는 자신이 생성됐을 때의 렉시컬 환경에 속한 변수 isShow를 기억하는 클로저다. 클로저가 기억하는 변수 isShow는 box 요소의 표시 상태를 나타낸다.
2) 클로저를 이벤트 핸들러로써 이벤트 프로퍼티에 할당한다.
→ 이벤트 프로퍼티에서 이벤트 핸들러인 클로저를 제거하지 않는 한 클로저가 기억하는 렉시컬 환경의 변수 isShow는 소멸하지 않는다. 즉, 현재 상태를 기억한다.
3) 버튼 클릭시 이벤트 핸들러인 클로저가 호출된다.
→ box 요소의 표시 상태를 나타내는 변수 isShow의 값이 변경된다. 변수 isShow는 클로저에 의해 참조되고 있기 때문에 유효하며 자신의 변경된 최신 상태를 게속해서 유지한다.
📌 클로저가 가장 유용하게 사용되는 상황은 현재 상태를 기억하고 변경된 최신 상태를 유지하는 것이다.
예제의 클로저 기능을 사용하지 않았더라면 전역변수를 사용해 할 것이다.
전역 변수의 사용 억제
<!-- html ▼ -->
<button id="inclease">+</button>
<p id="count">0</p>
// javaScript ▼
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();
};
▶ 버튼이 클릭될 때마다 클릭한 횟수가 누적되어 화면에 표시되는 카운터가 만들어진다. 클릭된 횟수가 유지해야할 상태이다.
1) 즉시실행함수가 호출된다.
→ 변수increase
에는 함수 function(){return ++counter;}
가 할당된다. 이 함수는 자신이 생성됐을 때의 렉시컬 환경을 기억하는 클로저이다.
2) 즉시실행함수는 호출된 이후 소멸한지만 반환한 함수는 변수 increase
에 할당되어, inclease
버튼을 클릭하면 이벤트 핸들러 내부에서 호출된다.
→ 이때 클로저인 이 함수는 자신이 선언됐을 때 렉시컬환경인 즉시실행함수의 스코프에 속한 지역변수 counter
를 기억한다.
3) 따라서 즉시실행함수의 변수 counter
에 접근할 수 있다.
→ 변수 counter
는 자신을 참조하는 함수가 소멸될 때까지 유지된다.
4) 즉시실행함수는 한번만 실행된다.
→ increase가 호출될 때마다 변수 counter가 재차 초기화될 일은 없다.
5) 변수 counter
는 private
변수이다.
→ 전역변수보다 안정적인 프로그래밍이 가능하다.
📌 변수의 값은 누군가에 의해 언제든지 변경될 수 있어 오류 발생의 원인이 될 수 있다. 상태 변경이나 가변(mutable) 데이터를 피하고 불변성(Immutability)을 지향하는 함수형 프로그래밍에서 이러한 오류를 방지하고, 프로그램의 안정성을 높이기 위해 클로저는 적극적으로 사용된다고 한다. 🤔
reference
coding everybody
hyunseob - closure
poiemaweb - closure
MDN-Closure
meetup
joheresig