2013년 4월 8일 월요일

Javascipt Scope & Closure

var temp = {
    a : 1,
    b : 2,
    c : this.a
};
console.log(temp.c);

결과값으로 무엇이 출력될 것이라고 생각하는가?


답은 undefined 이다.


위의 코드를 보면 무엇을 하고자 하는지 알 것이다.

객체 내부에서 자신의 프로퍼티 값을 전달하고자 하는 것이다.

정답부터 말하면, 정상적인 동작이 되기 위해서는 다음과 같이 구현해야 한다.

var temp = {
    a : 1,
    b : 2,
    c : function () { return this.a; }
};
console.log(temp.c());



얼마전까지도 Javascript 의 Scope 과 Closure 에 대한 개념없이 테스트 및 남들이 구현해 놓은 sample code 만을 이용하여, 그러려니 하고 어설픈 이해로 Closure 를 사용해 왔다.

그러다 얼마전 공부를 좀해서 이해를 하고 넘어갔다고 생각했다.

그런데 오늘 팀장님께서 딱 위의 질문을 하였는데

공부한 Scope 과 Closure 의 문제구나 하고 설명을 드리려고 했지만 자세히 생각이 나질 안았다.

아직 정확히 이해하고 있지 않은 것이었다.


Closure 란?


함수 객체와 함수의 변수가 해석되는 범위(변수 바인딩의 집합)의 조합.


함수 객체와 함수의 변수가 해석되는 범위는 Scope 을 의미한다.

그러므로 Scope를 알게 되면 Closure도 자연히 알게 될 것이다.


Scope


다음과 같은 특정 구문이 실행될 때 scope가 하나 생성된다.

  • function
  • with
  • catch

처음 이야기한 것처럼 {} 로 묶는다고 scope가 생성되는 것이 아닌것이다.

추가로 예를 들면

for (var i = 0; i < 10; i++) {
    var total = (total || 0) + i;
    var last = i;
    if (total > 16) {
        break;
    }
}
alert(total + " , " + last);

C 나 Java에서는 for 문 안에서 정의된 변수 i, total, last 는 for 문 안에서만 유효하여 마지막 라인에서 에러가 발생할 것이다.

그러나 Javascript에서는 정상적으로 실행이 잘 된다.

function func() {
    var a = "test";
}
alert(a);

위와 같이 function은 scope 생성하여 마지막 라인에서 변수 a 는 undefined 를 출력하게 된다.

Javascript의 scope을 설명할 때 가장 많이 사용하는 것이 아래 예제와 같은 이벤트 callback 등록이다.

var i, len = 3;
for (i = 0; i < len; i++) {
    document.getElementById('div' + i).addEventListener('click', function () {
        alert(i);
    }, false);
}

각 div를 click 하면 결과가 어떻할 것이라고 생각하는가?

모두 3 을 출력한다.

이를 이해하지 못한다면 scope 에대해 제대로 이해하지 못한 것이다. 

for 문은 scope 생성하지 않기에 click 할 때의 callback 안의 변수 i 는 모두 3 이 되어있기 때문인 것이다.

그렇다면 이를 어떻게 해결할 것인가?

답은 아래와 같다.

function setDivClick(index) {
    document.getElementById("div" + index).addEventListener("click", function () {
        alert(index);
    }, false);
}
var i, len = 3;
for (i = 0; i < len; i++) {
    setDivClick(i);
}

function 을 생성하여 각 i 의 값마다 scope을 새로 생성해 주는 것이다.

다른 방법들도 있겠지만 이런 방법이 가독성도 좋고 co-work 하는데 좋은 방법일 것이다.



위의 scope 자세히 표현한다면 위의 그림과 같을 것이다.

scope은 두단계로 생성되는데 이를 scope chain 이라고 한다.

여기서 중요한 것은 하위 scope에 해당하는 function이 살아있다면 상위의 scope들은 죽지 않고 계속 살아있게 된다는 것이고 이것이 closure의 가장 기본적인 개념이다.

즉, 위의 div0.onclick 이라는 함수가 살아있는 동안 setDivClick 을 통해 생성되었던 scope는 계속 살아있게 된다.

scope 이 지속되는 것은 다른 언어와는 다른 자바스크립트만의 강점 중 하나이다.

이러한 지속성이 자바스크립트에서 필요한 이유는

자바스크립트에서 새로운 scope가 생성되는 '함수'를 변수에 넣을수도 있고,

