클로저를 알아보기에 앞서 실행 컨텍스트와 스코프 체인에서 설명한 [[scope]]
프로퍼티에 대해 알고있으면 더욱 이해하기 쉬우니 참고하자!😆
클로저란 무엇인가?🤔
간단하게 설명하면, 실행이 끝난 외부 함수의 변수에 접근이 가능한 함수라고 할 수 있다.
일단 아래의 코드를 보며 확인해보자!
var outer = function(){
var x = 5;
var inner = function(){
console.log(x);
};
inner();
};
outer(); // 5
위의 코드처럼 스코프 체인을 통해 내부 함수에서 외부 함수의 변수에 접근이 가능하다는 것은 이미 알고 있다.
그렇다면 다음과 같이 코드를 바꿔보면 어떨까?
var outer = function(){
var x = 5;
var inner = function(){
console.log(x);
};
return inner;
};
var func = outer();
func(); // 5
이번엔 outer()
함수에서 inner()
함수를 호출하지 않고, inner()
함수를 반환한 뒤 변수에 대입해서 실행시켰지만 같은 결과를 출력하고 있다.
실행 컨텍스트의 구조를 생각하면 outer()
함수의 실행 컨텍스트는 호출이 끝난 후 스택에서 사라졌을 텐데, 어떻게 outer()
함수의 변수에 접근이 가능하지...?🙄
이유는 바로 함수 객체가 가지고 있는 내부 프로퍼티 [[scope]]
, 즉 함수가 선언된 위치를 기준으로 만들어진 스코프 체인을 통해 외부 함수의 변수를 참조하고 있어서 가비지 컬렉션(Garbage Collection)의 대상이 되지 않았기 때문이다.
이렇게 실행이 끝난 외부 함수의 변수에 접근이 가능한 함수를 클로저라고 부르고, 클로저가 참조하는 외부 함수의 변수를 자유 변수라고 한다.
그러면 아래의 경우에는 어떻게 되는지 보도록 하자!😉
var x = 10;
var outer = function(){
var x = 5;
var inner = function(){
console.log(x);
};
return inner;
};
var func = outer();
func(); // 5
이번엔 전역 객체에 var x = 10
을 전역 변수로 선언해줬다. 그렇지만 func()
함수의 결과는 전과 같이 5
를 출력하는 것을 볼 수 있다.
func()
함수도 따지고 보면 전역 변수인데, 왜 전역 컨텍스트에서 x
를 참조하지 않고 outer() 실행 컨텍스트
에서 x
값을 참조하였을까?
이유는 클로저 또한 렉시컬 스코프를 이용해 자신이 선언된 위치에서 상위 스코프를 결정하기 때문이다.
클로저가 뭔지는 알겠는데... 그럼 어디에 사용하는 녀석인가. 예제를 통해 하나씩 알아보자!😆
전역 변수의 선언은 많으면 많을수록 오류를 발생시키는, 적과도 같은 존재다.
클로저를 이용하면 이런 전역 변수의 사용을 최소화 할 수 있다. 다음 코드를 보며 이해해보자!🙂
var change = (function(){
var bool = false;
return function(){
bool = !bool;
return bool;
};
})();
console.log(change()); // true
console.log(change()); // false
console.log(change()); // true
즉시 실행 함수를 이용해 bool
변수를 전역 변수로 선언하지 않고도 해당 값을 유지하고, 호출을 통해 반전도 가능한 것을 볼 수 있다.
클로저를 이용하면 Java
의 접근 제어자 private
을 사용한 것 처럼 함수 객체 외부에서 내부 변수에 접근하는 것을 막을 수 있다. 코드를 보면서 알아보자!🤗
function Func(){
var num = 1;
this.plus = function(){
num++;
};
this.minus = function(){
num--;
};
this.print = function(){
console.log(num);
};
};
var obj = new Func();
obj.plus(); // num === 2
obj.plus(); // num === 3
obj.minus(); // num === 2
obj.print(); // 2
생성자 함수 Func()
의 num
은 프로퍼티가 아닌 변수이기 때문에 외부에서 접근할 수 없고, 다음과 같이 plus()
, minus()
, print()
함수를 이용해야만 접근할 수 있다.
하지만 이렇게 여러 가지 장점이 있는 클로저도 좋은 점만 있는 것은 아니었다...
클로저 활용의 주의점 하면 항상 나오는 그 녀석! 반복문 안에서의 클로저 사용이다. 문제를 일으키는 코드부터 보도록 하자!
var arr = [];
var func = function(){
for(var i = 0; i < 5; i++){
arr[i] = function(){
console.log(i);
};
}
}
func();
arr[0](); // 5
arr[1](); // 5
arr[2](); // 5
arr[3](); // 5
arr[4](); // 5
의도한 동작은 i
가 arr[]
의 index
에 따라 점점 증가하는 것을 출력하려고 했지만, 전부 5
가 출력되게 되었다.
이유는 arr[i]
에 담긴 함수가 변수 i
를 클로저로 참조하고 있기 때문인데, for 반복문
이 끝나고 증가식 i++
에 의해 i
값은 5
가 되게 된다.
이후 console.log(i)
를 통해 i
값을 출력하려고 할 때, 클로저로 인해 이미 반복이 끝나고 값이 증가해버린 i
값을 참조하게 되고, 그것 때문에 위의 상황이 일어나게 된 것이다.
이런 문제를 해결하려면 아래와 같이 새로운 스코프를 추가시켜주면 된다.
var arr = [];
var func = function(){
for(var i = 0; i < 5; i++){
arr[i] = (function(x){
return function(){
console.log(x);
};
})(i);
}
}
func();
arr[0](); // 0
arr[1](); // 1
arr[2](); // 2
arr[3](); // 3
arr[4](); // 4
함수 외부에 새로운 스코프를 만들고, 즉시 실행 함수를 이용해 i
가 아닌 i
를 전달 인자로 한 매개 변수 x
를 참조하도록 변경하고 정상적으로 동작하는 것을 볼 수 있다!😆
이는 var
키워드의 함수 단위 스코프 때문에 일어나는 현상이므로, ES6
에서 추가된 let
키워드를 이용하면 외부에 별도의 스코프를 만들어주지 않아도 간단하게 해결된다.
클로저는 어디까지나 [[scope]]
프로퍼티를 이용해 외부 함수의 변수가 가비지 컬렉션의 대상이 되는 것을 막는 것이다.
이 말은 반대로 원래 호출이 끝나고 반환해야 할 메모리를 클로저가 붙잡아 두고 있다는 소리도 된다.
따라서 클로저의 사용이 다 끝났다면 자유 변수를 참조하고 있는 변수에 null
을 대입하여 메모리 누수를 막도록 하자!
참고 자료