본문 바로가기
javascript

[javascript] 실행 컨텍스트

by 박연호의 개발 블로그 2020. 5. 2.

 

실행 컨텍스는 실행할 코드에 제공할 환경 정보들을 모아놓은 객체로 자바 스크립트의 동적 언어로서의 성격을 가장 잘 파악할 수 있는 개념입니다. 자바스크립트는 어떤 실행 컨텍스트가 활성화되는 시점에 선언된 변수를 위로 끌어올리고(호이스팅), 외부 환경 정보를 구성하고, this 값을 설정하는 등의 동작을 수행하는데, 이로 인해 다른 언어에서는 발견할 수 없는 특이상 현상들이 발생합니다.


실행 컨텍스트란 ?

앞서 '실행 컨텍스트를 실행항 코드에 제공할 환경 정보들을 모아놓은 객체'라고 했습니다. 동일한 환경에 있는 코드들을 실행할 때 필요한 환경 정보들을 모아 컨텍스트를 구성하고 이를 콜 스택에 쌓아 올렸다가 가장 위에 있는 컨텍스트와 관련 있는 코드들을 실행하는 식으로 전체 코드의 환경 순서를 보장합니다. 

 

여기서 '동일한 환경', 즉 하나의 실행 컨텍스트를 구성할 수 있는 방법으로 전역 공간, eval()함수, 함수 등이 있습니다. 자동으로 생성되는 전역 공간과 악마로 취급받는 eval을 제외하면 우리가 흔히 실행 컨텍스트를 구성하는 방법은 함수를 실행하는 것뿐입니다.

 

1 // ---------------- (1)
2 var a = 1;
3 function outer(){
4     function inner(){
5        console.log(a) // undefined
6        var a = 3;
7     }
8   inner(); // ------------ (2)
9   console.log(a);
10 }
11 outer(); // ------------ (3)
12 console.log(a) // 1

처음 자바스크립트 코드를 실행하는 순간(1) 전역 컨텍스트가 콜 스택에 담깁니다. 전역 컨텍스트라는 개념은 일반적인 실행 컨텍스트와 특별히 다를 것이 없습니다. 최상단의 공간은 코드 내부에서 별도의 실행 명령이 없어도 브라우저에서 자동으로 실행하므로 자바스크립트 파일이 열리는 순간 전역 컨텍스트가 활성화된다고 이해하면 됩니다.

 

이후 (3)에서 자바스크립트 엔진은 outer의 실행 컨텍스트를 콜스택에 담습니다. outer함수가 실행되고 (2)에서 inner의 실행 컨텍스트가 콜 스택에 담깁니다. inner() 함수가 실행된 후 inner 실행 컨텍스트가 콜 스택에서 제거되고 그 아래 있던 outer 실행 컨텍스트가 콜 스택의 맨 위에 존재하게 되어 다시 실행됩니다. 이후 outer 함수가 종료되고 콜 스택에는 전역 컨텍스트만 남아있게 됩니다. 마지막으로 a를 출력하고 전역 컨텍스트도 콜스택에서 제거됩니다.

 

이렇게 어떤 실행 컨텍스트가 활성화될 때 자바스크립트 엔진은 해당 컨텍스트에 관련된 코드들을 실행하는 데 필요한 환경정보들을 수집해서 실행 컨텍스트 객체에 저장합니다. 이 객체는 자바스크립트 엔진이 활용할 목적으로 생성할 뿐 개발자가 코드를 통해 확인할 수 없습니다. 여기에 담기는 정보들은 다음과 같습니다.

 

1. VariableEnvironment

현재 컨텍스트 내의 식별자들에 대한 정보 + 외부 환경 정보, 선언 시점의 LexicalEnvironment의 스냅샷으로 변경 사항을 반영되지 않음

 

2. LexicalEnvironment

처음에는 VariableEnvironment와 같지만 변경 사항이 실시간으로 반영됨

 

3. ThisBinding

this 식별자가 바라봐야 할 대상 객체


environmentRecord와 outer Environment

VariableEnvironment와 LexicalEnvironment에서는 environmentRecord와 outer Environment 객체를 내장하고 있습니다.

 

> environmentRecord

현재 실행 컨텍스트와 관련된 내부 식별자 정보의 집합으로, hoisting이라고 하며 hoisting은 식별자들을 최상단으로 끌어 올려 놓는 역할을 하며 합니다. environmentRecord 이 하는일을 좀 더 이해하기 쉽게 설명한 추상적인 개념

 

