티스토리 뷰


* C나 자바를 접하던 사람들이 처음으로 자바스크립트를 접하면 혼란스러워하는 것이 바로 scope와 this의 상이함일 것이다. 처음에 접할 때에는 객체지향 언어에서는 이해할 수 없는 동작들을 하고 있기 때문에 이것이 뭔가 싶다가도 자바스크립트가 이상하다고 스스로 판정을 내리게 된다. 하지만 이것들은 자바스크립트의 원리만 이해하면 아주아주 쉽고, 오히려 객체지향 언어보다 놀라운 유연함에 감탄을 하게 될 것이고, 자바스크립트를 하다가 다시 C나 자바를 하게 되면, '자바스크립트라면 쉽게 해결할 수 있는데..'라며 자바스크립트를 아쉬워하게 될 것이다. 그럼 이번에는 일단 자바스크립트의 가장 '기본'인 scope와 closure에 대해서 알아보자.



- 이전 강좌

2012/12/10 - [속깊은 자바스크립트 강좌] 시작 (예고편)


* Scope?

: Scope라 하면, 현재 접근 가능한 변수들이 결정되는 방법이다. 영어의 뜻을 가져와서 설명해보면, 현재 자신의 위치에서 볼 수 있는 변수들을 결정하는 방법인 것이다. 자신의 scope 안에 있다면 접근이 가능하여 변수를 읽거나 쓸 수 있는 것이고, scope 밖에라면 해당하는 변수는 접근이 불가능한 것이다. 간단하게 생각한다면 너무나 쉬운 내용이지만 왜 기존의 '능수능란한' 프로그래머들도 쉽게 이해하지 못할 함정에 빠지게 되는 것일까? 


: 아래는 정확하게 Scope 때문에 일어난 일은 아니지만, C와 자바의 프로그래머들이 어떻게 해결해야할지 가장 이해하지 못하는 상황이다. <div>가 0,1,2가 있고, 각 div에 클릭 이벤트를 넣어서 0,1,2를 출력하는 아주 간단한 프로그램을 짜고자 한다. div를 쉽게 늘릴 수 있게 해주기 위해 능숙하게 for loop을 이용해서 이벤트를 부여해줘야겠다고 생각한다. 그래서 아래와 같이 짰다.

<div id="div0">Click me! DIV 0</div>
<div id="div1">Click me! DIV 1</div>
<div id="div2">Click me! DIV 2</div>
<script>
    var i, len = 3;
    for (i = 0; i < len; i++) {       //#1
        document.getElementById("div" + i).addEventListener("click", function () {  //#2
            alert("You clicked div #" + i); //#3
        }, false);
    }
</script>



: 그 구현 결과는 아래와 같다. DIV 0, DIV 1, DIV 2를 눌러보자.


Click me! DIV 0
Click me! DIV 1
Click me! DIV 2


: 위와 같이 능숙하게 짰건만 구현하고나서 위의 DIV들을 각각 눌러보면 이상하게 "You clicked div #3"만 나온다. 왜 이렇게 나오는 것일까? 문제는, scope가 어떻게 결정되느냐가 아니라, scope가 생성되는 방법과 scope가 유지되는 방법인 것이다. click 이벤트에 대하여 콜백 함수를 작성하여 alert를 시켜주는 함수의 scope는 콜백함수로 선언될 때 #2에서 생성되며, 그 scope는 #1의 변수들에 대하여 접근 가능하기 때문에 해당하는 scope를 유지하게 된다. 나중에 div위에 click 이벤트가 발생해서 실제로 #3이 호출 될때 i는 여전히 #1에 대한 scope가 그대로 살아있어서 클릭된 시점의 i 값을 가져오게 된다. 간단하게 그림으로 설명하면 아래와 같다.


: 위의 for루프를 돌 때에는 scope가 생성되지 않고 i는 기본적으로 global scope에 존재하게 되고, addEventListener에 함수를 첨부할 때 익명 함수가 선언이 될 때 scope가 생성되어 참고를 하게 된다. 그리고 2번째, 3번째 루프를 돌면서 div2의 클릭 이벤트의 콜백함수를 설정하고 나면 scope는 아래와 같이 된다.



