5.3 스코프 체인

vencott·2021년 6월 2일

여기서부터는 활성 객체와 변수 객체를 변수 객체라는 표현으로 통일한다

이 절에서는 5.2 실행 컨텍스트 생성 과정에서 설명한 스코프 체인이 어떻게 만들어지는지 살펴본다

자바스크립트 코드를 이해하려면 스코프 체인의 이해는 필수적이다

이를 알아야 변수에 대한 인식 메커니즘을 알 수 있고, 현재 사용되는 변수가 어디에서 선언된 변수인지 정확히 알 수 있기 때문이다

자바스크립트도 다른 언어와 마찬가지로 스코프, 즉 유효 범위가 있다

이 유효 범위 안에서 변수와 함수가 존재한다

다음은 C 언어의 유효 범위를 확인할 수 있는 코드이다

void example_scope() {
    int i = 0;
    int value = 1;
    for(i = 0; i < 10; i++) {
        int a = 10;
    }
    printf("a : $d", a); // 컴파일 에러
    
    if (i == 10) {
        int b = 20;
    } 
    printf("b : %d", b); // 컴파일 에러
    printf("value  %d", value); // 1
}

C 언어에선 중괄호로 묶여 있는 범위 안에서 선언된 변수는 블록이 끝나는 순간 사라진다

특히, 함수의 {}뿐만 아니라 if, for 문의 {}도 한 블록으로 묶여, 그 안에서 선언된 변수가 밖에서는 접근이 불가능하다

하지만 자바스크립트에서는 함수 내의 {}블록은, 이를 테면 if{}, for{}와 같은 구문은 유효 범위가 없다

자바스크립트는 오직 함수만이 유효 범위의 한 단위가 된다

이 유효 범위를 나타내는 스코프가 [[scope]] 프로퍼티로 각 함수 객체 내에서 연결 리스트 형식으로 관리되는데, 이를 스코프 체인이라 한다

스코프 체인은 각 실행 컨텍스트의 변수 객체가 구성 요소인 리스트이다

각각의 함수는 [[scope]] 프로퍼티로 자신이 생성된 실행 컨텍스트의 스코프 체인을 참조한다

함수가 실행되는 순간 실행 컨텍스트가 만들어지고, 이 실행 컨텍스트는 실행된 함수의 [[scope]] 프로퍼티를 기반으로 새로운 스코프 체인을 만든다

5.3.1 전역 실행 컨텍스트의 스코프 체인

다음의 규칙을 기억하자

  1. 각각의 함수는 자신이 생성될 때 [[scope]] 프로퍼티로 자신이 생성된 실행 컨텍스트의 스코프 체인을 참조한다
  2. 함수가 실행되는 순간 실행 컨텍스트가 만들어지고, 이 실행 컨텍스트는 실행된 함수의 [[scope]] 프로퍼티를 기반으로 새로운 스코프 체인을 만든다
var var1 = 1;
var var2 = 2;
console.log(var1);
console.log(var2);

위 예제는 전역 코드이며 함수가 선언되지 않아 함수 호출이 없고, 실행 가능한 코드들만 나열되어 있다

이 자바스크립트 코드를 실행하면, 먼저 전역 실행 컨텍스트가 생성되고, 변수 객체가 만들어진다

이 변수 객체의 스코프 체인은 현재 전역 실행 컨텍스트 단 하나만 실행 되고 있어 참조할 상위 컨텍스트가 없으므로 자기 자신만을 가진다

즉, 변수 객체의 [[scope]] 프로퍼티는 변수 객체 자신을 가리키고 있다

그 후 var1, var2와 같은 변수들이 생성되고 이들은 변수 객체에 의해 참조된다

이 변수 객체는 전역 컨텍스트의 변수 객체이므로 이 변수 객체가 곧 전역 객체가 된다

5.3.2 함수를 호출한 경우 생성되는 실행 컨텍스트의 스코프 체인

var var1 = 1;
var var2 = 2;

function func() {
    var var1 = 10;
    var var2 = 20;
    console.log(var1); // 10
    console.log(var2); // 20
}

func();
console.log(var1); // 1
console.log(var2); // 2

이 예제를 실행하면 전역 실행 컨텍스트 생성되고, func() 함수 객체가 생성된다

이 함수 객체의 [[scope]]는 어떻게 될까?

위에서 설명한 규칙대로, 자신이 생성된 실행 컨텍스트의 스코프 체인을 참조한다

즉, 함수가 생성될 때 그 함수 객체의 [[scope]]는 현재 실행되는 컨텍스트의 변수 객체에 있는 [[scope]]를 그대로 가진다

따라서 func 함수 객체의 [[scope]]는 전역 변수 객체가 된다

이후, 함수를 실행하면 새로운 컨텍스트가 만들어진다

새로 생성된 func 컨텍스트의 스코프 체인은 실행된 함수의 [[scope]] 프로퍼티를 그대로 복사한 후, 현재 생성된 변수 객체를 스코프 체인의 맨 앞에 추가한다

func 함수 객체가 [[scope]]로 전역 변수 객체 하나만을 가지고 있었으므로, 여기에 func 변수 객체를 추가한다

즉, func 실행 컨텍스트의 스코프 체인은 [func 변수 객체 - 전역 객체]의 리스트가 된다

[[scope]]

생성된 함수 객체의 [[scope]]는 자신을 생성한 컨텍스트의 스코프 체인을 참조한다

함수의 이 [[scope]] 프로퍼티는 차후 함수 실행으로 생성된 새로운 컨텍스트의 스코프 체인에 의해 참조된다