> outer Environment

현재 실행 컨텍스트와 관련된 외부 식별자 정보의 집합, scope chain이라고 하며 위의 예에서 inner 함수의 outer Environment는 outer 함수의 Lexical Environment(environmentRecord+ outer Environment)입니다.

 

 

이렇게 보면 environmentRecord는 현재 실행 컨텍스 내부 식별자 정보의 집합이고 outer Environment는 현재 실행 컨텍스트 외부 식별자 정보의 집합입니다. 이렇게 두개의 객체를 사용하여 현재 실행중인 함수는 외부/내부의 정보에 모두 접근할 수 있습니다. 

 

아래의 사진에서 outer의 environmentRecord + outerEnvironment는 전역 환경의 LexicalEnvironment입니다. 즉 전역환경의 식별자에 접근할 수 있습니다. 하지만 outer의 경우 자신보다 더 작은 스코프인 inner의 environmentRecord에 접근할 수 없습니다. 참조할 수 있는 방법이 없기 때문입니다.

 

 


VariableEnvironment

environmentRecord + outer EnvironmentReference

 

 

 

VariableEnvironment와 LexicalEnvironment의 내부는 environmentRecord와 outer EnvironmentReference로 구성돼 있습니다. 초기화 과정 중에는 사실상 완전히 동일하고 이후 코드 진행에 따라 서로 달라지게 될 것이므로 자세한 내용은 LexicalEnvironment를 통해 함께 살펴보겠습니다.


LexicalEnvironment 

environmentRecord + outer EnvironmentReference

 

LexicalEnvironment은 '현재 컨텍스트의 내부에는 a,b,c와 같은 식별자들이 있고 그 외부 정보는 D를 참조다로고 구성돼 있다'라는, 컨텍스트를 구성하는 환경 정보들을 사전에서 접하는 느낌으로 모아놓은 것입니다.

 

1. environmentRecord와 호이스팅

environmentRecord에는 현재 컨텍스트와 관련된 코드의 식별자 정보들이 저장됩니다. 컨텍스트를 구성하는 함수에 지정된 매개변수 식별자, 선언한 함수가 있을 경우 그 함수 자체, var로 선언된 변수의 식별자 등이 식별자에 해당합니다. 컨텍스트 내부 전체를 처음부터 끝까지 쭉 훑어나가면 순서대로 수집합니다.

 

여기서 중요한 건 environmentRecord은  현재 실행될 컨텍스트의 대상 코드 내에서 어떤 식별자들이 있는지에만 관심있고, 각 식별자들에 어떤 값이 할당될 것인지는 관심이 없습니다.

 

전연 실행 컨텍스트는 변수 객체를 생성하는 대신 자바스크립트 구동 환경이 별도로 제공하는 객체, 즉 전역 객체를 활용합니다. 브라우저의 window, node.js의 global 객체 등이 있습니다. 이들은 자바스크립트 내장 객체가 아닌 호스트 객체로 분류됩니다.

 

이는 코드가 실행되기 전임에도 불구하고 자바스크립트 엔진은 이미 해당 환경에 속한 코드의 변수명을 모두 알고 있게 되는 셈이죠. 그렇다면 엔진의 실제 동작 방식 대신에 '자바스크립트 엔진은 식별자들을 최상단으로 끌어 올려 놓은 다음 실제 코드를 실행한다' 라고 생각해도 코드를 해석하는 데는 문제가 될 것이 전혀 없을 것입니다. 여기서 호이스팅(hoisting)개념이 나오는데 자바스크립트 엔진이 실제로 끌어올리지는 않지만 편의상 끌어올린 것으로 간주하자는 것이죠.

 

호이스팅 규칙

environmentRecord에는 매개변수의 이름, 함수 선언, 변수명 등이 담긴다고 했습니다. 예제를 통해 살펴보겠습니다.

 

※지금부터는 과정은 자바스크립트 엔진의 구동 동작을 좀 더 사람의 입장에서 이해해보고자 코드를 몇 차례 변경할 것입니다. 실제 엔진은 이러한 변환 과정을 거치지 않습니다.

 