다른 함수의 인자로 넘겨줄수도 있으며,

함수의 return 값으로도 활용할 수 있기 때문에 필요했던 개념이다.

즉, 지금 함수가 선언된 곳이 아닌 전혀 다른 곳에서도 함수가 호출될 수 있기 때문에,

그 함수의 scope는 지속될 필요가 있었던 것이다.

- 예제
function makeFunc() {
    var name = "Mozilla";
    function displayName() {
        alert(name);
    }
    return displayName;
}
var myFunc = makeFunc();
myFunc();



Closure

Closure 를 설명할 때 가장 많이 드는 예제를 살펴보자.
function makeAdder(x) {
  return function(y) {
    return x + y;
  };
}

var add5 = makeAdder(5);
var add10 = makeAdder(10);
 
alert(add5(2));  // 7
alert(add10(2)); // 12

makeAdder(5) 와 makeAdder(10) 이 각각 실행되면서 각각의 scope chain이 생성되고 해당 scope는 지속 되게 된다.

다음 add5(2) 가 실행될 때는 makeAdder(5) 에서 param으로 들어온 x = 5 의 값을 이용하여 7 이라는 값을 리턴한다.

add10(2) 이 실행될 때는 makeAdder(10) 에서 param으로 들어온 x = 10 의 값을 이용하여 12 라는 값을 리턴한다. 

서로 다른 scope chain의 closure 에 의하여 위와 같은 결과가 나오게 되는 것이다.

Closure를 이용하여 여러가지 활용을 할 수 있지만 대표적인 사용방법인 private 함수 적용하는 방법을 살펴보자.

var Counter = (function() {
  var privateCounter = 0;
  function changeBy(val) {
    privateCounter += val;
  }
  return {
    increment: function() {
      changeBy(1);
    },
    decrement: function() {
      changeBy(-1);
    },
    value: function() {
      return privateCounter;
    }
  }   
})();
 
alert(Counter.value()); // 0
Counter.increment();
Counter.increment();
alert(Counter.value()); // 2
Counter.decrement();
alert(Counter.value()); // 1

위의 설명을 잘 따라왔다면 어떻게 위의 코드가 동작되는지 쉽게 이해할 것이다.

변수 privateCounter 에 대해 직접적인 접근은 허용되지 않고 changeBy() 함수 또한 외부로 노출되지 않아 내부 구현은 private 하게 숨겨 구현 할 수 있다.

 increment(), decrement(), value() 함수들은 public 함수로 동작되게 되는 것이다.


너무 많이 돌아왔다.

다시 처음에 설명한 코드를 다시 살펴보자.
var temp = {
    a : 1,
    b : 2,
    c : this.a;
};

위에서는 scope가 정의되지 않았기 때문에 this 는 window가 된다.
var temp = {
    a : 1,
    b : 2,
    c : function () { this.a; }
};

이렇게 되면 scope가 생성되어 this는 temp가 되는 것이다.
var temp = {
    a : 1,
    b : 2,
    c : function () {
            return function () { this.a; }
        }
};

추가로 그럼 이경우에는 어떤값이 나올까? 1?

undefined 가 출력된다. 

함수가 메서드로 호출될 때, 외부 함수의 this 키워드와 특수한 변수인 arguments에는 정상적인 접근이 되지 않는다.

이는 다음과 같이 변경하여 해결 할 수 있다.

var temp = {
    a : 1,
    b : 2,
    c : function () {
            var that = this;
            return function () { that.a; }
        }
};

Closure도 불필요한 곳에 사용하면 성능면에서 비효율 적이라고 한다. 

각각의 scope chain이 호출될 때마다 생겨서 scope가 계속 유지되어 메모리를 차지하고 있으며,

각각의 함수에서 변수에 접근하기 위해 scope chain을 매번 역으로 올라가면 변수를 찾기 때문에 반복적으로 사용될 때는 성능면에서 좋지 않다고 한다.

그리고 직관성이 떨어질 수 있으므로 필요할 때만 사용해야 할 것이다.


Reference


MOZILLA 개발자 네트워크 - 클로져

Nonblock - 자바스크립트의 클로저

[속깊은 자바스크립트 강좌] 자바스크립트의 Scope와 Closure 기초

[속깊은 자바스크립트 강좌] Closure 쉽게 이해하기/실용 예제 소스

댓글 1개:

  1. 중간에 SyntaxHighlighter 가 깨지는데 이유를 모르겠네 ㅡㅡ;

    답글삭제