티스토리 뷰

Javascript - 속깊은 자바스크립트[2. 자바스크립트의 스코프와 클로저]


| 시작하기에 앞서


es6가 도입된 이후에, let 선언을 통해 block scope 단위의 변수 할당이 가능해졌다곤 알고 있었지만, 자바스크립트에서는 어떤 시점에 scope가 생성 되는지에 대한 이해가 충분히 안된 상태였다. 그리고 c 프로그래밍을 자주 접했던 터라, block scope에 익숙해져 있어 자바스크립트로 구현할때 scope와 관련해 많은 어려움이 있었어서, 이에 대해 학습하고 정리하려 한다.


| 1.코프란?


스코프란 현재 접근할 수 있는 변수들의 범위를 뜻한다. 다른 프로그래밍 언어를 사용해봤으면 쉬운 내용이지만, 자바스크립트의 특별한 스코프 개념때문에 여러 어려움이 있다. 아래 예제를 살펴보자.


<html>
<body>
   <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++) {
           document.getElementById("div" + i).addEventListener("click", function (){
               alert("You clicked div #" + i);
          }, false);
      }
   </script>
</body>
</html>

위의 출력값은 모두 you clicked div #3으로 나오게 된다. 그이유가 뭘까?
이문제는 정확하게 스코프 때문이라기 보다는, 스코프가 생성되고 유지되는 방법 때문에 생기는 것이다.
for루프를 톨때는 별도의 스코프가 생성되지 않고, i는 글로벌 스코프에 존재하게된다. 각 div 클릭 이벤트에 설정되었던 콜백함수들은 모두 같은 스코프의 변수인 i를 참조한다. 그리고 3번째 루프를 돌고 마지막에 i++를 실행해 최종적으로 콜백함수들은 3인 i를 참조한다. 따라서 모두 you clicked div #3 를 출력하게 되는것이다. 이와 같은 현상을 스코프 체인이라고 설명하기도 한다. 말그대로 스코프들이 연결되어 있다는 것이다.

> 1.1) 스코프의 생성


자바스크립트는 다른 언어와 달리 일반적인 블록 스코프를 가지지 않는다. 자바스크립트의 스코프는 특정 구문이 실행될때 새로 생성하여 위의 예제와 같은 스코프 체인을 생성하게 된다. 이렇게 스코프를 생성하는 구문들은 아래와 같다.

  • function
  • with
  • catch
자바 스크립트에서 이들의 사용법은 각각 다르지만, 중요한 것은 이런 구문들이 사용될때만 스코프가 생성되고, 다른 언어처럼 {}를 이용해 블록을 생성한다고해서 새로운 스코프가 생성되는 것이 아니라는 점이다.


function 구문의 스코프 생성


function foo(){
 var b = "can you access me?"
}
console.log(typof b === "undefined")

위의 결과 값은 true이다. 그이유는 foo 함수만의 scope 안에 있는 변수 b에 접근하려 했기 때문이다.

catch 구문의 스코프 생성


나머지 with과 catch 구문도 스코프를 생성하기는 하지만 function과는 다르게, 이 두구문은 괄호 안에 인자로 받는 변수들만 새로운 내부 스코프에 포함되어 그다음으로 오는 블록안에서만 접근할 수 있다. 반면 블록안에서 새로 정의한 변수들은 for-loop와 비슷하게 블록 외부에서도 접근할 수 있다.

<script>
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");
</script>

따라서 위의 결과 값은 모두 true이다.


with 구문의 스코프 생성


일단 with 구문은 자바스크립트를 오래 한 사람도 어떠한 용도로 사용하는지 모를 수 있다고 한다. 왜냐하면 with 구문은 eval과 함께 자바스크립트 개발자 사이에서 사용하지 말아야 할 구문 중 하나라고 한다. 일단 스코프를 생성하는 구문이니 예제를 살펴보자.

<html>
<body>
   <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>
</body>
</html>

위와 같이 with 구문을 활용하면 괄호안에 있는 파라미터를 활용해 새로운 스코프를 만들어 i값이 변화할때마다 새로운 스코프를 만들어서, 맨처음 소개했던 예제와 달리 you clicked div 1~3의 결과값을 출력할 수 있게 된다.
여기까지만 봐서는 왜 with 구문을 사용하지 말아야 할지 잘 모를수있는데 그에 대해서는 뒤에서 살펴보고 이제 스코프의 지속성에 대해서 살펴보며 더 알아보자.
> 1.2) 스코프의 지속성
엄밀하게 따지면 스코프가 생성되는 방식이 기존언어와는 다르지 않다고 한다. 하지만 스코프가 지속되는 것은 자바스크립트의 큰 장점 중 하나라고 한다. 그 이유는 새로운 스코프가 생성되고 스코프 체인을 참조하는 함수를 변수에도 넣을 수 있고, 다른 함수의 인자로 넘겨줄 수도 있으며 함수의 반환값으로도 활용할 수 있기 때문이라고 한다.
즉, 함수가 선언된 곳이 아닌 전혀 다른곳에서 함수가 호출 될 수 있어서, 해당 함수가 현재 참조하는 스코프를 지속할 필요가 없는것이다. 이러한 지속성을 이해하기 위해 앞의 클릭이벤트 문제를 아래와 같이 함수,클로저로 해결해보자.
함수를 이용한 문제해결