1 function a(x){            // 수집 대상 1 : 매개변수
2     console.log(x);       (1)
3     var x;                // 수집 대상 2 : 변수선언
4     console.log(x);       (2)  
5     var x = 2;            // 수집 대산 3 : 변수 선언
6     console.log(x);       (3)
7 }
8 a(1);

이 상태에서 변수 정보를 수집하는 과정, 즉 호이스팅을 처리해 봅시다. environmentRecord는 현재 실행될 컨텍스트의 대상 코드 내에 어떤 식별자들이 있는지에만 관심이 있고, 각 식별자에 어떤 값이 할당될 것인지는 관심이 없습니다. 따라서 변수를 호이스팅할 때 변수명만 끌어 올리고 할당 과정은 원래 그자리에 그대로 남겨둡니다. environmentRecord의 관심사에 맞춰 수집 대상 1,2,3을 순서대로 끌어 올리고 나면 다음과 같은 형태로 바뀝니다.

1 function a(){
2     var x;             // 수집 대상 1의 변수 선언 부분
3     var x;             // 수집 대상 2의 변수 선언 부분
4     var x;             // 수집 대상 3의 변수 선언 부분
5    
6     x = 1;             // 수집 대상 1의 할당 부분
7     console.log(x);    (1)
8     console.oog(x);    (2)
9     x = 2;             // 수집 대상 3의 할당 부분
10    console.log(x);    (3)
11 }
12 a(1);

 

출력되는 결과는 (1) : 1, (2) : 1, (3) : 2  입니다. 값이 할당되는 부분을 그대로 두고 변수명만 끌어올리기 때문에 다음과 같은 결과가 나옵니다. 여기서 매개변수 역시 호이스팅의 대상이 됩니다. 

 

번외로 함수의 경우 함수 선언문, 함수 표현식으로 나누어 생각할 수 있습니다.

함수 선언문은 함수 전체를 호이스팅 하고, 함수 표현식은 변수 선언부만 호이스팅 합니다. 

 

 

2. 스코프, 스포프 체인, OuterEnvironmentReference

스코프(scope)란 식별자에 대한 유효범위 입니다. 어떤 경계 A의 외부에서 선언한 변수는 A의 외부뿐 아니라 A의 내부에서도 접근이 가능하지만 A의 내부에서 선언한 변수는 오직 A의 내부에서만 접근할 수 있습니다. 이러한 '식별자의 유효범위'를 안에서부터 바깥으로 차례로 검색해 나가는 것을 '스코프 체인(scope chain)'이라고 합니다. 그리고 이를 가능케 하는 것이 바로 LexicalEnvironment의 두번째 수집 자료인 outerEnvironmentReference 입니다.

 

스코프 체인

outerEnvironmentReference는 현재 호출된 함수가 선언될 당시의 LexicalEnvironment를 참조 합니다. 과거 시점인 '선언될 당시'에 주목해 주세요. '선언하다'라는 행위가 실제로 일어날 수 있는 시점이란 콜 스택 상에서 어떤 실행 컨텍스트가 활성화된 상태일 때뿐입니다. 어떤 함수를 선언(정의)하는 행위 자체도 하나의 코드에 지나지 않으며, 모든 코드는 실행 컨텍스트가 활성화 상태일 때 실행되기 때문입니다.

 

