본문 바로가기
이론/Frontend

JavaScript의 스코프 - 2 (Scope of JavaScript)

by 유세지 2021. 5. 3.

지난 글에서는 스코프가 어떤 것인지 알아보았습니다. 이번 글에서는 자바스크립트를 비롯한 대부분의 프로그래밍 언어에서 사용하는 렉시컬 스코프에 대해 알아보겠습니다.

 

렉시컬 스코프

렉시컬 스코프는 스코프의 작동 방식 중 하나입니다. 컴파일러가 문자열을 의미있는 단어들로 나누어 내는 것을 토크나이징이라고 했는데, 이를 렉싱(Lexing)이라고 부르기도 합니다. 렉싱이 일어나는 시간대를 렉싱 타임(Lexing Time)이라고 하는데 결국 개발자가 작성한 코드들이 어디에 작성되었는지를 기반으로 처리된다고 볼 수 있겠습니다. 다른 방식으로는 동적 스코프가 존재하지만, 이 글에서 따로 다루지는 않겠습니다.

 

 

여러 함수들이 중첩되어 있는 코드의 스코프를 그림으로 나타내면 아래와 같습니다.

매개변수를 포함한 함수 내부까지의 영역이 하나의 박스로 묶여있는 것을 볼 수 있습니다. 이처럼 각각의 스코프 영역들은 나누어져있으며 서로 중첩되지 않는, 독립적인 관계를 갖습니다.

 

위의 그림처럼 스코프는 각각의 블록으로 나뉘어져있기 때문에 검색을 하기에도 용이합니다. 확인자(Identifier)를 찾는 과정을 살펴보면 대상이 호출된 위치의 스코프를 확인하고, 찾지 못했다면 상위 스코프로 한 겹씩 올라가다가 최상위 스코프까지 진행한뒤 검색이 종료됩니다. 그 과정에서 대상을 찾으면 즉시 종료됩니다.

 

이러한 검색 순서를 이용하여 의도적으로 동일한 확인자 이름을 다르게 할당하여 사용할 수도 있습니다. 여러 중첩 스코프 상에 같은 이름의 확인자를 정의하는 것을 섀도잉(Shadowing) 이라고 하며, 안쪽의 확인자가 바깥쪽의 확인자를 가리는 가려짐 현상을 유발하게 됩니다.

 

일반적으로 가려진 대상은 참조할 수 없지만, 예외적으로 글로벌 변수는 참조할 수 있는 방법이 있습니다. 바로 글로벌 객체인 window 를 통한 접근 방법을 사용하면 간접적으로 참조할 수 있게 됩니다. 그 외에는 불가능합니다.

 

var globalValue = 123;

function foo() {
	var globalValue = 321;
    console.log(window.globalValue); // 123
}

 

 

 

렉시컬 속이기

이러한 렉시컬 스코프들은 모두 렉싱 타임때 정해지고, 런타임 중에는 이미 구성된 렉시컬 스코프를 기반으로 여러 변수나 함수들을 참조하게 됩니다. 일반적인 방법으로는 불가능해 보이고, 가능하다고 해도 권장하지는 않을 것 같으나 런타임 도중에도 렉시컬 스코프를 수정하는 방법이 있습니다.

 

첫 번째 방법은 eval입니다.

 

eval은 문자열을 매개변수로 받아 코드의 일부분처럼 처리하게 하는 함수입니다.

 

var yellow = 'lemon';

function fruit(str) {
	eval(str);
    console.log(yellow); // lemon?
}

fruit("yellow = banana;");

/* 실행 결과 */
banana

 

예시 코드처럼 처음부터 eval 함수의 위치에 코드가 들어간 것과 같은 효과를 줍니다. 렉싱 타임이 아닌 런타임에서 렉시컬 스코프를 수정하게 되는 것입니다. 한 가지 유의할 점은 Strict Mode에서는 자체적인 스코프를 이용하기 때문에 기존의 스코프를 수정하지 않는다는 점을 기억해두는게 좋겠습니다.

 

다만 기본적으로 eval 함수 자체는 웬만한 경우에 사용을 권장하지 않는 함수이기도 하고, 런타임 단계에서 스코프를 수정하는 행위 자체도 지양하는 편이 좋습니다.

 

 

 

두 번째 방법은 with입니다. 

 

with은 어떤 객체의 속성을 참고할때 반복해서 참조자를 입력할 수고를 덜어주는 약어의 기능을 합니다. 마치 C++의 using namespace와 비슷하다고 생각하셔도 좋습니다.

 

우선 with은 객체 접근을 도와주는 키워드 답게 생성 당시의 객체가 가진 스코프를 갖습니다. 예시 코드를 보겠습니다.

 

let foo = {
	a = 1;
};

console.log(foo.a); // 1

with (foo) {
	a = 2;
}

console.log(foo.a); // 2

 

with 구문 안에서 a라는 변수를 호출하여 대입했기 때문에 foo 함수 내부의 스코프에서 변수 a를 찾아 2를 대입하게됩니다. 그런데 이런 경우엔 예상과는 조금 다르게 동작합니다.

 

let foo = {
	a = 1;
};

console.log(foo.a); // 1

with (foo) {
	b = 2;
}

console.log(foo.b); // undefined

 

foo 함수에 변수 b가 없긴 했지만, 분명 foo 함수 스코프 내에서 b를 선언했는데 찾을 수 없다는(undefined) 로그가 출력되었습니다. 지난 글에서 대입하려는 변수가 없다면, 자바스크립트의 엔진은 새로 하나 만들어준다고 이야기했었습니다. 그 덕분에 따로 변수를 선언하지 않고도 사용할 수 있었는데 이 과정을 잘 살펴보면 LHS 과정의 마지막에 일어났다는 것을 알 수 있습니다. LHS 과정의 마지막은 원래 스코프로부터 한 단계씩 상위 스코프로 이동하여 전역 스코프에서 종료되는 것을 떠올려본다면, b가 글로벌 변수로 선언되는 것도 그리 이상하지는 않을 것입니다.

 

무엇보다도 중요한 건 with을 사용했을 경우에 발생하는 이러한 예외적인 케이스가 어떤 결과를 가져오는지에 대해 알고 넘어가는 것이고, 실제로 with을 사용하는 것은 최대한 지양하시기를 바라겠습니다.

 

그 이유인 즉슨, with 키워드는 MDN 문서에 사용하지 않을 것을 강력히 권장하고 있습니다. 또한 엄격 모드에서는 (글로벌 객체 때문이겠지만) 이미 금지되어 있으니 eval보다도 더 질 나쁜(?) 코드임을... 알고 가시면 되겠습니다.

 

엄격 모드에서 금지된 with

 

 

렉시컬 스코프를 수정하는 것은 코드의 가독성과 미관을 해침과 동시에 성능적인 저하를 가져오기도 합니다. 컴파일 과정에서 이루어진 최적화를 무시하고 임의로 코드(스코프)를 변경하는 행위이기 때문에 거의 확실하게 느려진다고 볼 수 있습니다. 물론 대부분의 경우 크게 신경쓰지 않아도 괜찮은 수준이겠지만, 최적화가 의미 없어진다는 점 하나만으로도 굳이 권장되는 방법이 아닌 것은 분명해보입니다.

 

 

 

여기까지 렉시컬 스코프에 대해서 알아보았습니다. 다음 글에서는 조금 다른 스코프에 대한 내용으로 뵙겠습니다.

반응형

댓글