<html>
<body>
<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 () {
           alert("You clicked div #" + index);
      },
       false
  );
}
var i, len = 3;
for (i = 0; i < len; i++) {
   setDivClick(i);
}
</script>
</body>
</html>

앞서, function 구문으로 새로운 scope를 만들수있다고 설명 했으니 이하는 생략한다.

클로저를 이용한 문제해결


이제 중요한 개념인 클로저가 나오게 되는데, 자바스크립트의 특징 중 하나인 클로저에 대해 잠시 설명하겠다. 자바스크립트에서 매우 자주 등장하는 함수의 활용 방법으로, IIFE(Immediate Invoke Function Expression)이라고 불리는 것이 있다. 굳이 번역하면 즉시 호출 함수이다. 예전에는 그냥 바로 호출되는 함수정도로만 알고 있었는데, scope와 관련해 더 중요한 개념임을 알게 되었다. 왜냐하면 이를 활용함으로써, 스코프체인을 function으로 생성하여 그 스코프의 결과 값을 반환하는, 클로저를 활용하는것에 매우 유용하게 쓰인다. 예제를 보고 이해해보자.

<html>
<body>
<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) { return function () {
           alert("You clicked div #" + index); };
      }(i)),
       false);
}
</script>
</body>
</html>

위에서 즉시실행함수 부분을 분해해 보면 아래와 같이 설명할 수 있다. (function (index){생략}i) ) 부분
1.var func = function(index) {생략}2.var returnValue = func(i)3.returnValue = (function (index){생략}
1번에서 익명함수를 func에 넣고 2번에서 func변수에 i를 넘겨주고 3번 줄에서는 1과 2를 합쳐서 함수를 선언하고 바로 호출하는 것이다.
> 1.3) with 구문

<script>
var user = {
   name: "Sung-ihk",
   homepage: "unikys.tistory.com",
   language: "Korean"
}
with (user) {
   console.log(name === "Sung-ihk");
   console.log(homepage === "unikys.tistory.com");
   console.log(language === "Korean");
   language = "javascript";
   nickname = "unikys";
}
console.log(user.language === "javascript");
console.log(user.nickname === "unikys"); //??
</script>

위에서 중요하게 살펴봐야 할 부분은 세가지 정도라 판단된다.
1. with는 파라미터로 받은 객체를 스코프 체인에 추가해 동작한다.2. with 구문안에선 그 object가 가지고 있는 변수들을 local변수와 같이 활용가능하다.3. nickname 부분을 살펴보면, user객체에 nickname을 추가할 것처럼 보이지만, user의 속성이 아니므로, 상위의 스코프나 글로벌 영역에 할당된다. 따라서, user.nickname 과 같이 접근이 아래 콘솔에서 불가한것이다. 그래서 결과값이 false이다.
아래에선 with 구문을 실용적으로 사용할 수있는 예에 대해서 잠시 소개한다.

<html>
<body>
   <div id="myDiv">Using with</div>
   <script>
   with (document.getElementById("myDiv").style) {
       background = "yellow";
       color = "red";
       border = "1px solid black";
  }
   var r = 10, a, x, y;
   with (Math) {
       a = PI * r * r;
       x = r * cos(PI);
       y = r * sin(PI / 2);
  }
   function toString(string) {
       console.log(string);
  }
   with({nickname: "unikys"}) {
       with(window) {
           toString("Hello, " + nickname);
      }
  }
   </script>
</body>
</html>

이제 왜 with 를 자제해야 하는지 살펴보자.
> 1.4)with 구문을 자제해야하는 이유
첫번째 이유는 바로 스코프를 생성함으로써 생기는 추가 자원 소모이다. 위에서 본 nickname 예와 같이 with(user)의 스코프를 먼저 탐색하고, 그 상단의 글로벌 영역까지 탐색하는 과정도 진행되니 이를 설명 가능하다.
두번째 이유는, with 구문을 사용한 소스의 모호성이다.예로 살펴보자.

<script>
function doSomething(value, obj) {
   with (obj) {
       console.log(value);
       value = "which scope is this?";
  }
}
doSomething("value", {"value": "objValue"});
doSomething("value", {});
</script>

위에서 3번줄의 value가 어떤 value를 말하는지 모호할 수 있다. 2번줄에 있는 함수의 인자로 받는 value인지, obj.value 인지 말이다. 따라서 다른 사람이 짜놓은 소스면 굉장히 헷갈릴 수 있다.
세번째는 이러한 상황을 자제하라고 권하고, es6 표준에서 with 구문에서 아예 제외시켰다고 한다.
마치며
원래 클로저까지 포스팅 하려했으나, 생각보다 내용이 많아져서 다음 글에서 포스팅하겠다. 스코프를 생성하는 구문이 따로 정해져있다는 것이 매우 흥미로웠고, 그리고 클로저를 즉시 실행함수라고만 알고 있었는데 그게 아니라 즉시 실행함수 패턴이 클로저를 활용하는데 쓰이는것이라는 것 깨달았다. 또한 자바스크립트에서 왜 스코프가 중요한지 다시한번 깨닫게 되었다.



댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
TAG
more
«   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
글 보관함