1) 클로저란?
함수가 선언된 렉시컬 스코프를 기억해서, 함수가 스코프 밖에서 실행되어도 해당 스코프에 접근할 수 있도록 해주는 것을 클로저라고 말합니다. 함수 안에 함수가 있는 형태로 외부(outer) 함수가 실행 후 종료되어도 내부(inner) 함수에서 outer 함수의 지역변수를 참조할 수 있는 것이 특징입니다. 더 정확히 말하자면 outer 함수는 종료된다고 할 지라도 inner 함수가 실행되는 상태이면 outer 함수는 닫히지 못합니다.
클로저는 일반적으로 아래와 같은 조건을 충족합니다.
- 내부 함수는 익명 함수로, 외부 함수의 return 값으로 사용
- 반환된 내부 함수는 외부 함수의 실행컨텍스트에서 실행됨
- 내부 함수에서 사용되는 변수는 외부 함수의 변수 스코프에 있어야 함
코드를 예로 들어 자세히 살펴보겠습니다.
<script>
var counter = (function(){
var count = 0;
return function(){
++count;
document.getElementById("clickCount").innerHTML = count;
}
})();
</script>
<html>
<button onclick="counter()">Increase Count</button>
<div>
Click count is <span id="clickCount"> 0 </span> times.
</div>
</html>
버튼을 클릭하면 숫자를 증가시키는 코드입니다. 만약 위처럼 클로저를 사용하지 않았다면, 전역 변수로 clickCount를 생성하여 onclick 이벤트가 발생할 때마다 clickCount span 태그 내 값을 변경해주어야 했을 것이고, 이런 전역 변수가 많아지면 전역 스페이스가 오염되었을 것입니다.
2) 클로저를 사용한 자바스크립트 캡슐화
위에서 보았듯이 클로저를 이용하면 전역 변수가 전역 네임스페이스를 오염시키지 않도록 만들어줄 수 있다고 했습니다. 또 다른 클로저를 이용한 이점에는 private 메서드 혹은 변수를 작성할 수 있도록 해준다는 점입니다. 기본적으로 자바스크립트는 자바와 달리 private 접근 제한 기능을 제공하지 않습니다. 그래서 클로저를 이용하여 마치 private처럼 동작하도록 작성할 수 있습니다.
private 메서드가 중요한 이유는 객체지향의 장점인 정보 은닉과 캡슐화(코드에 대한 접근을 제한)를 제공한다는 점뿐만 아니라, 마찬가지로 불필요한 메서드가 전역 네임 스페이스를 혼란스럽게 만들지 않도록 도와준다는 점입니다.
코드를 예로 살펴보겠습니다.
[자바스크립트 캡슐화]
var counter = (function() {
var count = 0;
function operation(value) {
count += value;
}
return {
increase: function() {
operation(1);
},
decrease: function() {
operation(-1);
},
getCount: function() {
return count;
}
}
})();
console.log(counter.getCount()); // result : 0
counter.increase();
counter.increase();
counter.increase();
console.log(counter.getCount()); // result : 3
counter.decrease();
console.log(counter.getCount()); // result : 2
- private : count, operation()
- counter 함수 내의 익명 함수 내 세 개의 퍼블릭 함수(increase(), decrease(), getCount())만 접근 가능
- 위 코드에서 클로저는 세 개 함수 increase(), decrease(), getCoune()이며, 같은 렉시컬 환경을 공유하기 때문에 count 값이 유지된다.
이런 식으로 클로저를 사용하는 것을 모듈 패턴이라고 합니다.
3) 주의할 점
(1) 변수의 공유
아래 코드를 먼저 살펴보겠습니다.
[직접 참조]
function adder(){
var num= 0;
return {
singleAdder: function(){
num+=1;
console.log("num :"+num);
},
doubleAdder: function(){
num+=2;
console.log("num :"+num);
}
};
}
var out = adder();
var out2 = adder();
out.singleAdder(); // result : 1
out.doubleAdder(); // result : 3
out2.singleAdder(); // result : 1
out2.doubleAdder(); // result : 3
- 외부 함수의 변수는 값을 복사해 전달하는 것이 아니라 직접 참조 : adder의 singleAdder와 doubleAdder는 num을 공유
- 외부 함수 실행시 내부 함수가 반환되므로, 외부 함수의 리턴 값이 여러 개이면 클로저가 여러 개 생성되고 각 클로저마다 외부 함수의 변수는 다른 주소 값을 가짐 : singleAdder과 doubleAdder는 별개의 변수 => 따라서 클로저가 남용되면 메모리 측면에서 매우 비효율적
(2) Memory Leak
전통적인 방식의 서버 사이드 렌더링 애플리케이션에서는 페이지가 이동될 때 변수의 라이프사이클이 끝나기 때문에 이러한 고려를 할 필요가 없었습니다. 그러나 SPA에서는 클로저를 닫아주지 않으면 클로저가 계속해서 메모리에 쌓이게 되고 Memory leak이 발생할 수 있습니다. 따라서 클로저 변수를 더 사용하지 않는다면 null을 할당해주어 메모리에서 제거될 수 있도록 해 주어야 합니다.
p.s 내부 함수를 return하지 않아도 클로저를 생성하는 경우가 존재하는데, 이는 아래와 같습니다.
- setTimeout, setInterval 등과 같이 비동기적으로 호출되는 경우
- addEventListener와 같이 이벤트의 콜백 함수로 활용되는 경우
클로저는 위에서 언급했듯이 많은 장점들을 가지고 있고, 이를 통해 코드를 더욱더 쉽고 간결하게 만들어 줍니다. 또한 자바스크립트에서 가장 많이 이용되는 이벤트 및 비동기식 호출에서 기본적으로 클로저가 생성되기 때문에 클로저를 잘 알지 못하면 이러한 코드를 작성하기 어렵습니다. 무엇보다도 클로저의 단점(메모리를 소모하며, 클로저마다 새로운 스코프를 생성한다는 점에서 성능상의 이슈)이 애플리케이션에 치명적인 문제를 일으킬 수 있기 때문에, 클로저가 어떻게 동작하는지 잘 이해해 두는 것이 중요합니다.
[references]
4 Javascript Design Patterns You Should Know
'Javascript' 카테고리의 다른 글
[Javascript] Debounce & Throttle (0) | 2020.10.21 |
---|---|
[Javascript] 자바스크립트에서 메모리 누수의 4가지 형태 (0) | 2020.10.21 |
[Javascript] 실행 컨텍스트 (0) | 2020.10.21 |
[Javascript] Scope (0) | 2020.10.21 |
[ES6+] var vs let vs const (0) | 2020.10.21 |
댓글