생소했던 개념인 클로저, 드디어 이해해서 매우 기쁘다 😆
클로저
클로저는 js의 고유한 개념이 아니라 함수를 일급 객체로 취급하는 함수형 프로그래밍 언어에서 사용되는 중요한 특성이다.
클로저는 함수와 함수가 선언되었을 때의 렉시컬 환경과의 조합이다.
렉시컬 스코프
function outerFunc() {
var x = 10;
var innerFunc = function () { console.log(x); };
innerFunc();
}
outerFunc(); // 10
함수 outerFunc
내에서 함수 innterFunc
가 선언되고 호출되었다. 내부함수인 innterFunc
는 자신을 포함하고 있는 외부함수 outerFunc
의 변수 x
에 접근이 가능하다.
스코프는 함수를 호출할 때가 아니라 함수를 어디에 선언하였는지에 따라 결정된다. 이를 렉스컬 스코핑이라고 하는데 위의 innterFunc
은 외부 함수의 내부에서 선언되었으므로 상위스코프는 함수 outerFunc
가 된다. 만약 innterFunc
가 전역에 선언되었다면 함수 innterFunc
의 상위 스코프는 전역 스코프가 된다.
innterFunc
가 호출되면 자신의 실행 컨텍스트가 실행 컨텍스트 스택에 쌓이고 변수 객체와 스코프 체인 그리고 this에 바인딩할 객체가 결정된다. 이때 스코프 체인은 전역 스코프를 가리키는 전역 객체와 함수 outerFunc 를 가리키는 함수 outerFunc의 활성객체 그리고 자신의 스코프를 가리키는 활성 객체를 순차적으로 바인딩 한다. 스코프 체인이 바인딩한 객체가 렉스컬 스코프가 된다.
렉시컬 스코프 덕분에 내부함수 innterFunc
가 외부함수의 변수인 x에 접근이 가능하게 된다. 자바스크립트 엔진은 실행 컨텍스트의 스코프 체인을 검색하여 바인딩된 레퍼런스를 차례로 접근하며 x를 찾는다.innterFunc
함수 내에서 변수 x를 검색해서 실패하면 innterFunc
함수를 포함하는 외부함수 outerFunc의 스코프에서 변수 x를 찾는다.
클로저
다음은 위의 코드를 외부함수가 innterFunc
를 리턴하는 함수로 변경한 코드이다.
function outerFunc() {
var x = 10;
var innerFunc = function () { console.log(x); };
return innerFunc;
}
/**
* 함수 outerFunc를 호출하면 내부 함수 innerFunc가 반환된다.
* 그리고 함수 outerFunc의 실행 컨텍스트는 소멸한다.
*/
var inner = outerFunc();
inner(); // 10
outerFunc를 실행하면 innterFunc를 반환하고 생을 마감한다. 함수 outerFunc는 실행된 이후 콜스택(실행 컨텍스트)에서 제거되므로 outerFunc의 변수 x또한 유효하지 않게된다. 따라서 x에 접근할 방법이 달리 없어보일 수 있다. 그러나 위 코드의 실행 결과는 x의 값인 10이 된다.
이미 라이프사이클이 종료되어 실행 컨텍스트에서 제거된 함수 outerFunc의 지역 변수 x가 다시 부활한듯 동작한다.
이처럼 자신을 포함하고 있는 외부함수보다 내부함수가 더 오래 유지되는 경우, 외부함수 밖에서 내부 함수가 호출되더라도 외부함수의 지역변수에 접근 가능한데 이러한 함수를 클로저라고 부른다.
클로저는 함수와 함수가 선언되었을 때의 렉시컬 환경과의 조합이다.
위 정의에서 가리키는 함수는 예제의 내부 함수이고 렉시컬 환경은 내부 함수가 선언 되었을 때의 스코프를 의미한다.
클로저는 반환된 내부 함수가 자신이 선언됐을 때의 환경(Lexical environment)인 스코프를 기억하여 자신이 선언됐을 때의 환경 밖에서 호출되어도 그 환경에 접근할 수 있는 함수를 말한다.
더 간단하게 말하면 클로저는 자신이 생성될 때의 환경을 기억하는 함수 라고 할 수 있다.
자유 변수 (Free variable)
클로저에 의해 참조되는 외부함수의 변수, 즉 outerFunc 함수의 변수 x를 자유 변수라고 한다.
클로저라는 이름은 자유변수에 함수가 닫혀있다(closed)라는 의미가 된다.
실행 컨텍스트의 관점에서 내부 함수가 유효한 상태에서 외부함수가 종료하여 외부함수의 실행컨텍스트가 반환되어도, 외부 함수 실행 컨텍스트 내의 활성 객체는 내부함수에 의해 참조되는 한 유효하여 내부함수가 스코프 체인을 통해 참조할 수 있는 것을 의미한다.
즉 외부함수가 이미 반환되었어도 외부함수 내의 변수는 이를 필요로 하는 내부함수가 하나 이상 존재하는 경우 계속 유지된다. 이 때 내부함수가 외부함수에 있는 변수의 복사본이 아니라 실제 변수에 접근한다 !!
클로저의 활용
클로저는 자신이 생성될 때의 환경(렉시컬 환경)을 기억해야 하므로 메모리 차원의 손해를 볼 수 있다. 하지만 클로저는 js의 강력한 기능으로 이를 적극적으로 사용해야 한다.
다음은 클로저가 유용하게 사용되는 상황들이다.
상태 유지
현재 상태를 기억하고 변경된 최신 상태를 유지하기 위해서 클로저를 사용한다.
display를 토글하는 동작을 클로저로 작성할 수 있다.
<!DOCTYPE html>
<html>
<body>
<button class="toggle">toggle</button>
<div class="box" style="width: 100px; height: 100px; background: red;"></div>
<script>
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;
</script>
</body>
</html>
- 즉시 실행하는 함수는 함수를 반환하고 즉시 소멸한다. 즉시 실행 함수가 반환하는 요소는 자신이 생성되었을 때의 렉시컬 환경에 속한 변수
isShow
를 기억하는 클로저다. 클로저가 기억하는 변수isShow
는 box요소의 표시를 상태를 나타낸다 - 클로저를 이벤트 핸들러로 이벤트 프로퍼티에 등록한다. 이벤트 핸들러에서 클로저를 제거하지 않는 한 클로저가 기억하는 렉시컬 환경의 변수
isShow
는 소멸하지 않는다. = 현재 상태를 기억한다. - 버튼을 클릭하면 이벤트 프로퍼티에 할당한 이벤트 핸들러인 클로저가 호출된다. 이 때 box의 상태를 나타내는 변수
isShow
값이 변경된다.isShow
는 클로저에 의해 참조되고 있기 때문에 유효하며 변경 상태를 계속 최신으로 유지한다.
이처럼 클로저는 현재 상태를 기억하고 이 상태가 변경되어도 최신 상태를 유지해야 하는 상황에 매우 유용하다.
클로저 기능이 없다면 상태를 유지하기 위해 전역변수를 사용할 수 밖에 없다. 전역 변수는 언제든지 누구나 접근 가능하고 변경 가능하기 때문에 부작용을 유발해 오류의 원인이 되므로 사용을 억제해야 한다.
전역 변수의 사용 억제
버튼이 클릭될 때 마다 클릭한 횟수가 누적되어 화면에 표시되는 카운터를 만든다. 전역 변수에 카운트를 두고 해당 변수를 증가시킬 수 있다.
그러나 클로저를 사용하면 전역 변수를 만들 필요가 없다. 그러나 전역변수이기 때문에 누구나 언제든지 접근하여 변경이 가능하다. 이는 의도치않게 값이 변경될 수 있음을 의미하고, 의도치 않게 전역 변수의 값이 바뀌어버리면 오류로 이어진다.
아래는 전역변수를 사용하여 카운터를 관리하는 코드이다.
<!DOCTYPE html>
<html>
<body>
<p>전역 변수를 사용한 Counting</p>
<button id="inclease">+</button>
<p id="count">0</p>
<script>
var incleaseBtn = document.getElementById('inclease');
var count = document.getElementById('count');
// 카운트 상태를 유지하기 위한 전역 변수
var counter = 0;
function increase() {
return ++counter;
}
incleaseBtn.onclick = function () {
count.innerHTML = increase();
};
</script>
</body>
</html>
클로저를 사용하여 카운트 변수를 지역변수로 상태를 유지하며 관리할 수 있다.
<!DOCTYPE html>
<html>
<body>
<p>클로저를 사용한 Counting</p>
<button id="inclease">+</button>
<p id="count">0</p>
<script>
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();
};
</script>
</body>
</html>
스크립트가 실행되면 즉시 실행 함수가 호출되고 변수 increase에는 함수 function () { return ++counter; }
가 할당된다.
이 함수는 자신이 생성됐을 때의 렉시컬 환경을 기억하는 클로저다. 즉시 실행 함수는 호출된 이후 소멸되지만 반환된 함수가 increase에 할당되어 increase 버튼을 클릭하면 클릭 이벤트 핸들러 내부에서 호출된다.
이때 클로저인 이 함수는 자신이 선언됐ㅇ르 때의 렉시컬 환경의 지역변수 counter를 기억한다. 따라서 즉시 실행 함수의 변수 counter에 접근할 수 있고 변수 counter는 자신을 참조하는 함수가 소멸될 때 까지 유지된다.
즉시 실행 함수는 한번만 실행되므로 increase가 호출될 때 마다 변수 coutner가 재차 초기화될 일은 없을 것이다. 변수 counter는 외부에서 직접 접근할 수 없는 private변수이므로 전역 변수를 사용했을 때와 같이 의도적이지 않은 변경을 걱정할 필요가 없으므로 보다 안정적인 프로그래밍이 가능하다.
함수형 프로그래밍에서 클로저를 활용하는 예제
// 함수를 인자로 전달받고 함수를 반환하는 고차 함수
// 이 함수가 반환하는 함수는 클로저로서 카운트 상태를 유지하기 위한 자유 변수 counter을 기억한다.
function makeCounter(predicate) {
// 카운트 상태를 유지하기 위한 자유 변수
var counter = 0;
// 클로저를 반환
return function () {
counter = predicate(counter);
return counter;
};
}
// 보조 함수
function increase(n) {
return ++n;
}
// 보조 함수
function decrease(n) {
return --n;
}
// 함수로 함수를 생성한다.
// makeCounter 함수는 보조 함수를 인자로 전달받아 함수를 반환한다
const increaser = makeCounter(increase);
console.log(increaser()); // 1
console.log(increaser()); // 2
// increaser 함수와는 별개의 독립된 렉시컬 환경을 갖기 때문에 카운터 상태가 연동하지 않는다.
const decreaser = makeCounter(decrease);
console.log(decreaser()); // -1
console.log(decreaser()); // -2
makeCounter는 보조 함수를 인자로 전달받고 함수를 반환하는 고차함수이다. 함수 makeCounter가 반환하는 함수는 자신이 생성됐을 때의 렉시컬 환경인 함수 makeCounter의 스코프에 속한 변수 counter을 기억하는 클로저다.
makeCounter는 인자로 전달받은 보조 함수를 합성하여 자신이 반환하는 함수의 동작을 변경 할 수 있다. 이때 makeCounter를 호출해 함수를 반환할 때 반환된 함수는 자신만의 독립된 렉시컬 환경을 가진다.
변수 increaser와 변수 decreaser에 해당하는 함수는 각각 자신만의 독립된 렉시컬 환경을 갖기 때문에 카운트를 유지하기 위한 자유 변수 counter를 공유하지 않아서 더하고 빼는 것이 연동되지 않는다.
따라서 독립된 카운터가 아니라 연동하여 증감하는 카운터를 만드려면 렉시컬 환경을 공유하는 클로저를 만들어야 한다.
정보 은닉
생성자를 만들고 counter 객체를 만든다.
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 메소드를 갖는 인스턴스를 생성한다. 이 메소드들은 자신이 생성됐을 때의 렉시컬 환경에 속한 counter 를 기억하는 클로저이며 렉시컬 환경을 공유한다.
생서자 함수가 생성한 객체의 메소드는 객체의 프로퍼티에만 접근할 수 있는 것이 아니며 자신이 기억하는 렉시컬 환경의 변수에도 접근가능하다.
이 때 생성자 함수 Counter의 변수 counter는 this에 바인딩된 프로퍼티가 아니라 변수이다. counter가 this에 바인딩된 프로퍼티라면 생성자 함수 Counter가 생성한 인스턴스를 통해 외부에서 접근 가능한 public 프로퍼티가 생성되지만, Counter내에서 선언된 변수 counter는 생성자 함수 Counter외부에서 접근 불가하다.
그러나 내부 메소드인 increase, decrease는 클로저이기 때문에 자신이 생성됐을 때의 렉시컬 환경인 생성자 함수 Counter의 변수 counter에 접근할 수 있다.
이러한 클로저의 특징을 사용해 클래스 기반 언어의 private를 흉내낼 수 있다.
출처
아래 글을 읽고 정리한 내용입니다.
https://poiemaweb.com/js-closure
Closure | PoiemaWeb
클로저(closure)는 자바스크립트에서 중요한 개념 중 하나로 자바스크립트에 관심을 가지고 있다면 한번쯤은 들어보았을 내용이다. execution context에 대한 사전 지식이 있으면 이해하기 어렵지 않
poiemaweb.com