2019. 06. 01 수정


1. 클로저(Closure)란?

클로저란 이미 생명 주기가 끝난 외부 함수의 변수를 참조하는 함수를 클로저라고 합니다.



예를 들어, outer() 함수가 선언될 당시에 그 내부에는 x라는 변수와 inner()함수를 정의하고 있고 outer() 함수는 inner 함수를 반환합니다.

만약 outer() 함수 외부에서 outer() 함수를 호출하면, 다음과 같은 실행 순서를 따릅니다.

  1. inner 함수가 반환되어 inner() 함수 실행
  2. 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)

위의 코드는 앞에서 살펴봤던 예제와 비슷하며, 동작 과정은 다음과 같습니다.

  1. outer() 함수 내부에서 변수 x값을 10으로 할당합니다.
  2. inner() 함수에서 x를 증가시킵니다.
  3. 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() 함수를 선언할 때 외부에 존재하는 변수 xouter() 함수의 유효한 범위가 아니였기 때문에 독립적인 값을 갖게 됩니다.

따라서 결과는 -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를 공유하고 있습니다.

  1. i === 1 일 때 1초 뒤에 console.log(i)를 수행합니다.
  2. 이어서 i === 2가 되고 역시 1초 뒤에 console.log(i)를 수행합니다.
  3. 이어서 i === 3이 되고 역시 1초 뒤에 console.log(i)를 수행합니다.
  4. ... (반복) ...
  5. 이어서 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