본문 바로가기
공부/JS

[JavaScript] 실행 컨텍스트에 대하여

by Piva 2023. 12. 23.
  • 자바스크립트를 써온지도 몇 년, 당장 원하는 기능의 코드를 짜낼 수는 있어도 정작 언어 자체를 깊이 공부해본 적이 없다는 생각이 들었다.
  • 지금이라도 부족했던 기본을 다지기 위해, 책과 이런저런 문서를 참고하며 조금씩 공부를 하고 있다. 블로그에서는 책을 읽으며 공부한 것을 요약하여 정리하고자 한다.
  • 책의 경우, 코어 자바스크립트를 보고 있다. 조만간 모던 자바스크립트 Deep Dive 또한 참고하며 이런저런 내용을 공부할 예정이다.
    (책을 나눔해준 버블버블에게 무한한 감사...)

실행 컨텍스트(Execution Context)란?

: 실행할 코드에 제공할 환경 정보들을 담아둔 객체. 클로저를 지원하는 대부분의 언어에 이것 비슷한 개념들이 존재한다고.

→ 어떠한 컨텍스트가 실행되면, 호이스팅 발생/외부 환경정보 구성/this 값 설정 등의 다양한 일이 일어난다.

 

컨텍스트와 콜 스택

동일한 환경에 있는 코드를 실행할 때, 자바스크립트는 필요한 환경 정보를 모아서 컨텍스트를 구성한다.

여기서 '동일한 환경'이란 전역 / eval() / 함수 등이 있다. 우리는 주로 함수를 통해 실행 컨텍스트를 구성하게 된다.

이때, 컨텍스트는 '콜 스택'에 들어가게 되어, 늦게 실행된 코드의 컨텍스트가 가장 위에 올라오게 됨과 동시에 가장 먼저 실행되는 구성을 갖는다. 이는 함수 실행 순서와 환경을 보장하게 한다.

 

// 1. 전역 컨텍스트

function A () {
	/* ... */
    
    function B () { // 3. A 함수 내부의 B 함수 호출
    	/* ... */
    }
    B();
}

A(); // 2. A 함수 호출

예를 들어, 위와 같은 코드에서 콜 스택은 다음과 같이 변화한다.

(1) 전역      

코드를 맨 처음 실행할 시, 전역 컨텍스트가 첫 번째로 콜 스택에 담긴다.

(2) 전역 A    

2번에서 A함수가 실행되면, A함수의 실행 컨텍스트가 콜 스택에 추가된다.

(3) 전역 A B  

3번에서 A함수 내부의 B함수가 실행되면, 콜 스택에 B함수의 실행 컨텍스트가 추가된다.

(3) 종료 전역 A    

B함수가 종료되면, B함수의 실행 컨텍스트가 콜 스택에서 제거되고, 콜 스택 최상단에 A함수의 실행 컨텍스트가 위치하므로 A함수의 실행을 계속한다.

(2) 종료 전역      

A함수의 실행도 종료되면, A함수의 실행 컨텍스트가 콜 스택에서 제거되며 전역 컨텍스트가 최상단에 위치한다.

전역 종료        

전역 공간에 더 실행할 코드가 남지 않으면 콜 스택에서 전역 컨텍스트 또한 제거된다.

 

 

실행 컨텍스트 객체의 구성

실행 컨텍스트에는 다음과 같은 객체들이 담긴다.

  • VariableEnvironment
  • LexicalEnvironment
  • ThisBinding

 

VariableEnvironment

: 실행 컨텍스트 생성 시, 환경 정보들을 VariableEnvironment에 먼저 저장한다.

이후로 VariableEnvironment에 담긴 내용은 변경되지 않기 때문에, 어떤 코드를 최초 실행했을 때의 스냅샷을 그대로 유지한다.

 

LexicalEnvironment

: VariableEnvironment의 내용물이 복사되어서 만들어진다.

 

VariableEnvironment와 LexicalEnvironment에는 아래의 정보들이 담긴다.

 

environmentRecord

