본문 바로가기

Javascript

[JavaScript] 렉시컬 스코프, Lexical Scope / 동적 스코프, Dynamic Scope

반응형

 

⌗ 스코프, Scope
특정 상소에 변수를 저장하고 나중에 그 변수를 찾는데는 잘 정의된 규칙이 필요하는데 이를 스코프(Scope)라고 한다.
변수를 검색하는 이유는 변수에 값을 대입(LHS 참조)하거나 변수의 값을 얻어오기(RHS 참조) 위해서다.

스코프는 두가지 방식으로 작동하는데,

1 ) 렉시컬 스코프, Lexical Scope : 다른 방식보다 훨씬 더 일반적이고 다수의 프로그래밍 언어가 사용하는 방식

2 ) 동적 스코프, Dynamic Scope : Bash Scripting이나 Perl 의 일부 모드와 같은 몇몇 언어에서 사용하는 방식

 

렉시컬 스코프, Lexical Scope


일반적인 언어의 컴파일러는 첫 단계로 토크나이징 또는 렉싱이라 불리는 작업을 시작하여,

렉싱 처리 과정에서 소스 코드 문자열을 분석하여 상태 유지 파싱의 결과로 생성된 토큰에 의미를 부여한다.

 

이때 렉시컬 스코프는 렉싱 타임(Lexing Time)에 정의되는 스코프로,

프로그래머가 코드를 짤 때 변수와 스코프 블록을 어디서 작성하는가에 기초해서 렉서(Lexer)가 코드를 처리할 때 확정된다.

 

스코프 버블은 스코프 블록이 쓰이는 곳에 따라 결정되는데, 스코프 블록은 서로 중첩될 수 있다.

아래의 예시처럼 t 의 버블은 test 의 버블 내부에 완전히 포함된다. 바로 test 의 내부에서 t 를 정의했기 때문이다,

 

예시 )

function test(a) {
	var b = a * 2;
	
	funtion t(c) {
		console.log(a, b, c);
	}
    
	t(b * 3);
}

test(2); // 2, 4, 12

 

즉, 어떤 함수의 버블도 동시에 다른 두 스코프 버블 안에 존재할 수 없고, 어떤 함수도 두 개의 부모 함수 안에 존재할 수 없다.

 

검색

  엔진은 스코프 버블의 구조와 상대적 위치를 통해 어디를 검색해야 확인자를 찾을 수 있는지 안다.

  스코프는 목표와 일치하는 대상을 찾는 즉시 검색을 중단한다.

  여러 중첩 스코프 층에 걸쳐 같은 확인자 이름을 정의할 수 있고, 이를 '섀도잉, Shadowing'이라 한다.

  섀도잉과 상관없이 스코프 건색은 항상 실행 시점에서 가장 안쪽 스코프에서 시작하여 최초 목표와 일치하는 대상을 찾으면 멈추고,

  그전까지는 바깥쪽에서 위로 올라가면서 수행한다.

  어떤 함수가 어디서(혹은 어떻게) 호출되는지 상관없이 함수의 렉시컬 스코프는 함수가 언언된 위치에 따라 정의된다.

 

수정

  렉시컬 스코프는 프로그래머가 작성할 때 함수를 어디에 선언했는지에 따라 결정된다.

  따라서 런타임때 렉시컬 스코프를 수정할 수 있으나, 이는 성능을 떨어뜨리는 방법으로 권장하지는 않는다.

 

  1 ) eval

    자바스크립트의 eval() 함수는 문자열을 인자로 받아들여 실행 시점에 문자열의 내용을 코드의 일부분처럼 처리한다.

    즉, 처음 작성한 코드에 프로그램에서 생성한 코드를 집어넣어 마치 처음 작성될때부터 있던 것처럼 실행한다.

    기본적으로 코드 문자열이 하나 이상의 변수 또는 함수 선언문을 포함하면 eval()이 그 코드를 실행하면서

    eval()이 호출된 위치에 있는 렉시컬 스코프를 수정한다.

 

function test(str, a) {
	eval(str);
	console.log(a, b);
}

var b = 2;
test( "var b = 3;", 1); // 1, 3

 

    eval()은 흔히 동적으로 생성된 코드를 실행할 때 사용된다.

    혹은 함수 생성자 new Function()도 비슷한 방식으로 코드 문자열을 마지막 인자로 받아서 동적으로 생성된 함수로 바꾼다.

    하지만 동적으로 생성한 코드를 프로그램에서 사용하는 경우는 굉장히 드물며, 사용할때 성능 저하를 감수할 만큼 활용도가 높지 않다.

 

  2 ) with

    키워드 with은 일반적으로 한 객체의 여러 속성을 참조할때 객체 참조를 매번 반복하지 않기 위해 사용한다.

    with 문은 속성을 가진 객체를 받아 마치 하나의 독립된 렉시컬 스코처럼 취급한다.

    따라서 객체의 속성은 모두 해당 스코프 안에 정의된 확인자로 간주된다.

    물론 with 블록이 객체를 하나의 렉시컬 스코프로 취급하지만,

    with 블록 안에 일반적인 var 선언문이 수행될 경우 선언된 변수는 with 블록이 아니라 with를 포함하는 함수의 스코프에 속한다.

 

var obj = {
	a: 1,
	b: 2,
	c: 3
};

