[JavaScript] 클로저(Closure)

클로저(Closure)

클로저란 어떤 함수에서 선언한 변수를 참조하는 내부함수를 외부로 전달할 경우, 함수의 실행 컨텍스트가 종료된 후에도 해당 변수가 사라지지 않는 현상이다.

function outer() {
  var a = 2;
  function inner() {
    console.log(a);
  }
  return inner;
}

var func = outer();
func(); // 2

여기에서 GC(Garbage Collector)가 outer()의 참조를 없앨 것 같지만 내부함수인 inner()가 해당 스코프의 변수인 a를 참조하고 있기 때문에 없애지 않는다. 따라서 스코프 외부에서 inner()가 실행되도 해당 스코프를 기억하기 때문에 2를 출력하게 된다. 즉, 여기에서 클로저는 inner()가 되고, func에 담겨 밖에서도 실행되며, 렉시컬 스코프를 기억한다.

클로저와 메모리 관리

클로저는 어떤 필요에 의해 의도적으로 함수의 지역변수를 메모리를 소모하도록 함으로써 발생한다. 그렇다면 그 필요성이 사라진 시점에는 더는 메모리를 소모하지 않게 해주면 된다. 참조 카운트를 0으로 만들면 언젠가 GC가 수거해갈 것이고, 이때 소모됐던 메모리가 회수된다. 참조 카운트를 0으로 만드려면 식별자에 참조형이 아닌 기본형 데이터(보통 null이나 undefined)를 할당하면 된다.

// (1) return에 의한 클로저의 메모리 해제
var outer = (function () {
  var a = 1;
  var inner = function () {
    return ++a;
  };
  	return inner;
})();
console.log(outer()); // 2
console.log(outer()); // 3
outer = null; // outer 식별자의 inner 함수 참조를 끊음

클로저 활용

반복문 클로저

function func() {
  for (var i = 1; i < 5; i++) {
    setTimeout(function() { console.log(i); }, i*500);
  }
}
func(); // 5 5 5 5

코드가 의도한 바는 1부터 4까지 0.5초 시간 간격을 두고 출력하는 것이었는데, 5가 4번 출력되었다. 왜 이렇게 되는 걸까?

setTimeout()을 반복문 안에서 사용하면 콜백함수가 계속해서 task queue에 쌓이게 되고, 반복문이 끝나고 나서 call stack으로 돌아와서 실행된다. 콜백함수는 클로저이기 때문에 상위 스코프에게 i의 값을 물어보고 상위 스코프인 func의 스코프에선 i가 5까지 증가했기 때문에 5가 4번 출력된다.

해결방법

위 문제를 해결하기 위해서는 2가지 방법이 있다.

  • 새로운 함수 스코프로 해결하기
function func() {
  for (var i = 1; i < 5; i++) {
    (function (j) {
      setTimeout(function() { console.log(j); }, j*500);
    })(i);
  }
}
func(); // 1 2 3 4

setTimeout()을 IIFE(Immediately Invoked Function Expression, 즉식실행함수 표현식)로 감싸게 되면, 새로운 스코프를 형성하고, 나중에 콜백함수가 j를 참조할 때, 그 시점의 i 값을 갖기 때문에 원하는 결과를 얻을 수 있게 된다.

  • 블록 스코프로 해결하기
function func() {
  for (let i = 1; i < 5; i++) {
    setTimeout(function() { console.log(i); }, i * 500);
  }
}
func(); // 1 2 3 4

함수 스코프가 아닌 블록 스코프를 갖는 let을 사용하면 for문 내의 새로운 스코프를 갖기 때문에 매 반복마다 새로운 i가 선언되고, 반복이 끝난 이후의 값으로 초기화가 된다. 따라서, setTimeout()의 클로저인 콜백함수가 i를 참조하기 위해서 상위 스코프를 검색할 때 블록 스코프에서 매 반복마다 선언 및 초기화 된 i를 참조하는 것이다.

클로저를 통한 은닉화

일반적으로 JavaScript에서 객체지향 프로그래밍을 말한다면 Prototype을 통해 객체를 다루는 것을 말한다.

Prototype을 통한 객체를 만들 때의 주요한 문제 중 하나는 Private variables에 대한 접근 권한 문제다. 예제 코드를 보자.

function Hello(name) {
  this._name = name;
}  

Hello.prototype.say = function() {
  console.log('Hello, ' + this._name);
}
  
var hello1 = new Hello('승민');
var hello2 = new Hello('현섭');
var hello3 = new Hello('유근');
  
hello1.say();
hello2.say();
hello3.say();
hello1._name = 'anonymous';
hello1.say(); // 'Hello, anonymous'

위에서 Hello()로 생성된 객체들은 모두 _name이라는 변수를 가지게 된다. 변수명 앞에 underscore(_)를 포함했기 때문에 일반적인 JavaScript 네이밍 컨벤션을 생각해 봤을 때 이 변수는 Private variable으로 쓰고 싶다는 의도를 알 수 있다. 하지만 실제로는 여전히 외부에서도 쉽게 접근가능한 변수일 뿐이다.

이 경우에 클로저를 사용하여 외부에서 변수에 접근하는 것을 제한할 수 있다.

function hello(name) {
  var _name = name;
  return function() {
    console.log('Hello ' + _name);
  };
}

var hello1 = hello('승민');
var hello2 = hello('현섭');
var hello3 = hello('유근');

hello1(); // 'Hello, 승민'
hello2(); // 'Hello, 현섭'
hello3(); // 'Hello, 유근'

특별히 인터페이스를 제공하는 것이 아니라면, 여기서는 외부에서 _name에 접근할 방법이 전혀 없다. 이렇게 은닉화도 생각보다 쉽게 해결할 수 있다.

참고

좋은 웹페이지 즐겨찾기