2019. 06. 01 수정
1. 클로저(Closure)란?
클로저란 이미 생명 주기가 끝난 외부 함수의 변수를 참조하는 함수를 클로저라고 합니다.
예를 들어, outer() 함수가 선언될 당시에 그 내부에는 x라는 변수와 inner()함수를 정의하고 있고 outer() 함수는 inner 함수를 반환합니다.
만약 outer() 함수 외부에서 outer() 함수를 호출하면, 다음과 같은 실행 순서를 따릅니다.
- inner 함수가 반환되어 inner() 함수 실행
- outer() 함수에서 정의된 변수 x를 참조해서 ++연산자를 수행합니다.
즉, outer() 함수를 호출하면 inner 함수에서 변수 x가 자신의 유효 범위가 아님에도, outer() 함수에 정의된 변수를 참조합니다.
이 때 inner() 함수를 클로저라고 하며, outer() 함수에 정의된 변수 x를 자유 변수라고 합니다.
클로저 이름의 의미는 "자유 변수에 닫혀있다", "자유 변수에 엮여있다"는 의미입니다.
함수형 언어가 아니라면 inner() 함수의 유효 범위(scope) 밖에서, 즉 outer() 내부에 존재하고 inner() 함수 외부에 존재하는 변수, 함수에 접근할 수 없지만, JS에서는 클로저를 통해 접근할 수 있으며 또한 값을 변경할 수도 있습니다.
왜냐하면 outer() 함수가 선언될 당시의 유효한 환경을 기억하고 있다가, outer() 함수를 호출할 때 기억했던 환경을 사용할 수 있기 때문입니다.
2. 클로저를 사용하여 값 추측 예제
클로저에 대한 개념은 위와 같고, 이제 클로저를 사용하면 변수의 값이 어떻게 변할지 예제를 살펴보도록 하겠습니다.
function outer(){
var x = 10;
function inner(){
x++;
console.log(x)
};
return inner;
}
var x = -10;
var foo = outer();
foo(); // (a)
foo(); // (b)
console.log(x); // (c)
위의 코드는 앞에서 살펴봤던 예제와 비슷하며, 동작 과정은 다음과 같습니다.
- outer() 함수 내부에서 변수 x값을 10으로 할당합니다.
- inner() 함수에서 x를 증가시킵니다.
- outer() 함수 외부에서 x 값을 정의한 후, outer() 함수를 두 번 실행합니다.
먼저 (a), foo() 함수를 한 번 호출하면 어떤 값이 출력될까요?
11이 출력될까요? -9가 출력될까요?
즉, 어떤 outer() 외부에 있는 x를 참조할까요, 내부에 있는 x를 참조할까요?
클로저는 함수가 선언될 당시의 환경을 기억하고 있다가, 그 함수가 호출될 때 기억하고 있던 환경을 사용할 수 있다고 했습니다.
즉, foo() 함수를 호출했을 때 x는 선언될 당시의 환경인 10을 기억하고 있기 때문에 x++의 결과 x는 11이 됩니다.
(b) foo() 함수를 한번 더 호출해볼까요?
이번에는 11이 출력될까요? 12가 출력될까요?
클로저를 통해 기억하고 있던 환경의 값을 변경할 수도 있다고 했습니다.
따라서 이전 foo()함수 호출 결과 x는 11이 되었으므로, x 값이 변경된 11을 기억하고 있기 때문에 x는 12가 됩니다.
이번에는 foo() 함수를 호출하지 말고, 그냥 x를 출력해보도록 하겠습니다. (c)
12가 출력될까요? -10이 출력될까요?
outer() 함수 외부에 선언한 변수 x는 outer()함수와 관련이 없습니다.
outer() 함수를 선언할 때 외부에 존재하는 변수 x는 outer() 함수의 유효한 범위가 아니였기 때문에 독립적인 값을 갖게 됩니다.
따라서 결과는 -10이 출력됩니다.
3. 클로저를 통한 캡슐화
클로저 덕분에 JS에서는 객체지향 프로그래밍, 즉 변수 또는 함수를 private 으로 활용할 수 있습니다.
클로저는 일종의 보호막이라 할 수 있습니다.
해당 함수가 존재하는 하는 동안 그 함수의 유효 범위에 있는 변수와 함수를 가비지 컬렉션으로부터 보호받기 때문이죠.
즉, 클로저는 변수의 유효범위를 제한하려는 용도로 사용할 수 있습니다. ( 캡슐화가 가능 )
예제
function Outer(){
var x = 10;
this.getX = function(){
return x;
}
this.setX = function(newNum){
x = newNum;
}
}
var foo = new Outer();
console.log(foo.getX());
console.log(foo.x)
foo.setX(20);
console.log(foo.getX());
Outer() 함수에 정의된 변수 x는 캡슐화 되어있습니다.
객체지향 프로그래밍 언어(자바)와 대응 시켜보면 아래와 같이 선언된 것으로 생각할 수 있습니다.
private int x = 10;
그래서 getter를 통해 x 값을 얻을 수 있으며, setter를 통해 x 값을 설정할 수 있도록 했습니다.
만약 직접 x에 접근하면, undefined를 반환합니다.
이를 미루어 보아 클로저는 단순히 생성 시점 유효 범위의 환경을 순간 포착하는 것 뿐만 아니라,
외부에는 노출시키지 않으면서 선언 당시 유효 범위의 접근을 가능하게 하고 상태를 수정할 수 있게 해주는 정보 은닉 수단으로 활용 할 수도 있습니다.
4. 클로저로 인해 발생할 수 있는 문제
비동기로 동작하는 함수를 사용하는 함수 내에서 반복문을 작성할 때, 클로저로 인해 문제가 발생할 수 있습니다.
예제 1
다음은 1,2,3 ... 9를 정상적으로 출력하는 예제입니다.
function count() {
for (var i = 1; i < 10; i++) {
console.log(i);
}
}
count();
결과는 1,2,3,4,5,6,7,8,9 가 정상적으로 출력이 됩니다.
예제2
하지만 비동기로 작성된 다음 예제는 어떨까요?
function count() {
for (var i = 1; i < 10; i++) {
setTimeout(function(){
console.log(i);
}, 1000);
}
}
count();
setTimeout() 함수는 비동기 함수입니다.
즉, 시간이 만료 했다는 이벤트가 발생하면 첫 번째 인자로 전달된 함수( 콜백 함수 )가 실행이 됩니다.
count() 함수를 호출하면 반복문을 총 9번 수행하는데, 반복문을 수행할 때마다 변수 i를 공유하고 있습니다.
- i === 1 일 때 1초 뒤에 console.log(i)를 수행합니다.
- 이어서 i === 2가 되고 역시 1초 뒤에 console.log(i)를 수행합니다.
- 이어서 i === 3이 되고 역시 1초 뒤에 console.log(i)를 수행합니다.
- ... (반복) ...
- 이어서 i === 9이 되고 역시 1초 뒤에 console.log(i)를 수행합니다.
컴퓨터는 연산 속도가 엄청나므로 setTimeout() 함수를 호출하는 반복문 9번을 수행하는데 1초가 안걸립니다.
그래서 처음 i === 1일 때 1초 뒤에 호출하려고 했던 console.log(i)가 실행되기 전에 i === 10이 된 상태입니다.
다시 말하면, 처음에 출력 되는 값은 1이 될 것이라 기대하지만 1초가 되기 전에 i는 한참 전에 10이 된 상태로 기다리고 있습니다.
따라서 결과는 10을 9번 출력하게 되는 것입니다.
그 이유는 반복문을 수행할 때 클로저에서 같은 변수를 공유하고 있기 때문입니다.
이에 대한 해결책으로는 두 가지가 있습니다.
해결책(1) - 즉시 실행함수
function count() {
for (var i = 1; i < 10; i++) {
(function(count){
setTimeout(function(){
console.log(count);
}, 1000);
})(i);
}
}
count();
즉시 실행 함수를 수행하면, 반복문의 단계가 수행할 때마다 i 값을 인자로 넘겨주기 때문에 해당 값을 출력하게 됩니다.
해결책(2) - 블록 스코프 ( let )
function count() {
for (let i = 1; i < 10; i++) {
setTimeout(function(){
console.log(i);
}, 1000);
}
}
count();
for 문에서 변수 선언을 var가 아닌 let 키워드를 사용합니다.
let은 블록 스코프( block scope )이기 때문에, 반복문의 각 단계가 같은 변수 i를 공유하고 있지 않습니다.
요즘은 적어도 ES6 버전 이상으로 개발을 많이 하기 때문에, let을 많이 사용하죠.
비동기로 작성되는 node.js 프로그래밍 할 때 var을 사용면, 위와 같은 현상이 일어날 수 있으니 주의하시길 바랍니다.
이상으로 클로저 개념에 대해 마치도록 하겠습니다.
클로저란 "함수와 그 함수가 만들어진 환경이며 캡슐화를 수행할 수 있다."로 기억하시면 좋을 것 같습니다.
[ 참고 ]
http://beomy.tistory.com/8