1 var a = 1;
2 var outer = function() {
3    var inner = function() {
4       console.log(a);
5       var a = 3;
6     };
7   inner();
8   console.log(a);
9  }
10 outer();
11 conosle.log(a);

 

  1. 전역 컨텍스트가 활성화 됩니다. 전역 컨텍스트의 environmentRecord에 {a, outer} 식별자를 저장합니다. 전역 컨텍스트는 선언 시점이 없으므로 전역 컨텍스트의 outerEnvironmentReference에는 아무것도 담기지 않습니다.
  2. 1번째 줄과 2번째 줄 : 전역 컨텍스트에 있는 뱐수 a에 1을, outer에 함수를 할당합니다.
  3. 10번째 줄 : outer함수를 호출합ㄴ디ㅏ. 이에 따라 전역 컨텍스트의 코드는 10번째 줄에서 중단되고 outer 실행 컨텍스트가 활성화 되어 2번째 줄로 이동합니다.
  4. 2번째 줄 : outer 실행 컨텍스트의 environmentRecord에 { inner } 식별자를 저장합니다. outerEnvironment에는 outer 함수가 선언될 당시의 LexicalEnvironment가 담깁니다. outer 함수는 전역 공간에서 선언됐으므로 전역 컨텍스트의 LexicalEnvironment를 참조 복사합니다. 이를 [ GLOBAL, { a, outer } ] 라고 표기 하겠습니다. 첫번째는 실행 컨텍스트의 이름, 두번째를 environmentRecord 객체 입니다.
  5. 3번째 줄 : outer 스코프에 있는 변수 inner에 함수를 할당합니다.
  6. 7번째 줄 : inner 함수를 호출합ㄴ디ㅏ. 이에 따라 outer 실행 컨텍스트의 코드는 7번째 줄에서 임시중단되고, inner 실행 컨텍스트가 활성화 되어 3번째 줄로 이동합니다.
  7. 3번째 줄 : inner 실행 컨텍스트의 environmentRecord에 { a } 식별자를 저장합니다. outerEnvironment에는 inner 함수가 선언될 당시의 LexicalEnvironment가 담깁니다. inner 함수는 outer 함수 내부에서 선언됐으므로 outer 함수의 LexicalEnvironment, 즉 [ outer, { inner ] }를 참조복사 합니다.
  8. 4번째 줄 : 식별자 a에 접근하고자 합니다. 현재 활성화 상태인 inner 컨텍스트의 environmentRecord에서 a를 검색합니다. a가 발견됐는데 여기에는 아직 할당된 값이 없습니다(undefined 출력)
  9. 5번째 줄 : inner 스코프에 있는 변수 3에 3을 할당합니다.
  10. 6번째 줄 : inner 함수 실행이 종료됩니다. inner 실행 컨텍스트가 콜 스택에서 제거되고, 바로 아래의 outer 실행 컨텍스트가 다시 활성화 되면서 앞서 중단했던 7번째 줄의 다음으로 이동합니다.
  11. 8번째 줄 : 식별자 a에 접근하고자 합니다. 이때 자바스크립트 엔진은 활성화된 실행 컨텍스트와 LexicalEnvironment에 접근합니다. 첫 요소의 environmentRecord에서 a가 있는지 찾아보고 없으면 outerEnvironmentReferece에 있는 environmentRecord에 넘어가는 식으로 계속해서 검색합니다. 예제에서는 두번째, 즉 전역 LexicalEnvironment에 a가 있으니 그 a에 저장된 값 1을 반환합니다(1 출력)
  12. 9번째 줄 : outer 함수 실행이 종료됩니다. outer 실행 컨텍스트가 콜 스택에서 제거되고, 바로 아래의 전역 컨텍스트가 다시 활성화 되면서 앞서 중단했던 10번째 줄의 다음으로 이동합니다.
  13. 11번째 줄 : 식별자 a에 접근하고자 합니다. 현재 활설화 상태인 전역 컨텍스트의 environmentRecord에서 a를 검색합니다. 바로 a를 찾을 수 있습니다(1 출력). 이로써 모든 코드의 실행이 완료됩니다. 전역 컨텍스트가 콜 스택에서 제거되고 종료합니다.

 

전역변수와 지역변수

위의 예제에서 전역 변수는 전역 스코프에서 선언한 a와 outer 둘 입니다. 지역 변수는 outer 함수 내부에서 선언한 inner와 inner 함수 내부에서 선언한 a 둘 입니다. 즉 전역공간에서 선언한 변수는 전역변수이고, 함수 내부에서 선언한 변수를 무조건 지역 변수입니다.


ThisBinding

마지막으로 실행 컨텍스트의 thisBinding에는 this로 지정된 객체가 저장됩니다. 실행 컨텍스트 활성화 당시에 this가 지정되지 않은 경우 this에는 전역 객체가 저장됩니다. 그 밖에는 함수를 호출하는 방법에 따라 this에 저장되는 대상이 다릅니다. 이 부분은 다음장, this에서 자세히 배우겠습니다.


참고 : 코어 자바스크립트(Core JavaScript), 정재남

'javascript' 카테고리의 다른 글

[javascript] 클로저  (0) 2020.05.08
[javascript] this  (0) 2020.05.04
[javascript] undefined와 null  (0) 2020.05.02
[javascript] 데이터 타입  (0) 2020.05.01
[javascript] 프로토타입(prototype)  (0) 2019.03.14