First written: 23-02-15
Uploaded: 23-07-29
Compiler가 하는 작업 중 Tokenizing 혹은 Lexing이라고 불리는 작업이 있다. Lexing을 하는 과정에서 Compiler는 소스코드를 분석하고 parsing 후 생성된 token에 의미를 부여하게 된다. 그리고 여기서 말하는 lexing이 Lexical scope의 어원이다. 즉, Lexical Scope는 lexing을 하는 과정에서 정의된 scope다. 이와 대비되는 개념으로 Dynamic Scope가 있는데, 사실 이 둘의 차이가 어떤 것인지, lexing이란 무엇인지 등에 대해 여기서는 다루지 않도록 한다. 아래의 코드를 보자.
function fn1(a) {
var b = a + 1;
function fn2(c) {
console.log(a, b, c);
}
fn2(b + 1);
}
fn1(1); // 1 2 3
fn1
은 global scope에 속한다.
a
, b
, fn2
는 fn1
의 scope에 속한다.
c
는 fn2
의 scope에 속한다.
따라서 최종적으로 console.log(a, b, c);
에서 a와 b와 c를 참조하려 했을 때, 다음과 같은 일이 벌어진다.
우선 a
를 찾기 위해 "Engine"
이 fn2 "Scope"
에 질문을 하고, fn2 "Scope"
가 모른다고 하자 한 단계 위의 fn1 "Scope"
에게 물어 a
에 할당된 값을 알아낸다(b
역시 마찬가지 과정을 거친다). 하지만 c
는 fn2 "Scope"
에 정의된 variable이라 fn1 "Scope"
나 global "Scope"
로 올라가지 않고 할당된 값을 알아낼 수 있다.
그리고 이렇게 겹쳐진 function들을 통해 형성된 중첩된 scope들은 모두 functional scope
이다.
Functional scope를 잘 이용하면 '최소 권한의 원칙'을 지킬 수 있고, 또한 충돌을 방지할 수도 있어 좋다. 아래와 같은 코드를 보자.
function doSomething(a) {
function doSomethingElse(a) {
/* ... */
}
var result = doSomethingElse(100) + 100;
return result;
}
doSomething(data);
function fn1() {
function fn2(a) {
var i = 10;
/* ... */
console.log(a * i);
}
for(var i=0; i<10; i++){
bar(i);
}
}
fn1();
예시의 doSomething
은 자기 자신만이 사용하는 doSomethingElse
함수를 자신의 scope로 가져옴으로써 최소 권한의 원칙을 지켰고, 혹여나 발생할 수 있는 충돌도 방지했다.
아래 예시의 fn1 scope
를 보면 fn2 scope
와 for scope
가 있는데, 자주 사용되는 i
를 선언하고 할당할 때 var
키워드를 활용해 자신의 scope로 제한시켜 충돌을 방지했음을 알 수 있다.
하지만 누군가는 그런 상상을 할 수 있다. Functional scope를 활용하려면 계속해서 함수를 정의해야 하고(위의 예시에서는 그 결과로 global scope에 확인자 두 개가 생겼다 - doSomething, fn1), 호출해야만 코드가 실행된다.
var name = "Jane";
(function fn() {
var name = "Juan";
console.log(name); // Juan
console.log(fn); // [Function: fn]
})();
console.log(name); // Jane
위의 pattern은 IIFE(Immediately, Invoked, Function, Expression)라고 불린다. function fn() {...}
를 괄호로 감싸면서 이를 표현식으로 바꾸었고, 그 뒤에 따라붙은 ()
는 해당 함수를 '즉시' 실행하는 기능을 맡는다. 이로써 function fn
은 자기 자신의 scope에 속하게 되었고, global scope를 오염시키지 않게 되었다. IIFE pattern의 다른 형식은 아래와 같다.
(function fn() {
var name = "Juan";
console.log(name); // Juan
console.log(fn); // [Function: fn]
}());
함수를 실행해주는 ()
의 위치가 안쪽으로 들어갔을 뿐, 동작은 같으므로 원하는 style을 따라가면 된다.
IIFE도 결국 함수라는 것을 이용하면 아래와 같은 코드도 가능하다.
var name = "Jane";
(function fn(global) {
var name = "Juan";
console.log(name); // Juan
console.log(fn); // [Function: fn]
console.log(global.name); // Jane
})(window);
console.log(name); // Jane
global scope를 인자로 넘겨 global scope에 정의된 variable인 name에 접근했다!
개인적으로는 좀 더 복잡하지만 아래의 방법도 같은 결과를 불러온다. UMD(Universal Module Definition) project에서 많이 쓰인다고 한다.
var name = "Jane";
(function IIFEfn(func) {
func(window);
})(function fn(global) {
var name = "Juan";
console.log(name); // Juan
console.log(fn); // [Function: fn]
console.log(global.name); // Jane
});
console.log(name); // Jane
바로 이해가 되지 않는다면 이전 내용들을 바탕으로 차근차근 생각해보자.
Functional Scope를 보는 김에 관련해서 언급될 수 있는 두 가지 것들을 짚고 넘어가자. 그다지 중요하진 않으니 위에서 골에 쥐가 난 것 같다면 건너뛰어도 좋다.
조금이라도 Javascript와 친숙한 사람이라면 eval
과 같이 나온 with
에조차 의구심 가득한 눈초리를 보낼 수밖에 없다. 그 악명높은 eval
이라니! 그렇다, 결론부터 말하자면 둘 다 쓰지 말자는 차원에서 소개하는 것이다. 성능이 저하되기 때문이다.
여기서는 특히나 lexical scope를 수정해버리는 두 메서드의 놀라운 능력을 살펴볼 것이다.
function fn(str, a) {
eval(str);
console.log(a, b);
}
var b = 2;
fn("var b = 1;", 10); // 10, 1
다들 알겠지만 eval()
은 인자로 받은 string을 코드처럼 실행시킨다. 따라서 fn scope
에는 없던 b
가 생겨버리는 마법을 부릴 수 있게 되었다. 당연하게도 fn scope
에서 b
를 찾았으니, global scope
에 선언되었던 b
는 찬밥신세를 면치 못했다. 참고로 "Strict Mode"에서는 eval()
이 자체적인 scope를 사용하게 되므로 조금이나마 낫다.
다음으로는 with
를 살펴보자.
var obj = {
name: "Juan",
age: 18
}
with (obj) {
name = "Gerrardo";
age = 28;
}
obj; // { name: 'Gerrardo', age: 28 }
with
는 익숙하지 않을 수 있어 어떨 때 쓰이는 메서드인지를 보여주기 위해 예시를 가져왔다. 매번 귀찮게 obj.name
, obj.age
에 접근해서 바꾸는 것이 아니라 일괄적으로 객체의 속성을 참조하기 위해 쓰인다.
문제는 아래의 경우다.
function changeAge(obj) {
with (obj) {
age = 20;
}
}
var obj1 = {
name: "Hwani",
age: 30,
nationality: "South Korea"
}
var obj2 = {
name: "Ivan",
gender: "M",
nationality: "USA"
}
changeAge(obj1);
obj1.age; // 20
changeAge(obj2);
obj2.age; // undefined
age; // 20
changeAge
는 parameter로 들어온 object의 age
를 변경하는 함수다. obj1
에는 age
가 있어 문제가 없었으나, obj2
에는 age
가 애초에 없었다.
사실 with
는 인자로 들어온 object를 엄연한 하나의 lexical scope로 여긴다(eval
은 본인이 속한 lexical scope를 어지럽게 했던 것과 비교해서 기억해보자). obj2 scope
에는 아쉽게도 age
가 없다. changeAge scope
에도 없다. global scope
에도 없다(LHS 확인자 참조 과정). 그래서 age = 20;
은 결국 global scope
에서 실행되고 만다(물론 "Strict Mode"가 아닐 때의 이야기다).
이 두 메서드의 단점은 최적화를 무의미하게 만든다는 것이다. eval()
은 lexing time에 lexical scope를 교란시킬 가능성이 있다. with
는 상황에 따라 새 lexical scope를 생성해버릴 수 있다. 하여 with
나 eval
을 사용했다는 이유만으로도 코드가 느려질 수 있는 것이다.
Scope와는 관련이 크게 없으나, 체크해두면 좋을 내용이다. 아래의 코드를 보자.
setTimeout( function(){
/* ... */
}, 1000);
익숙하다. 하지만 익명 함수다. 디버깅이 어려울 가능성이 있고, 재귀 호출이 매우 까다로우며(arguments.callee
를 사용해야 함), 협업시 이름이 명시된 것에 비해 이해하기가 어렵다. 하여 위의 방식이 우리에게 아주 친숙하더라도, 아래와 같은 방식이 더 권장된다.
setTimeout( function timeoutFn(){
/* ... */
}, 1000);
참고 서적 : 카일 심슨, 2017, 한빛미디어, 『YOU DON'T KNOW JS: 타입과 문법, 스코프와 클로저』