with(obj) {
	a = 3;
	b = 4;
	c = 5;
}

 

    하지만 with 사용을 권장하지 않으며, 이 기능은 곧 없어질 예정으로 참고만 하자.

 

  ⌗ 성능

    런타임에 스코프를 수정하거나, 새로운 렉시컬 스코프를 만드는 eval() 과 with 모두 성능이 좋지 않다.

    자바스크립트 엔진은 컴파일레이션 단계에서 상당수의 최적화 작업을 진행한다.

    이 최적화의 일부분이 하는 핵심 작업은 렉싱된 코드를 분석하여 모든 변수와 함수 선언문이 어디에 있는지 파악하고

    실행 과정에서 확인자 검색을 더 빠르게 하는 것이다.

    그러나, 스코프를 수정하거나 eval()과 with를 사용하는 코드가 있다면

    대다수 최적화가 의미가 없어 아무런 최적화도 되지 않은것과 마찬가지가 된다.

    따라서 eval() 이나 with를 사용했다는 사실만으로 그 코드는 거의 확실히 더 느리게 동작할 것이다.

 

★ 렉시컬 this

'뚱뚱한 화살표(=>)'은 주로 function 키워드의 줄임말로 언급되나, 사실은 훨씬 중요한 요소가 있다.

 

예시 )

var obj = {
    id: "test",
    cool: function coolFn() {
        console.log(this.id);
    }

};

var id = "main";
obj.cool(); // test
setTimeout(obj.cool, 100); // main

 

setTimeout을 하게되면 obj.cool 함수에서 사용하던 this가 사라진다는 점이다.

이 문제를 해결할 방법은 여러가지 중 아래와 같은 방법이있다.

 

예시 해결 )

var obj = {
    id: "test",
    cool: function coolFn() {
        var self = this;

        if (self.count < 1) {
            setTimeout( function timer() {
                self.count++;
                console.log("main?");
            }, 100);
        
        }
        console.log(this.id);
    }

};

obj.cool(); // test

 

이렇게 사용하면 렉시컬 스코프에서 클로저를 이용해 얻을 수 있는 self를 사용하면,

this 바인딩이 어떻게 되는지에 대한 고민이 사라질 것이다.

 

그러나 이렇게 장황하게 쓰는게 싫어 '화살표 함수(=>)'를 쓰게되면 골치 아파진다..

 

골치아픈 예시 )

var obj = {
    count: 0,
    cool: function coolFn() {
        if (this.count < 1) {
            setTimeout( () => {
                this.count++;
                console.log("main?");
            }, 100);
        
        }
    }

};

obj.cool(); // main?

 

간단히 설명하자면, '화살표 함수(=>)'는 this 바인딩과 연계될 때는 일반 함수처럼 작동하지 않는다. 

'화살표 함수(=>)'는 모든 this 바인딩에 대한일반 규칙을 폐기하고, 대신 자기 가까이에 둘러싼 렉시컬 스코프에서 this값을 받아온다.

그래서 위 예제처럼 예상치 못하게 this 바인딩이 해제되는 일없이 cool()함수의 this 바인딩을 승계한다.

 

코드를 작성할때 특정한 접근법을 받아들이는 것은 자연스럽지만,

같은 코드에 여러 접근법을 섞는 것은 좋지않다.

위 문제를 해결하려면 더 적절한 방식은 this 매커니즘을 명확히 이해하고 사용하는 것이다.

 

좋은 예시 )

var obj = {
    count: 0,
    cool: function coolFn() {
        if (this.count < 1) {
            setTimeout( function timer() {
                this.count++;
                console.log("main!");
            }.bind(this), 100);
        
        }
    }

};

obj.cool(); // main!

 

화살표 함수(=>)의 새로운 동작인 렉시컬 this를 선호하든, 이미 증명된 bind()함수를 선호하든

화살표 함수(=>)는 단순히 키워드 function 과 적용 방식에서 차이가 있다.

 

동적 스코프, Dynamic Scope


렉시컬 스코프는 엔진이 변수를 찾는 검색 방식과 위치에 대한 규칙이다.

렉시컬 스코프의 주요 특성은 이 스코프가 프로그래머가 코드를 작성할 때 결정된다는 것이다.

 

예시)

function test() {
    console.log(a);
}

function main() {
    var a = 3;
    test();
}

var a = 2;
main(); // 2

 

위의 예시로 보면, 우리들 눈에는 중첩(렉시컬) 스코프 체인에 익숙해져있는 모습을 볼 수 있고 그렇게 판단해왔다.

 

만약 동적 스코프라는 말을 들으면, 스코프를 코드 작성때가 아닌 런타임 때에 동적으로 결정하는 모델이구나 싶을 것이다.

동적 스코프는 함수와 스코프가 어떻게 어디서 선언됐는지 상관없고, 오직 어디서 호출됐는 지와 연관된다.

즉, 동적 스코프 체인은 코드 내 스코프의 중첩이 아니라 '콜-스택'과 관련 있다.

 

만약 자바스크립트가 동적 스코프를 사용한다면, 마지막에 main()의 결과값으로는 2가 아닌 3이 나왔을 것이다.

 

정확하게 다시 말하면, 자바스크립트는 동적 스코프를 사용하지 않고 렉시컬 스코프만 사용한다.

단, this 메커니즘이 동적 스코프와 비슷한이있다.

 

* 렉시컬 스코프는 작성할때, 동적 스코프(그리고 this)는 런타임에 결정된다.

* 렉시컬 스코프는 어디서 함수가 선언됐는지와 관련 있지만, 동적 스코프는 어디서 함수가 호출됐는지와 관련있다.

 


[JavaScript] 스코프, Scope : chati.tistory.com/157

 

[JavaScript] 스코프, Scope

⌗ 스코프, Scope 특정 상소에 변수를 저장하고 나중에 그 변수를 찾는데는 잘 정의된 규칙이 필요하는데 이를 스코프(Scope)라고 한다. 변수를 검색하는 이유는 변수에 값을 대입(LHS 참조)하거나 ��

chati.tistory.com

 


[참고] You Don't Know JS 타입과 문법, 스코프와 클로저 - 카일 심슨

 

반응형

❥ CHATI Github