: 각 div의 click 이벤트에 부착되었던 콜백 함수들을 모두다 같은 scope의 변수 i를 보게 되었고, 3번째 루프를 다 돌고나서 마지막에 i++이 되고나면 최종적으로 위의 함수들은 공통적으로 global scope에 있는 i=3의 i를 보게 되는 것이다. 그리고 나중에 클릭 이벤트가 일어나서 클릭을 하게 된다면 모두다 똑같이 "You clicked div #3"을 출력하게 된다.


: 이러한 현상은 자바스크립트에서 scope가 함수로 인해 생성되고 함수가 호출될 때에도 계속 지속되는 특성에 의해 생긴 문제이다. 자바스크립트에서 이는 자주 발생하는 문제로 이 개념만 쉽게 이해하고 있다면 기본은 끝냈다고 생각해도 된다. 그럼 위의 문제를 해결하기 위해 먼저 scope가 생성되는 방법에 대해 살펴보자.



* scope의 생성

: scope의 생성은 특정 구문이 실행될 때나 객체들이 생성될 때 새롭게 scope가 하나 생성하게 된다. 그 구문들은 다음과 같다.

  • function
  • with
  • catch

: 이들이 scope를 생성하게 되는 방법은 각각 다르지만, 중요한 것은 이런 객체들이 생성될 때에만 scope가 생성되고 {} 의 블럭이 생성된다고해서 scope이 하나 생성되는 것이 아니다. for 루프를 적용해보면 바로 차이를 느낄 수 있을 것이다. 아래의 소스코드는 실생활에서 사용할 일이 없지만 scope를 이해하는데 약간은 도움이 될 것이다.


* 목적: 수를 0~9까지 더하다가 총합이 16를 넘는 숫자를 구하고 싶다.

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


: 위의 소스코드에서 #1은 자바스크립트에서 자주 사용하는 표현에 한정됐지만, #2의 last는 C나 자바에서도 자주 사용하는 표현일 것이다. 특정 객체의 index를 찾는 등을 할 때 자주 이용하는데 for 루프 안에 var last로 선언되어있다. C나 자바에서라면 #3에서 당연히 에러가 나야겠지만, 자바스크립트에서는 잘 돌아간다. 이것은 블럭{}을 사용할 때에는 scope이 생성되지 않는다는 뜻이다. 반면 function의 안에 있는 값들은 접근이 불가능하다.

function foo() {
    var b = "access me";
}
typeof b === 'undefined';


: 이렇게 scope가 생기는 것은 다른 언어와 같기 때문에 당연한 것이다. 하지만 with와 catch는 조금 다르다. function은 블럭{} 안에 있는 모든 내용이 새로운 내부 scope에 포함되지만, with와 catch는 괄호() 안에 인자로 받는 변수들만 새로운 내부 scope에 포함되어 이후에 따르는 블럭{}에서만 접근이 가능하다.

try {
    throw new exception("fake exception");
} catch (err) {
    var test = "can you see me";
    console.log(err instanceof ReferenceError === true);
}
console.log(test === "can you see me");
console.log(typeof err === 'undefined');


: 위와 같은 경우 외부에서 test는 일반 블럭처럼 접근이 가능하지만 catch의 인자로 들어온 err에는 접근을 할 수가 없다. with도 마찬가지이다.

with ({test: "You can't see me"}) {
    var notScope = "but you can see me";
    console.log("Inside: " + (test === "You can't see me"));
}
console.log(typeof test === 'undefined');
console.log(notScope === "but you can see me");


: function과는 또 다른 독특한 scope를 가지고 있다. 그렇다면 이것만 가지고 위의 클릭 이벤트에서 일어나는 오류를 해결할 수 있을까?


