자바스크립트 클로져(Javascript Closure)

클로져란 함수가 그 함수의 Lexical Scope를 벗어난 다른 곳에서 호출이 되어도, Lexical Scope를 기억하고 있는 자바스크립트 함수의 특성을 말합니다. 바꿔말하면, 클로져란 함수를 다른 곳에 전달하거나 혹은 return을 하든지 상관하지 않고, 함수가 참조하고 있는 lexical scope의 변수를 기억하고 접근할 수 있는 기능을 말합니다.


내부함수를 callback으로 전달한 경우

아래의 코드는 foo함수의 내부함수 baz를 global 영역의 bam 함수에 argument로 전달했습니다. bam 함수는 baz 함수를 lexical scope를 벗어난 곳에서 호출했지만, baz 함수가 참조하고 있는 foo 함수의 내부 변수 bar에 접근이 가능한 것을 확인할 수 있습니다. 글보다 아래의 코드를 살펴보며 클로져를 음미해 보도록 하겠습니다.

function foo() {
  var bar = 'bar';

  function baz() {
    console.log(bar);
  }

  bam(baz);
}

function bam(baz) {
  baz();
}

foo();

함수를 return하는 경우

자바스크립트에는 가비지 컬렉터가 있습니다. 자바스크립트는 프로그래머들이 일일이 메모리 할당을 하는 수고를 덜어주기 위해, 값을 선언할 때 메모리를 자동으로 할당합니다. 자바스크립트의 가비지 컬렉터는 메모리 할당을 추적하고, 할당된 메모리가 필요 없으면 할당을 해제하는 작업을 합니다.

일반적으로 함수가 return이 되면, 가비지 컬렉터에 의해 내부 변수가 정리됩니다. 하지만 아래의 경우 클로저가 생성되어 showName 함수의 Lexical Scope 밖에서도 내부 변수에 접근할 수 있습니다.

function foo() {
  var bar = "bar";

  return function() {
    console.log(bar);
  };
};

function bam() {
  foo()(); // ---> "bar"
};

bam();

위의 bam 함수 내부에서 foo 함수가 한 번 호출되고, 다른 함수를 리턴합니다. foo함수가 리턴되었으니 foo의 scope는 가비지 컬렉터에 정리되었다고 생각할 수 있습니다. 하지만, 클로져에 의해 정리가 되지 않습니다. 리턴된 함수가 foo의 내부 변수를 참조하고 있어, 가비지 컬렉터에 의해 정리되지 않습니다.

var global_var = "Hello";

function showName(firstName) {
  var nameIntro = "Your name is ";

  function makeFullName(lastName) {
    return nameIntro + firstName + " " + lastName;
  }

  return makeFullName;
};

var innerfunc = showName("Michael");
innerfunc("Jackson"); // "Your name is Michael Jackson."

위의 코드에서 내부함수 makeFullName이 외부 함수의 parameter를 참조하고 있는 걸 확인할 수 있습니다. 이처럼 내부 함수는 외부 함수의 parameter 역시 접근할 수 있습니다.


setTimeout

또 다른 예로 setTimeout에 대해 살펴보겠습니다. setTimeout도 클로져를 이용하는 경우가 많습니다. foo라는 함수가 종료된 후 변수 bar가 콘솔창에 출력이 됩니다. setTimeout의 callback이 변수 bar까지 에워싸고 있습니다. (close over the variable bar)

function foo() {
  var bar = 'bar';

  setTimeout(function() {
    console.log(bar);
  });
}

foo();

Event Handler

아래의 click handler 역시 closure가 적용된 예입니다. (It closes ove the variable bar.)

function foo() {
  var bar = "bar";

  $("#btn").click(function(evt) {
    console.log(bar);
  });
}

foo();

클로저는 함수가 선언되는 순간, 스코프 밖 외부변수를 참조하고 있다면, 함수가 실행될 때를 대비해 그 외부 변수를 참조할 수 있게 해주는 우리의 친구입니다. 하지만, 세상만사가 그렇듯 클로져 역시 주의할 점이 있습니다.

저는 setTimeout과 같이 클로져로 명확하게 인지하지 못 하고 사용한 경험이 있습니다. 하지만 더 좋은 프로그래밍을 하기 위해 자신의 코드가 클로져인지 아닌지, 그리고 왜 클로져를 사용하고 있는지 명확하게 아는 것이 중요하다고 해요.

왜 중요한지를 설명하기에 앞서 아래의 코드를 살펴보겠습니다.


아래의 코드는 같은 변수를 closing over하고 있는 두 함수가 있습니다. 두 clusure가 스냅샷을 찍듯 변수 'var bar = 0'를 기억하고 있다고 생각할 수도 있습니다. 하지만 그렇지 않습니다. 아래 코드를 실행하면 0과 1이 차례로 콘솔창에 출력됩니다.

function foo() {
  var bar = 0;

  setTimeout(function() {
    console.log(bar++);
  }, 100);

  setTimeout(function() {
    console.log(bar++);
  }, 200);
};

foo(); // 0, 1 이 출력됨.

클로져는 여러 Scope에 걸쳐 형성될 수도 있습니다. 클로져는 참조하고 있는 변수가 있는 한 여러 Scope를 간직합니다. 클로져는 함수가 존재하는 한 그러한 변수에 접근할 수 있도록 합니다.

function foo() {
  var bar = 0;

  setTimeout(function() {
    var baz = 1;

    console.log(bar++);

    setTimeout(function() {
      console.log(bar+baz);
    }, 200);
  }, 100);
};

foo(); // 0 2

만약 우리가 작성한 코드의 한 Scope에 많은 변수가 있고, 모든 함수가 임무를 다해 garbage collector에 의해 정리가 되었다고 가정해 봅시다. 히자만, 변수를 참조하고 있는 함수 한 개가 event handler에 걸쳐 있다면 위의 변수들은 가비지 컬렉터에 정리가 되지 않습니다.

다시 말하면, 이것은 우리가 실수로 클로져를 만들고 클로져를 인지하지 못 하면, 불필요한 데이터가 가비지 컬렉터에 의해서 정리가 되지 않는 경우가 발생할 수 있습니다.

따라서, 우리는 클로져를 정확히 이해하고 의도하지 않은 클로져는 생성되지 않도록 하며 클로져를 사용해야 합니다.