eval 의 모든 것!

발생일: 2013.02.25

문제:

다음 달부터 다른 팀 분들을 대상으로 작은 자바스크립트 스터디의 강사를 맡기로 했는데,

좀 걱정(?)이 된다.(무식이 탄로날까봐... -_-a ㅎㅎ)


이번을 기회 삼아,

설명하려고 하면 이것 저것 떠올라 횡설수설하게 될 것 같은 개념들을 자세히 정리해보려고 한다.


제목이 좀 거창하지만, 첫 번째로 eval()을 주제로 삼았다.



해결책:

eval(string)은 문자열로 넘어온 자바스크립트 구문을 실행하는데,

호출하는 위치와 방식에 따라 eval의 실행 컨텍스트와 범위(scope)가 달라진다.


기본적으로, eval 은 실행 시점의 함수 범위(scope)에서 실행되며,

eval()의 실행 컨텍스트(this) 또한 실행 시점의 함수의 것과 동일하다.


아래 코드를 보면 쉽게 이해할 수 있다.


  var a = {

    b: function () {

      var foo = 'foo';

      return eval('foo'); // (A)

    },

    c: function () {

      return eval('this'); // (B)

    }

  };


  a.b(); //--> 'foo'

  a === a.c(); //--> true



(A)의 eval은 a.b() 메서드 내에서 호출되며 b()와 동일한 범위를 갖기 때문에,

지역변수인 foo에 접근할 수 있다.

또한, (B)에서와 같이 this 또한 c()의 실행 컨텍스트인 a 객체를 가리킨다.


단, eval을 직접 호출하지 않는 경우,

eval은 전역 범위(global scope)에서 실행된다.


'직접 호출(direct call)'하지 않는다는 것은, eval의 참조를 이용해 호출하는 것을 의미하며,

'indirect call'이라 한다. (우리 말론 '간접 호출'이라고 해야할까...?)



아래 코드를 보면 이해할 수 있다.


  var foo = 'global_foo';


  function directEval() {

    var foo = 'local_foo';

    return eval('foo');

  }


  function indirectEval() {

    var foo = 'local_foo';

    var f = eval; // 직접 eval을 호출하지 않고 eval의 참조를 호출한다.

    return f('foo');

  }


  directEval(); //--> 'local_foo'

  indirectEval(); //--> 'global_foo'



최근의 자바스크립트 컴파일러는 성능 향상을 위해 미리 코드를 컴파일하는데,

eval()과 같이 동적으로 실행 범위가 결정되는 코드는 미리 컴파일할 수 없다.


그렇기 때문에, 함수 내에서 실행되는 eval()이 반드시 함수의 실행 범위를 가져야 하는 것이 아니라면,

eval의 참조를 이용해(indirect call) 명시적으로 전역 범위에서 실행되도록 하는 것이 좋다.



indirect call 은 위와 같이 참조를 사용해서 호출할 수도 있지만,

어떤 코드를 보면 아래와 같이 팬시한 패턴으로 호출하기도 한다.


  (0, eval)(string);


(0, eval) 의 형식이 좀 생소하지만, 이 코드는 컴마 연산자의 특징을 활용한 것이다.

'컴마 연산자'라고 하니 뭔가 새로운 것 같지만, 우리가 늘 사용하던 연산자다.


  var x, y, z;

  z = (x = 1, y = 2, x + y);

  //--> x = 1, y = 2, z = 3


컴마 연산자는, 위와 같이 표현식을 순서대로 실행하고 마지막 표현식의 값 또는 참조를 리턴한다.

위 코드의 (0, eval)의 경우, 아무 의미 없는 0 이후, eval의 참조를 리턴하기 때문에 indirect call 이 된다.




어떤 코드를 보면,

전역 객체(global object, 브라우저로 치자면 window)를 가져오는 용도로 아래와 같이 사용한다.


  var global = function () {

    return this || (0, eval)('this');

  };


왜 this 를 리턴할 수 있는데 굳이 eval()을 실행하려고 하는지 궁금할 것 같다.

위 코드는 ECMAScript5 환경에서 전역 객체를 가져오기 위한 코드인데,

ECMAScript5  환경에서는, 전역 범위에서 this가 undefined를 리턴하기 때문이다.

위 함수는 먼저 this를 확인하고, 없다면 indirect call로 전역 범위에서 코드를 실행해 객체를 가져온다.


ECMAScript5 환경에서의 this와 eval은 아래 코드를 실행해보면 자세히 이해할 수 있다.


  (function () {

    'use strict'; // ES5 환경에서 실행한다.

    console.log( this ); //--> undefined

    console.log( eval('this') ); //--> undefined, direct call로 함수 범위에서 실행된다.

    console.log( (0,eval)('this') ); //--> window, indirect call로 전역 범위에서 실행된다.

    console.log( window ); //--> window, 명시적으로 전역 객체를 가져올 수 있다.

  }());


'use strict' 구문은 함수 범위 내에서만 유효하다.

(0,eval)('this')를 이용해 indirect call로 전역 범위에서 실행하면,

'use strict'가 적용된 ES5 환경에서 벗어나기 때문에 this로 전역 객체에 접근할 수 있다.



약간 벗어난 주제이지만, ES5 환경에서 전역을 가져올 수 있는 방법은,

위의 4번째처럼 전역 객체의 이름(window)을 명시하거나,

함수의 파라미터로 전달하는 방법이 있다.


라이브러리 코드를 보면,

전체 코드를 익명함수로 감싸고 전역 변수를 파라미터로 전달하는 패턴이 많은데,

이런 이유 때문이기도 하다.


  (function (global) {

    'use strict';

    console.log(this); //--> undefined

    console.log(global); //--> 전역 객체에 접근할 수 있다.

  }(this));




사실 eval 자체가 보안에 취약한 코드이기 때문에 아예 사용하지 말라고도 하지만, (심지어 MDN의 eval API에서도)

가끔은 필요한 경우도 있다.

아예 쓰지 않는 것보다는, 개발자가 잘 판단해서 결정할 부분이라 생각한다.


자세히 알고 잘 활용하면, 더 효과적으로 풀어낼 수 있지 않을까~


# 2014.11.29 추가

익스플로러에 indirect eval call 의 스콥이 적용된 건, IE9부터이다.

IE8 이하 버전까지는 모든 eval 콜이 eval 이 실행된 스콥에서 수행된다.

자세한 내용은 http://ohgyun.com/527 참고.



# 참고:

- MDN eval: https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/eval

- ECMA262 - indirect call: http://ecma262-5.com/ELS5_HTML.htm#Section_10.4.2

- The JavaScript Comma Operator: http://javascriptweblog.wordpress.com/2011/04/04/the-javascript-comma-operator/

- (1,eval)('this') vs eval('this'): http://stackoverflow.com/questions/9107240/1-evalthis-vs-evalthis-in-javascript

카테고리

분류 전체보기 (702)
About me. (6)
Daylogs (667)
비공개 (0)
영어공부 (0)
My works - 추억 (29)