: 잠시 다른 이야기지만, 자바스크립트를 오래한 사람들은 머리에 수없이 박히게 들어왔을, 왠만해서는 정말 어쩔 수 없는 경우가 아니라면사용하지 말아야할 2가지 함수가 있다.

    • eval
    • with

: 자바스크립트의 활성화에 가장 큰 기여를 한 Douglas Crockford가 말한 명언이 있다.


"eval is evil"


: 이미 eval을 사용하는 것은 JSON.parse() 기능이 나오고난 이후에 퍼포먼스상, 보안상 단점만 있는 기능이 되어버렸다. 그리고 그와 동급으로 with는 처음부터 없는 듯이 사는 것이 좋을 때도 있다고 언급하고 있다. 이미 자바스크립트를 많이 해왔던 사람들 중에서도 with가 무엇을 하는 구문인지 모르는 사람들도 많겠지만 위의 클릭 이벤트 문제에서는 이렇게 천대받던 with도 뭔가 한가지 역할을 찾을 수 있게 된다. 만약 with를 쓴다면 이것이 with문을 거의 유일하게 효율적으로 이용할 수 있는 방법이 아닐까 싶다.


<div id="divWith0">Click me! DIV 0</div>
<div id="divWith1">Click me! DIV 1</div>
<div id="divWith2">Click me! DIV 2</div>
<script>
    var i, len = 3;
    for (i = 0; i < len; i++) {
        with ({num: i}) {
            document.getElementById("divWith" + num).addEventListener("click", function () {
                alert("You clicked div #" + num);
            }, false);
        }
    }
</script>

: 위의 구현 결과는 아래와 같다.


Click me! DIV 0
Click me! DIV 1
Click me! DIV 2



: with를 하나 추가해줬을 뿐인데 뭐가 달라진 것일까? 이건 단순히 scope가 하나 생겼다고해서 이해되는 현상이 아니고 자바스크립트의 비동기적인 콜백 함수의 특성과 scope의 지속성이 합쳐진 결과이다. 이렇게 with를 이용해서 해결할 수도 있지만 with는 변수 사용에 있어서 혼란을 가져오고 있기 때문에 그래도 사용을 비추천한다. 다른 해결 방법은 아래에서 설명하고 그래도 with로 문제가 해결은 되었기 때문에 일단 위의 소스에서 with가 어떻게 문제를 해결하는지 살펴보자.



: 먼저 with는 괄호() 안에 새로운 scope를 만들게 된다. 그리고 num의 값에 i의 값을 부여하고 click 이벤트 콜백 함수를 선언한다. 이제 click 이벤트 콜백 함수는 with의 num을 보며 고정된 num의 값 0을 보게 되는 것이다. 이어서 다음 루프에서 scope가 생성되는 것을 다시 살펴보면 아래와 같다. 이전에 div0의 click 이벤트 콜백 함수가 보는 scope는 유지되고, 새롭게 with의 scope가 생성되면서 num을 i의 값인 1로 초기화를 시킨다. div1의 click 이벤트의 콜백함수는 이 scope의 num을 이용하게 되는 것이고, 3번째 루프에서도 똑같은 방식으로 div2의 click 이벤트 콜백도 만들어지게 된다. 결국 오른쪽 그림과 같이 scope가 형성되어 div에서 click 이벤트가 일어나 콜백 함수를 호출 할 때에는 맞게 값이 출력하게 된다.


 

 

: 위와 같이 scope가 생성되는 것 뿐만 아니라 함께 scope에서 변수값을 지속시키고 유지 시키는 특성이 이러한 문제를 해결할 수 있게 도와준 것이다. 그럼 scope의 지속성에 대해서 조금 더 공부해보자.



* scope의 지속성

