렉시컬 속이기
렉시컬 스코프는 개발자가 작성할 대 함수를 어디에 선언했는지에 따라 결정된다. 그렇다면 런타임 때 어떻게 레깃컬 스코프를 수정할 수 있을까?
자바스크립트에서는 렉시컬 스코프를 속일 수 있는 두 가지 방법이 있따. 두방법 모두 개발자 커뮤니티에서는 코드를 작성할 때 권장하지 않는 방법이다. 그러나 ㅁ ㅏㄴㅎ은 사람이 이런 방법을 비판하지만, 가장 중요한 논점을 빠트린다. 바로 렉시컬 스코프를 속이는 방법은 성능을 떨어뜨린다는 점이다.
성능 문제를 설명하기 전에 앞서 말한 두 가지 방법이 어떻게 작동하는지 알아보자.
eval
자바스크립트의 eval()
함수는 문자열을 인자로 받아들여 실행 시점에 문자열의 내용을 코드의 일부분처럼 처히한다. 즉, 처음 작성한 코드에 프로그램에서 생성한 코드를 집어넣어 마치 처음 작성될 때부터 있던 것 처럼 실행한다.
eval()
의 성격을 생각해보면 eval()
을 통해 어떻게 렉시컬 스코프를 수정하고 원래 작성했던 코드인 양 속일 수 잇는지 이해할 수 있다. eval()
이 실행된 후 코드를 처리할 때 엔진은 지난코드가 동적으로 해석되어 렉시컬 스코프를 변경시켰는지 알 수도 없고 관심도 없다. 엔진은 그저 평소처럼 레ㅓㄱ시컬 스코프를 검색할 뿐이다.
function foo(str, a) {
eval(str); // cheating!
console.log(a, b);
}
var b = 2;
foo("var b = 3;", 1); // 1, 3
문자열 "var b = 3;"은 eval()
이 호출되는 시점에 원래 있던 코드인 것처럼 처리된다. 이코드는 새로운 변수 b를 선언하며서 이미 존재하는 foo()
의 렉시컬 스코프를 수정한다. 사실, 앞에서 언급한 것처럼 이코드는 실제로 foo()
안에 변수 b를 생성하여 바깥(글로벌) 스코프에선언된 b를 가린다.
console.log()
가 호출될 때 a와 b모두 foo()
의 스코프에서 찾을 수 있으므로 바깥의 b는 아예 찾지도 않는다. 따라서 결괏밗은 일반적인 경우처럼 "1, 2"가 아니라 "1, 3"이 나온다.
이 예제에서는 설명을 위해 넘겨주는 코드 문자열을 단순히 고정된 문자로 한정했다. 그러나 이런 문자열은 프로그램로직을 이용하여 문자를 합쳐서 쉽게 생성할 수 있따. eval()은 흔히 동적으로 생성된 코드를 실행할 때 사용된다. 이는 고정된 문자열에서 정적 코드를 동적으로 생성하는 것은 코드를 직접 입력하는 것보다 이득이 없기 때문이다.
기본적으로 코드 문자열이 하나 잇아의 변수 또는 함수 선언문을 포함하면 eval()
이 그 코드를 실행하면서 eval()
이 호출된 위치에 있는 렉시컬 스코프를 수정한다. 기술적으로 다양한 트릭을 이용해 eval()
을 간접적으로 호출할 수 있고, 이 경우 코드는 글로벌 스코프 속에서 실행되고 글로벌 스코프를 수정한다. 어떤 경우든 eval()
은 개발자가 작성했던 때의 렉시클 스코프를 런타임에서 수정할 수 있다.
Strict Mode 프로그램에서 eval을 사용하면 eval은 자체적인 렉시컬 스코프를 이용한다. 즉, eval 내에서 실행된 선언문은 현재 위치의 스코프를 실제로 수정하지 않는다.
자바스크립트에는 eval()
과 매우 비슷한 효과를 내는 다른 방법이 있다. setTimeout()
과 setInterval()
은 첫째 인자로 문자열을 받을 수 있고, 문자열의 내용은 동적 생성된 함수 코드처럼 처리된다. 이 방법은 구식에다가 없어질 예정이니 사용하지 말자.
함수 생서자 new Function()
도 비슷한 방식으로 코드 문자열을 마지막 인자로 받아서 동적으로 생성된 함수로 바꾼다(입력받은 시작 인자들이 있으면 생성된 함수의 인자로 사용한다). 이 함수 생성자 문법은 eval()
보다는 좀더 안전하지만, 여전히 코드에 사용하지 않는 것이 좋다.
동적으로 생성한 코드를 프로그램에서 사용하는 경우는 굉장히 드문데, 사용할 때 성능 저하를 감수할 만큼 활용도가 높지 않기 때문이다.
with
카워드 with는 렉시컬 스코프를 속일 수 있는 자바스크립트의 또다른 기능이다(하지만 사용을 권장하지는 않는다. 이 기능은 없어질 예정이다). with를 설명하는 방법이 여럿 있지만, 필자는 with가 렉시컬 스코프와 어떻게 상호작용하고 스코프에 어떤 영향을 주느냐는 관점에서 설명하고자 한다.
with는 일반적으로 한 객체의 여러 속성을 참조할 대 객체 참조를 매번 반복하지 않기 위해 사용하는 일종의 속기법이라 할 수 있다.
var obj = {
a: 1,
b: 2,
c: 3,
};
// more "tedious" to repeat "obj"
obj.a = 2;
obj.b = 3;
obj.c = 4;
// "easier" short-hand
with (obj) {
a = 3;
b = 4;
c = 5;
}
with는 단순히 객체 속성을 편하게 접근할 수 있는 속기법 이상의 효과가 있다.
function foo(obj) {
with (obj) {
a = 2;
}
}
var o1 = {
a: 3,
};
var o2 = {
b: 3,
};
foo(o1);
console.log(o1.a); // 2
foo(o2);
console.log(o2.a); // undefined
console.log(a); // 2
이 예제에서 객체 o1과 o2가 생성되었다. 하나는 a라는 속성이 있고, 다른 하나는 그런 속성이 없다. 함수 foo()
는 객체 참조 obj를 인자로 받아서 with (obj){}
를 호출하고 with 블록 내부에서 변수 a에 대한 평범한 렉시컬 참조를 수행한다. 이는 LHS참조로 변수 a를 찾아 값 2를 대입하는 작업이다.
o1를 인자로 넘기면 a = 2
대입문 처리 과정에서 o1.a
를 찾아 값 2를 대입한다. 그 결과는 다음 줄에 호출된 console.log(o1.a)
문에 반영된다. 그러나 o2를 인자로 넘길 때는 o2에 a라는 속성이 없으므로 새로이 속성이 생성되지 않고 o2.a
는 undefined
로 남는다.
이때 발생하는 상당히 특이한 부작용에 주목해야 한다. 바로 대입문 a = 2
가 글로벌 변수 a를 생성한다는 점이다. 어떻게 이런 일이 가능할까?
with 문은 속성을 가진 객체를 받아 마치 하나의 독립된 렉시컬 스코프처럼 취급한다. 따라서 객체의 속성은 모두 해당 스코프 안에 정의된 확인자로 간주된다. 물론 with 블록이 객체를 하나의 렉시컬 스코프로 취급하기는 하지만, with 블록안에서 일반적인 var 선언문이 수행될 경우 선언된 변수는 with 블록이 아니라 with를 포함하는 함수의 스코프에 속한다.
eval()
은 인자로 ㅂ다은 코드 문자열에 하나 이상의 선언문이 있을 경우 이미 존재하는 렉시컬 스코프를 수정할 수 있지만, with 문은 넘겨진 객체를 가지고 난데없이 사실상 하나의 새로운 렉시컬 스코프를 생성한다.
이렇게 볼 대, o1를 넘겨받은 with 문은 o1이라는 스코프를 선언하고 그 스코프는 o1.a 속성에 해당하는 확인자를 가진다. 그러나 o2가 스코프로 사용되면 그 스코프에는 a확인자가 없으므로 이후 작업은 일반적인 LHS 확인자 검색 규칙에 따라 진행된다.
o2의 스코프, foo()
의 스코프, 글로블 스코프에도 a 확인자는 찾을 수 없다. 따라서 a = 2
가 수행되면 자동으로 그에 해당하는 글로벌 변수가 생성된다.
런타임에 with 문이 하낭의 객체와 그 속성을 하나의 스코프와 확인자로 바꾸는 동작은 매우 이해하기 어렵고 이상한 일이다. 하지만 이것이 결과를 가장 잘 설명한다.
참고
- You Don't Know JS - 타입과 문법, 스코프와 클로저( 한빛 미디어 )
댓글