새로운 컨텍스트의 스코프 체인은 이 함수의 [[scope]] 프로퍼티의 복사본에 자신의 변수 객체를 그 앞에 추가하는 방법으로 만들어진다

스코프 체인

각 함수 객체는 [[scope]] 프로퍼티로 현재 컨텍스트의 스코프 체인을 참조한다

한 함수가 실행되면 새로운 실행 컨텍스트가 만들어지는데, 이 새로운 실행 컨텍스트는 현재 실행 되는 함수 객체의 [[scope]] 프로퍼티를 그대로 복사한 뒤 그 체인의 제일 앞에 새롭게 생성된 변수 객체를 추가하는 방법으로 자신의 스코프 체인을 만든다

요약하면 스코프 체인 = 현재 실행 컨텍스트의 변수 객체 + 상위 컨텍스트의 스코프 체인 이라고 할 수 있다

아래 두 예제의 차이점을 명확이 알아두자

예제 1

var value = "value1";

function printFunc() {
    var value = "value2";
    
    function printValue() {
        return value;
    }
    
    console.log(printValue()); // value2
}

printFunc();
  1. 전역 실행 컨텍스트 생성
  2. 전역 변수 value 생성
  3. 함수 printFunc 생성
    1. 생성된 함수 printFunc의 [[scope]]는 현재 실행중인 전역 실행 컨텍스트의 스코프 체인을 참조
  4. 전역 변수 value를 "value1"로 초기화
  5. printFunc() 실행
    1. printFunc 실행 컨텍스트 생성(스코프 체인은 [printFunc - 전역 변수 객체])
    2. printFunc 내 변수 value 생성
    3. printFunc 내 printValue 함수 생성
      1. 생성된 함수 printValue의 [[scope]]는 현재 실행중인 printFunc 컨텍스트의 스코프 체인을 참조
    4. printFunc 내 변수 value를 "value2"로 초기화
    5. printFunc 내 printValue() 실행
      1. printValue 실행 컨텍스트 생성(스코프 체인은 [printValue - printFunc - 전역 변수 객체])
      2. printValue 내 return value; 실행 (이때 참조하는 value : printValue에 value 없으므로 printFunc의 value 참조)
      3. "value2"를 반환하고 printValue 실행 컨텍스트 종료
    6. console.log("value2");을 실행하고 printFunc 실행 컨텍스트 종료
  6. 전역 실행 컨텍스트 종료

예제 2

var value = "value1";

function printValue() {
    return value;
}

function printFunc(func) {
    var value = "value2";
    console.log(func());
}

printFunc(printValue); // value1 
  1. 전역 실행 컨텍스트 생성
  2. 전역 변수 value 생성
  3. 함수 printValue 생성
    1. 생성된 함수 printValue의 [[scope]]는 현재 실행중인 전역 실행 컨텍스트의 스코프 체인을 참조
  4. 함수 printFunc 생성
    1. 생성된 함수 printFunc의 [[scope]]는 현재 실행중인 전역 실행 컨텍스트의 스코프 체인을 참조
  5. 전역 변수 value를 "value1"로 초기화
  6. printFunc() 실행
    1. printFunc 실행 컨텍스트 생성(스코프 체인은 [printFunc - 전역 변수 객체])
    2. printFunc 내 변수 value 생성
    3. printFunc 내 변수 value를 "value2"로 초기화
    4. func()(printValue()) 실행
      1. printValue 실행 컨텍스트 생성(스코프 체인은 [printValue - 전역 변수 객체])
      2. return value; 실행 (이때 참조되는 value : printValue에 value 없으므로 전역 변수 객체의 value 참조)
      3. "value1"을 반환하고 printValue 실행 컨텍스트 종료
    5. console.log("value1");을 실행하고 printFunc 실행 컨텍스트 종료
  7. 전역 실행 컨텍스트 종료

이렇게 만들어진 스코프 체인으로 식별자 인식이 이루어진다

식별자 인식은 스코프 체인의 첫 번째 변수 객체부터 시작하여, 식별자와 대응되는 이름을 가진 프로퍼티가 있으면 이를 참조하고, 없으면 다음 변수 객체로 이동하여 찾는다

여기서 this는 식별자가 아닌 키워드로 분류되므로, 스코프 체인의 참조 없이 접근할 수 있다

호이스팅

여기까지 이해했으면 4.1.5 함수 호이스팅의 원리를 이해해야 한다

foo();
bar();

var foo = function () {
    console.log("This is foo, x = " + x);
};

function bar () {
    console.log("This is bar, x = " + x);
};

var x = 1;

위 코드는 다음과 같은 순서로 진행된다

// 변수 생성
var foo;

function bar () {
    console.log("This is bar, x = " + x);
};

var x;

// 코드 실행
foo(); // TypeError
bar(); // This is bar, x = undefined

foo = function () {
    console.log("This is foo, x = " + x);
};

x = 1;

함수 생성 과정에서 변수 foo, 객체 bar, 변수 x가 차례대로 생성된다

이때 foo와 x에는 undefined가 할당된다

코드가 실행되면 foo(); 에서 TypeError가 발생한다

이는 foo가 아직 함수가 아니고 undefined이기 때문이다

bar();를 실행하면 bar 실행 컨텍스트가 생성되고 내부 코드가 실행되나, x는 아직 undefined이므로 This is bar, x = undefined가 출력된다

x에 1이 할당된다


출처: 송형주, 고현준 저, 『인사이드 자바스크립트』, 한빛미디어(2014)

profile
Backend Developer

0개의 댓글