실무경험을 하면서 클로저를 사용한 경험이 많지 않았다. 물론 클로저가 없어도 코드를 작성하는데 큰 무리가 있었던 경험은 없지만 최근 자바를 공부하면서 조금 더 객체지향적으로 코드에 접근하는것이 성장하는데 큰 기여를 할거 같다는 생각이 들었다. 클로저에 대한 경험을 더 정확하게 파악한다면 실무에서 더 적극적으로 사용할 수 있지 않을까 하는 생각에 클로저의 개념을 더 명확하게 알기위해서 mdn 문서를 통해 정리를 해보고자 한다.
mdn 문서에 따르면
클로저
는 함수와 함수가 선언된 어휘적 환경의 조합이다. 클로저를 이해하려면 자바스크립트가 어떻게 변수의 유효범위를 지정하는지(Lexical scoping)를 먼저 이해해야 한다.
라고 쓰여 있다. 코드를 통해 이해해보면
function init() {
var name = "Mozilla"; // name is a local variable created by init
function displayName() { // displayName() is the inner function, a closure
alert (name); // displayName() uses variable declared in the parent function
}
displayName();
}
init();
위에 코드에서 init()
을 실행시키면 displayName
이라는 함수가 생성이 된다. 여기서 displayName
은 일반 함수처럼 파라미터를 설정하고 함수내부의 지역변수만을 사용해서 특정 기능을 실행시키는 함수가 아닌 외부 렉시컬 환경의 변수 name
에 접근할 수 있는 함수가 된다. 이를클로저
라고 한다. (물론 displayName
이 별개로 name이라는 변수를 선언하면 내부에 선언된 지역변수 name을 참조할 것이다.)
클로저는 어떤 데이터(어휘적 환경)와 그 데이터를 조작하는 함수를 연관시켜주기 때문에 유용하다. 이것은 객체가 어떤 데이터와(그 객체의 속성) 하나 혹은 그 이상의 메소드들을 연관시킨다는 점에서 객체지향 프로그래밍과 분명히 같은 맥락에 있다.
클로저는 객체지향의 상속의 특징을 가지고 있다. 코드를 통해 이해해보면
function makeSizer(size) {
return function() {
document.body.style.fontSize = size + 'px';
};
}
var size12 = makeSizer(12);
var size14 = makeSizer(14);
var size16 = makeSizer(16);
makeSizer
는 클래스, 그리고 size12
, size14
, size16
은 생성자를 통해 생성된 인스턴스가 처럼 보인다. makeSizer
는 함수를 생성하는 공장과 같다. 결과적으로 생성된 함수들은 클로저가 되며 서로다른 어휘적 환경을 저장한다.
위 처럼 생성된 함수를 아래 코드와 같이 사용 할 수 있다.
document.getElementById('size-12').onclick = size12;
document.getElementById('size-14').onclick = size14;
document.getElementById('size-16').onclick = size16;
<a href="#" id="size-12">12</a>
<a href="#" id="size-14">14</a>
<a href="#" id="size-16">16</a>
이번에는 클로저를 활용해서 카운터함수를 만들어 보자.
const makeCounterClosure = function () {
let counter = 0;
function increaseBy(val) {
counter = counter + val
}
function decreaseBy(val) {
counter = counter - val
}
return {
plus: function (val = 1) {
increaseBy(val)
},
minus: function (val = 1) {
decreaseBy(val)
},
value: function () {
return counter
}
}
}
const counterClosure1 = makeCounterClosure();
const counterClosure2 = makeCounterClosure();
console.log(counterClosure1.value()) // 0
counterClosure1.plus()
console.log(counterClosure1.value()) // 1
counterClosure1.minus(3)
console.log(counterClosure1.value()) // -2
console.log(counterClosure2.value()) // 0
위 코드에서는 counterClosure1.plus
, counterClosure1.minus
, counterClosure1.value
세 함수에 의해 공유되는 하나의 어휘적 환경을 만든다. counterClosure1
함수는 makeCounterClosure
를 실행할 때 어휘적 환경을 가지는 클로저가 된다. 이 어휘적 환경은 counter
변수와 increseBy
, decreaseBy
함수를 가지는데 익명 함수의 외부에서는 익명 함수의 어휘적 환경에 접근할 수 없다. 이는 마치 자바의 private method
와 비슷한데 익명 함수의 어휘적 환경에 접근하기 위해서는 makeCounterClosure
에서 반환 된 세 개의 퍼블릭 함수를 사용해야만 한다.
counterClosure1
과 counterClosure2
는 서로 다른 어휘적 환경을 가지는 클로저이므로 어휘적 환경 변수 counter
를 공유하지 않는다.
ES6 이전에는 스코프가 함수 스코프, 전역 스코프만 존재했었다. 하지만 ES6 부터 블록 또한 블록 스코프를 가지게 되었는데 문제는 var
의 경우 블록 스코프 안에서 선언 시 전역 변수를 생성한다. 따라서 블록 스코프를 유지하기 위해서는 let
과 const
를 사용해야 한다.
console.log(x); // undefined
if (Math.random() > 0.5) {
var x = 1;
} else {
var x = 2;
}
console.log(x); // 1 or 2
if (Math.random() > 0.5) {
const y = 1;
} else {
const y = 2;
}
console.log(y); // ReferenceError: y is not defined
위의 문제점은 for 문 블록 스코프 안에서도 발생한다. 아래 코드를 통해 이해해보자.
function setClick() {
for (var i = 0; i < 3; i++) {
btns[i].onclick = function () {
console.log(i);
};
}
}
setClick();
다음과 같이 코드를 작성하게 되면 var
로 선언된 변수 i 가 결론적으로 전역 변수로 생성되기 때문에 console.log(i)에서 i는 모두 전역 변수인 i를 가리키게 되고 최종적으로 i는 2가 되므로 버튼을 누를 때 모두 2를 출력하게 된다.
위를 해결하기 위해서 ES6 이전에는 클로저를 다음과 같이 해결책으로 제시했다.
function makeClosure (val) {
return function closure() {
console.log(val)
}
}
function setClick() {
for (var i = 0; i < 3; i++) {
btns[i].onclick = makeClosure(i)
}
}
setClick();
하지만 ES6 이후로 let
과 const
가 나오면서 var
를 let
과 const
로 선언해주기만 해도 위와 같은 문제를 피할 수 있다.
function setClick() {
for (const i = 0; i < 3; i++) {
btns[i].onclick = function () {
console.log(i);
};
}
}
setClick();
오늘은 closure
에 대해서 정리하는 시간을 가져봤다. 앞으로 실무에서 closure
를 응용해서 실용적인 코드를 짤 기회가 생기면 적극적으로 활용할 수 있을 것 같다.