: 현재 컨텍스트와 관련된 코드의 식별자 정보를 갖는다. 여기서 식별자는 아래의 항목들이 해당된다.

  • var로 선언된 변수 식별자
  • 선언한 함수 자체
  • 함수에 지정된 매개변수 식별자

이 때, 코드를 살피며 식별자를 '순서대로' 수집한다.

식별자는 코드 실행에 따라 가리키는 내용이 변할 것이다. 그렇지만 이 식별자 수집 과정에 의해, JS는 코드가 실행되기도 전에 코드 내 어떤 식별자가 존재하는지 미리 알 수 있다. 이것이 마치 식별자를 코드 최상단으로 끌어올린 것과 비슷하다고 하여 '호이스팅(hoisting)'이 등장하게 된다.

 

※ 호이스팅 (Hoisting)

function foo (a) {
    console.log(a);
    var a;
    console.log(a);
    var a = 'hello';
    console.log(a);
}

foo('Hi');

 

컨텍스트의 식별자 수집으로 인한 호이스팅을 고려하여 위 함수의 출력값을 예상할 수 있다.

 

  •  environmentRecord 구성 시, 그 값에 관계 없이 '식별자'를 수집한다고 했다. 따라서 foo 함수 내에 변수 선언 부분을 위로 끌어올린 것과 같이 작동한다. 값은 관계없으므로 할당 과정은 빼고 끌어올린다. 이 때, 함수의 인자 또한 똑같이 수집되는데, environmentRecord 입장에서는 인자가 가장 먼저 선언된 변수와 똑같이 작용하기 때문이다.
function foo () {
    var a; // 인자
    var a;
    var a;
    
    a = 'hi';
    console.log(a);
    console.log(a);
    a = 'hello';
    console.log(a);
}

foo('hi');
  • 호이스팅 과정에 의해 원본 코드는 위의 코드와 똑같이 동작한다. 첫 번째 출력에서 'hi', 두 번째에서 'hi', 세 번째에서 'hello'로 출력되는 것이다.

함수 선언에도 호이스팅은 똑같이 동작한다.

function foo () {
    console.log(a);
    var a = 'hello';
    console.log(a);
    function a () {}
    console.log(a);
}

foo();

 

  • environmentRecord의 수집 대상에는 선언한 함수 자체도 포함되므로, 변수 선언과 함수 선언이 모두 호이스팅된다.
function foo () {
    var = a;
    function a () {}

    console.log(a);
    a = 'hello';
    console.log(a);
    console.log(a);
}

foo();
  • 호이스팅 과정에 의해 원본 코드는 위의 코드와 똑같이 동작한다. 첫 번째 출력에서 a 함수, 두 번째에서 'hello', 세 번째에서 'hello'로 출력될 것이다.

 

 

※ 함수표현식과 함수선언문

새로운 함수의 선언 방식에는 위의 2가지가 있다.

  • 함수 선언문: 별도의 할당 없이 function 정의부만 존재하는 것.
// example
function foo () { /* ... */ }
  • 함수 표현식: 정의한 function을 별도의 변수에 할당하는 것.
// example
const foo = function () { /* ... */ };
const foo = function bar () { /* ... */ };

함수 표현식으로 정의한 함수는, 함수를 변수에 할당하는 모양이므로 호이스팅 시 선언부만 끌어올려진다. 반면 앞서 언급했듯, 함수 선언문은 environmentRecord에 의해 전체가 호이스팅된다. 즉, 함수선언문으로 함수를 선언할 시 원치 않는 호이스팅이 발생할 가능성이 있으며, 이로 인한 혼선이 생길 수 있다!

 

outerEnvironmentReference

: 현재 호출된 함수가 선언될 당시의 LexicalEnvironment를 참조한다.

 

function A () {
	/* ... */
    
    function B () {
    	/* ... */
        
        function C () {
        	/* ... */
        }
        C();
    }
    B();
}

A();

이 코드를 예시로 outerEnvironmentReference를 살펴보면 아래와 같다.

  • 함수 B의 outerEnvironmentReference는 함수 B가 선언되던 때의(== 함수 A) LexicalEnvironment를 참조한다.
  • 함수 C의 outerEnvironmentReference는 함수 C가 선언되던 때의(== 함수 B) LexicalEnvironment를 참조한다.

