본문 바로가기

프로그래밍/자바스크립트

자바스크립트 javascript 클로저(Clouser)



클로저(Clouser)란?


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



function outer(){

    var tt = 10;

 

    function inner(){

      tt++;

      console.log(tt)

    };

    return inner;

}





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


그리고 outer() 함수는 inner() 함수를 반환하는데, outer() 함수 외부에서 outer() 함수를 호출하면 inner 함수가 반환되어 outer() 함수에서 정의된 변수 tt를 참조해서 ++연산자를 수행합니다.


즉, outer()함수를 호출할 때 outer()함수의 유효 범위가 아님에도, inner()함수에서 outer()함수에 정의된 변수를 참조합니다.


이 때 inner()함수를 클로저라고 하며, outer()함수에 정의된 변수 tt를 자유 변수라고 합니다.


클로저라는 이름의 의미는 "자유 변수에 닫혀있다", "자유 변수에 엮여있다"는 의미입니다.




함수형 언어가 아니라면 outer() 함수 외부에서, 즉 유효 범위(scope) 밖에서 outer() 내부에 존재하는 변수나 함수에 접근할 수 없지만, javascript에서는 클로저를 통해 접근할 수 있으며 또한 값을 변경할 수도 있습니다.


왜냐하면 outer()함수가 선언될 당시의 유효한 환경을 기억하고 있다가, outer()함수를 호출할 때 기억했던 환경을 사용할 수 있기 때문입니다.





function outer(){

    var tt = 10;

 

    function inner(){

      tt++;

      console.log(tt)

    };

    return inner;

}

 

var tt = -10;

var foo = outer();

foo();

foo();

console.log(tt);

 






outer() 함수 내부에서 tt 값을 10으로 할당하고 inner() 함수에서 tt를 증가시킵니다.


outer() 함수를 정의한 뒤에는 tt 에 -10을 새롭게 할당합니다.




이 상황에서 foo() 함수를 한 번 호출하면 어떤 값이 출력될까요?


11이 출력될까요? -9가 출력될까요?




클로저를 통해 함수가 선언될 당시의 환경을 기억하고 있다가, 그 함수가 호출될 때 기억하고 있던 환경을 사용할 수 있다고 했습니다.


foo() 함수를 호출했을 때 tt는 선언될 당시의 환경인 10을 기억하고 있기 때문에 tt++의 결과


tt는 11이 됩니다.




foo() 함수를 한번 더 호출해볼까요?


이번에는 11이 출력될까요? 12가 출력될까요?


클로저를 통해 기억하고 있던 환경의 값을 변경할 수도 있다고 했습니다.


따라서 이전 foo()함수 호출결과 tt는 11이 되었으므로, tt 값이 변경되어 11을 기억하고 있습니다.


그래서 tt는 12가 됩니다.




이번에는 foo()함수를 호출하지 말고, 그냥 tt를 출력해보도록 하겠습니다.


12가 출력될까요? -10이 출력될까요?


outer() 함수 외부에 선언한 변수 tt는 outer()함수와 관련이 없습니다.


outer() 함수를 선언할 때 외부에 존재하는 변수 tt는 outer() 함수의 유효한 범위가 아니였기 때문에 독립적인 값을 갖게 됩니다.


따라서 결과는 -10이 출력됩니다.










캡슐화


자바스크립트에서 클로저를 통해 객체지향 프로그래밍에서 제공하는 private 기능으로 활용할 수도 있습니다.


해당 함수가 존재하는 하는 동안 그 함수의 유효 범위에 있는 변수와 함수를 가비지 컬렉션으로부터 보호하기 때문에 클로저는 일종의 보호막이라 볼 수 있습니다.


즉 클로저는 변수의 유효범위를 제한하려는 용도로 사용할 수 있습니다.( 캡슐화가 가능합니다.)







 function Outer(){
  var tt = 10;
 
  this.getTT = function(){
    return tt;
  }
 
  this.setTT = function(newNum){
    tt = newNum;
  }
}
 
var foo = new Outer();
console.log(foo.getTT());
console.log(foo.tt)
 
foo.setTT(20);
console.log(foo.getTT());
 


Outer() 함수에 정의된 변수 tt는 캡슐화 되어있습니다.

객체지향 프로그래밍 언어(자바)와 대응 시켜보면,

private int tt = 10;

위와 같이 선언된 것으로 생각할 수 있습니다.



그래서 getter를 통해 tt 값을 얻을 수 있으며, setter를 통해 tt 값을 설정할 수 있습니다.

하지만 직접 tt에 접근하면 undefined를 반환합니다.





이를 미루어 보아 클로저는 단순히 생성 시점 유효 범위의 환경을 순간 포착하는 것 뿐만 아니라,

외부에는 노출시키지 않으면서 선언 당시 유효 범위의 접근을 가능하게 하고 상태를 수정할 수 있게 해주는 정보 은닉 수단이라 할 수 있습니다.

 


클로저로 인해 발생할 수 있는 문제

함수 내에 반복문을 작성할 때, 클로저로 인해 문제가 발생할 수 있습니다.



다음은 정상적인 출력을 하는 예제입니다.

예제)

 function count() {
  for (var i = 1; i < 10; i++) {
    console.log(i);
  }
}
 
count();

결과는 1,2,3,4,5,6,7,8,9 가 정상적으로 출력이 됩니다.
 

비동기로 작성된 예제입니다

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를 공유하고 있지 않습니다.

 

이상으로 클로저 개념에 대해 마치도록 하겠습니다.

클로저란 " 함수와 그 함수가 만들어진 환경이며 캡슐화를 수행한다. "로 기억하시면 좋을 것 같습니다.