: 사실 scope의 생성되는 방식이 기존의 언어와 가장 다른 점은 아니다. 하지만 scope가 지속되는 것은 다른 언어와는 다른 자바스크립트만의 강점 중 하나이다. 이러한 지속성이 자바스크립트에서 필요한 이유는     바로 자바스크립트에서 새로운 scope가 생성되는 '함수'를 변수에 넣을수도 있고, 다른 함수의 인자로 넘겨줄수도 있으며, 함수의 return 값으로도 활용할 수 있기 때문에 필요했던 개념이다. 즉, 지금 함수가 선언된 곳이 아닌 전혀 다른 곳에서도 함수가 호출될 수 있기 때문에, 그 함수의 scope는 지속될 필요가 있었던 것이다. 그럼 간단하게 그 지속성을 이해하기 위해 위의 클릭 이벤트 문제를 또 다른 방식으로 해결해보겠다.


<div id="divScope0">Click me! DIV 0</div>
<div id="divScope1">Click me! DIV 1</div>
<div id="divScope2">Click me! DIV 2</div>
<script>
    function setDivClick(index) {
        document.getElementById("divScope" + index).addEventListener("click", function () {  // #1
            alert("You clicked div #" + index);
        }, false);
    }
    var i, len = 3;
    for (i = 0; i < len; i++) {
        setDivClick(i);
    }

</script>

: 이번에는 굳이 자바스크립트 언어가 아니더래도 이해하기 쉬울 것이다. 하지만 비동기적으로 호출되는 자바스크립트의 특성에서는 중요하게 생각해야할 개념이다. 일단 위의 구현 결과를 아래에서 살펴보자. 각 DIV를 누르면 누른 번호가 맞게 출력된다.


Click me! DIV 0
Click me! DIV 1
Click me! DIV 2


: 위의 with와는 똑같은 개념으로 scope가 생성되고 지속되기 때문에 그림은 비슷하게 나온다. 하지만 다른점이 있다면, with는 with의 내부에서 with의 특성을 따라가게 되고 scope가 완전히 분리된것이 아닌 global scope와 반쯤 섞여있는 형태를 취하고 있는 반면, function으로 구현한 경우 정말로 별도의 scope를 생성하게 된다.

 

: 소스상 with를 쓰는 것이 간편하기 때문에 이렇게 간단한 경우에'만' 사용할 것이고, 그 외에 조금이라도 복잡해지거나 다른 사람들과 협업을 한다면 function으로 분리할 것을 추천한다. 왜냐하면 자바스크립트를 자주 사용하는 사람이더래도 with가 들어가게 되면 그 용법에 대해서 다시 한번 고민을 하고 검색까지 해봐야할지도 모르기 때문에 모두가 이해할 수 있는 쉬운 용법이 있으니 협업에서는 그것을 활용하는 것이 맞는 것이다. 하지만 위와 같이 함수를 따로 뽑아내는것도 귀찮고 일이다. 그럼 이것을 위의 with처럼 간단하게 처리하고 싶을 때에는 closure와 익명 함수의 조합을 이용하면 된다.

<div id="divScope0">Click me! DIV 0</div>
<div id="divScope1">Click me! DIV 1</div>
<div id="divScope2">Click me! DIV 2</div>
<script>
    var i, len = 3;
    for (i = 0; i < len; i++) {
        document.getElementById("divScope" + i).addEventListener("click", (function (index) {   // #1
            return function () {  // #2
                alert("You clicked div #" + index);
            };
        }(i)), false);  //#3
    }

</script>

: 위의 소스가 이해된다면 자바스크립트를 어느 정도 봐온 사람일 것이다. 일단 with랑 비슷하게 생각하면 되지만, with가 아닌 새로운 function을 통해 새로운 scope를 생성하는 것이다. 이러한 기법은 자바스크립트에서 아주 많이 사용되고 있고, 응용 범위도 아주 넓어 익혀두면 유용한 기법이다. 일단 어떻게 돌아가는 것인지 간단하게 설명을 한다면, #1에 선언된 익명 함수는 인자를 index를 받는것인데, 이 인자의 값은 #3에 있는 (i)의 값이 index로 들어오게 된다. 이는 함수가 변수임을 다시 생각하고 보면 쉽다. 즉 아래와 비슷하게 생각하면 된다.