위와 같은 식으로 LexicalEnvironment를 참조해 올라갈 수 있다. (그렇기에 outerEnvironmentReference는 연결리스트 형태를 띄움을 알 수 있다)

또한, 각 outerEnvironmentReference는 자신이 선언되던 때의 LexicalEnvironment를 참조하므로, 이 순서를 거스르는 것은 불가하다.(더 상위의 LexicalEnvironment에 바로 접근할 수 없다)

 

이러한 특징은 스코프 체인(Scope Chain)의 발생이 가능하도록 한다.

 

 

스코프 & 스코프 체인(Scope & Scope chain)

스코프

: 식별자에 대한 유효 범위를 의미한다.

  • 본래 JS는 ES5까지 함수 스코프만이 생성되었다. 다만 ES6부터는 블록 스코프 또한 생성이 가능하게 되었다.

 

스코프 체인

: 식별자의 유효범위, 즉 스코프를 안쪽에서 바깥쪽 방향으로 검색해나가는 것을 의미한다.

  • 여기서 위에서 다룬 outerEnvironmentReference 가 활용되는데, outerEnvironmentReference는 연결 리스트 형태로 이루어져 있기 때문에 스코프 체인 상에서 가장 가까운 쪽부터 먼쪽으로 탐색이 가능하다.
var foo = 'Hello';
var A = function () {
    var B = function () {
    	console.log(foo);
        var foo = 'world';
    }
    B();
    console.log(foo);
}
A();
console.log(foo);
  • 함수 B가 실행될 때, outerEnvironmentReference에는 함수 A의 LexicalEnvironment를 참조한다.
  • 함수 B 내부의 console.log가 실행된다. foo가 호이스팅에 의해(environmentRecord의 식별자 수집) 값이 할당되지 않은 채로 접근이 가능하기 때문에, undefined가 출력된다.
  • 함수 B의 실행이 끝나고 콜 스택에서 B의 실행 컨텍스트가 제거되면, 남은 함수 A를 실행한다.
  • 함수 A 내부의 console.log가 실행된다. 함수 A 내부(LexicalEnvironment의 environmentRecord)에는 foo에 대한 정보가 없으므로, outerEnvironmentReference를 통해 바깥의 environmentRecord를 검색한다.
  • 전역의 LexicalEnvironment에 foo가 있으므로, 해당 foo의 값 Hello를 출력한다. 이로써 함수 A의 실행이 종료되고, 콜 스택에서 제거된다.
  • 남은 전역 코드가 실행된다. console.log(foo)에 의해 Hello가 출력되고, 전역 코드의 실행이 끝나며 콜 스택에서 제거된다.

 

변수 은닉화(Variable Shadowing)

: 동일한 식별자가 다수의 스코프 내에 존재할 경우, 어떤 스코프에서는 다른 스코프의 식별자에 접근하지 못하는 것.

  • 위의 스코프 체인 예시 코드에서, 함수 B는 내부에서 이미 식별자 foo를 선언했기 때문에 다른 스코프에 존재하는 foo에 접근할 수 없다.

 

ThisBinding(this)

: this로 지정된 객체가 저장된다.


  • 실행 컨텍스트는 콜 스택에 대해서만 어렴풋이 알고, 자세한 내용을 본 적이 없어서 새로웠다.
  • 컨텍스트를 구성하는 요소들의 이름이 (길어서) 아직 헷갈리는데... 꾸준히 보면서 어렵지 않게 떠올릴 수 있도록 복습해야겠다.

 

 

 

 

 

 

 

'공부 > JS' 카테고리의 다른 글

[Javascript] 클로저 알아보기 - 2  (1) 2024.01.08
[Javascript] 클로저 알아보기 - 1  (1) 2024.01.05
Typescript 정리 - 2  (1) 2023.01.16
Typescript 정리 - 1  (1) 2023.01.10
Clean Code Study - 2  (0) 2023.01.03