React의 Hook을 좀 공부해 보고 싶었는데, 클로저라는 개념이 선행이 되어야 한다는걸 알게되었따.
그리고 또 클로저라는게 자바스크립트에서도 중요한 개념 중 하나로 이해했다.
그래서 한번 알아보려고 한닷!
function outerFunc() {
var x = 10;
var innerFunc = function () { console.log(x); };
innerFunc();
}
outerFunc(); // 10
함수 outerFunc
내에서 내부함수 innerFunc
가 선언되고 호출되었는데, 이때 내부함수인 innerFunc
는 자신을 포함하고 있는 외부함수 outerFunc
의 변수 x에 접근할 수 있다!
왜냐하면 innerFunc
는 outerFunc
내부에 선언되었기 때문이다.
이것이 바로 이전에 알아본 렉시컬 스코핑
이라는 개념인데,
함수가 어디서 호출되는지는 중요하지 않고, 함수가 어디에서 선언되었는지에 따라 상위 스코프가 결정된다.
this의 개념은 이와 좀 다르다, this는 어디서 호출했느냐에 따라서 바인됭 되는게 다르다.
그러면 내부함수 innerFunc
를 outerFunc
내에서 호출하는것이 아니라 반환하도록 해보자.
function outerFunc() {
var x = 10;
var innerFunc = function () { console.log(x); };
return innerFunc;
}
/**
* 함수 outerFunc를 호출하면 내부 함수 innerFunc가 반환된다.
* 그리고 함수 outerFunc의 실행 컨텍스트는 소멸한다.
*/
var inner = outerFunc();
inner(); // 10
함수 outerFunct
는 내부함수 innerFunc
를 반환하고 생을 마감했다. (이후에 추가적으로 호출되지 않았다는 의미)
즉! outerFunc는 실행 이후 callStack에서 제거 되어서 변수 x에 대해서 접근할 방법이 없어보이지만, 실제로는 그렇지 않다.
이처럼 자신을 포함하고 있는 외부함수보다 내부함수가 더 오래 유지되는 경우, 외부 함수 밖에서 내부 함수가 호출되더라도 외부함수의 지역변수에 접근할 수 있는 함수를 클로저 라고 한다.
클로저는 자기가 선언(생성)될 때의 환경을 기억해야 하기때문에 메모리 차원에서 손해볼 수 있다. (메모리 관리 업으로 했던 사람으로써 kill 마렵긴 함ㅋㅋ)
하지만 자바스크립트의 강력한 기능으로 적극적으로 사용해야한다고 한다.
클로저가 가장 유용하게 사용되는 상황은 현재 상태를 기억하고 변경된 최신 상태를 유지하는 것이다.
<!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의 상태를 클로저를 통해서 계속 기억하도록 할 수 있다.
전역변수를 사용함으로써 접근이 용이하면서 오는 사이드 이펙트를 클로저를 사용함으로써 방지할 수 있다.
추가적으로 var toggle = (...)
이 부분에 toggle에 함수 선언식은 즉시실행함수? 라고해서 바로 할당과 동시에 선언되고 죽는다.
<!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 변수에 counter = 0 상태를 기억하고 있는 클로저를 할당된다.
즉시 실행함수는 바로 죽어버리지만, counter를 참조하고 있는 내부함수는 increase에 할당되어 죽지 않고 이벤트 핸들러 내부에서 호출된다.
따라서 즉시실행함수는 바로 죽어버리지만, 내부 변수인 counter를 참조하는 내부함수와 counter는 소멸되지 않는다. (일단은)
위 코드처럼 전역변수 대신 클로저를 사용하면 외부 환경에 의해서 카운터가 노출되거나 수정되는 일이 없기에 좀 더 프로그램의 안정성을 높일 수 있다.
// 함수를 인자로 전달받고 함수를 반환하는 고차 함수
// 이 함수가 반환하는 함수는 클로저로서 카운트 상태를 유지하기 위한 자유 변수 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