var func = function (index) { /* 생략 */};    //#4
var returnValue = func(i);    //#5
returnValue = (function (index){ /* 생략 */}(i));    //#6

 

: 위의 #4와 #5를 하나로 합치게 되면 #6이 되는 것이다. 이것을 위의 #1~#3까지 표현한것이라고 보면 된다. 이것에 대한 것은 기본적으로 이해하고 넘어가길 바란다. 자바스크립트 라이브러리의 가장 기본적인 활용 패턴 중 하나이기 때문이고, 이 개념을 이해하고 응용할 줄 아는 것은 자바스크립트 프로그래머의 기본이다.

 

: 이제 내부에서 #2는 현재 호출된 함수의 return값으로 익명 함수를 하나 또 return한다. 이건 또 무슨 뜻인지 처음에 보면 헷갈리겠지만, 위의 #6번에서 returnValue의 안에 함수가 들어가는 것으로 이해하면 된다. 즉, 익명 함수의 리턴값으로 함수가 반환되고 그 반환된 함수가 addEventListener의 2번째 인자로서 들어가게 되는 것이다. 이 개념 또한 아주 기본적인 개념이니까 이해하고 넘어가길 바란다.

 

: 이렇게 간단하게 closure를 이용하는 방법이 있는 반면, closure자체는 엄청난 응용이 가능하기 때문에 closure에 대한 내용은 나중에 closure의 활용 방법에 대해서 자세하게 다시 한번 더 다룰 것이다.

 

 

* 정리

- scope는 다음의 명령어들을 호출할 때 새로 생성하게 되고, 일반적인 for, switch 등의 블럭{}에 의해서 구분되지 않는다.

    • function
    • with
    • catch

- scope는 비동기 함수가 호출될 때까지 계속해서 지속되어 참고된다. 이를 새로운 scope를 생성함으로써 비동기적으로 호출 될 때의 scope를 조율할 수 있다.

 

* 덧

: 위에서 언급했지만 여기서 잠깐 다룬 scope와 closure는 자바스크립트에서 가장 기본이 되는 내용이다. 이 내용을 모르고는 자바스크립트를 자바스크립트답게 사용할 수 없기 때문에 위의 간단한 예들이 어떻게 돌아가는 것인지 천천히 이해하고 넘어가는 것이 좋다. 많은 강좌들에서는 문법들을 다루고 closure를 가장 뒤에서 소개하고 있지만, 이 내용은 진정 자바스크립트의 가장 기초가 되는 개념이고 이것을 이해해야 다양한 자바스크립트만의 기법들이 활용될 수 있기 때문에 기본이라고 말한 것이다.


: 그럼 다음에는 this가 결정되는 방법과 function에 대해서 공부해보도록 하자. 자바스크립트를 자바스크립트답게 만드는 것이 바로 이 function이므로 이에 대해서도 가장 먼저, 사실 scope보다도 먼저, 공부를 해야할 필요가 있다.

 

 

자바스크립트의 Scope와 Closure 기초 끝.

 

- 다음 편

2013/01/07 - [속깊은 자바스크립트 강좌] function declaration vs function expression 차이점

2013/01/10 - [속깊은 자바스크립트 강좌] 함수를 호출하는 방법과 this의 이해

2013/01/21 - [속깊은 자바스크립트 강좌] Closure의 이해 / 오버로딩 구현하기

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

2013/02/13 - [속깊은 자바스크립트 강좌] 쉬어가기: 웹 개발 방법론의 변화/자바스크립트의 재발견

2013/02/22 - [속깊은 자바스크립트 강좌] 객체지향의 기본: prototype

2013/10/04 - [속깊은 자바스크립트 강좌] 상속, new와 Object.create의 차이

2013/11/06 - [속깊은 자바스크립트 강좌] 변수 선언 방법에 대하여

2016/11/13 - [속깊은 자바스크립트 강좌] 마무리(는 책으로!)


 


공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
«   2024/04   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30
글 보관함