[Javascript] 실행 컨텍스트(Execution Context)와 실행 스택(Execution Stack)

박기영·2022년 12월 11일
1

Javascript

목록 보기
29/45

자바스크립트 기초 개념에는 호이스팅, 스코프 등이 있다.
이 개념들의 동작 원리를 설명할 때 많이 등장하는 단어가 실행 컨텍스트(Execution Context)이다.
도대체 이 실행 컨텍스트라는건 뭘까?

본 게시글은 Sukhjinder Arora님 블로그의 글을 참고하여 작성하였습니다

실행 컨텍스트란 무엇인가?

실행 컨텍스트(Execution Context)란 자바스크립트 코드가 평가되고 실행되는 환경에 대한 추상적인 개념이다.
자바스크립트에서 어떤 코드가 동작하든, 그것은 실행 컨텍스트 내에서 이뤄진다.

실행 컨텍스트의 타입

자바스크립트에는 3개의 실행 컨텍스트 타입이 있다.

  1. 전역 실행 컨텍스트(Global Execution Context)
  • 초기 혹은 기초(default or base)적인 실행 컨텍스트이다.
    그 어떤 함수에도 속하지 않는 코드들은 전부 전역 실행 컨텍스트에 속한다.
    이는 두 가지 일을 한다.
    window 객체라고 불리는 전역 객체를 생성하기도 하며(브라우저의 경우),
    this의 값을 전역 객체와 같게 세팅한다.
    프로그램에는 오직 하나의 전역 실행 컨텍스트만이 존재한다.
  1. 함수 실행 컨텍스트(Functional Execution Context)
  • 함수가 호출(invoked)될 때마다, 그 함수를 위해서 새로운 실행 컨텍스트가 생성된다.
    각각의 함수는 본인의(own) 실행 컨텍스트를 가지지만, 그것은 함수가 호출될 때 생성된다.
    함수 실행 컨텍스트는 얼마든지(any number of) 존재할 수 있다.
    새로운 실행 컨텍스트가 생성될 때 마다, 뒤에서 다루게 될 정의된 순서(defined order)에 따른 일련의 절차(series of steps)를 거친다.
  1. Eval Function Context - 정확한 의미 파악이 힘들어 해석 생략
  • eval 함수 내에서 실행되는 코드 또한 그들 본인의 실행 컨텍스트를 가진다.
    하지만 eval은 보통 자바스크립트 개발자들이 사용하지 않기 때문에 이는 설명을 생략하고자한다.

실행 스택(Execution Stack)

다른 프로그래밍 언어에서 Calling Stack이라고도 불리는 실행 스택(Execution Stack)
코드가 실행되는 동안 생성되는 모든 실행 컨텍스트를 저장하는데 사용되는
LIFO(Last In First Out) 구조의 스택이다.

자바스크립트 엔진이 당신의 스크립트를 처음 마주쳤을 때,
엔진은 전역 실행 컨텍스트를 생성하고, 그것을 현재(current) 실행 스택에 넣는다(pushes).
엔진이 함수 호출을 찾을 때마다, 그 함수를 위해 새로운 실행 컨텍스트를 만들고,
그것을 스택의 가장 위에 넣는다.

엔진은 스택의 가장 위에 있는 실행 컨텍스트를 가지는 함수를 실행한다.
이 함수가 완료되었을 때, 그것의 실행 스택을 스택에서 빼내고(popped off),
엔진(control, 필자의 의역입니다)은 그 아래에 있던 컨텍스트에 접근한다.

이제 코드 예시를 살펴보자.

let a = "Hello World!";

function first() {
  console.log("Inside first function");
  second();
  console.log("Again inside first function");
}

function second() {
  console.log("Inside second function");
}

first();

console.log("Inside Global Execution Context");

이 코드의 실행 컨텍스트 스택(Execution Context Stack)은 다음과 같다.
Sukhjinder Arora님 블로그에 있는 이미지입니다

참고 이미지

위 코드가 브라우저에 불려왔을 때(loads),
자바스크립트 엔진은 전역 실행 컨텍스트를 생성하고 그것을 현재 실행 스택에 넣는다.
함수 first()의 호출을 마주쳤을 때,
자바스크립트 엔진은 그 함수를 위한 새로운 실행 컨텍스트를 생성하고
현재 실행 스택의 최상단에 그것을 넣는다.

함수 second()가 함수 first() 내에서 호출되었을 때,
자바스크립트 엔진은 그 함수(함수 second)를 위한 새로운 실행 컨텍스트를 생성하고,
현재 실행 스택의 최상단에 그 것을 넣는다.
함수 second()가 완료되었을 때, 그 것의 실행 컨텍스틀를 현재 스택에서 빼내고,
엔진이 그 아래에 있는 실행 컨텍스트(함수 first의 컨텍스트)에 도달한다.

함수 first()가 완료되었을 때,
실행 스택에서 빼내지고 엔진은 전역 실행 컨텍스트에 도달한다.
한번 모든 코드가 실행되고나면, 자바스크립트 엔진은 전역 실행 컨텍스트를 현재의 스택에서 제거한다.

실행 컨텍스트는 어떻게 생성될까?

지금까지 자바스크립트 엔진이 실행 컨텍스트를 어떻게 다루는지 살펴봤으니,
자바스크립트 엔진이 실행 컨텍스트를 어떻게 생성하는지 알아보자.

실행 컨텍스트는 2 단계를 거쳐서 생성된다.

  1. 생성 단계(Creation Phase)
  2. 실행 단계(Execution Phase)

생성 단계

실행 컨텍스트생성 단계(Creation Phase) 중 생성된다.
그 때 일어나는 것은(happen) 아래와 같다.

  1. Lexical Environment 요소(component)가 생성된다.
  2. Variable Environment 요소가 생성된다.

따라서, 실행 컨텍스트는 개념적으로는 아래와 같이 표현될 수 있다.

ExecutionContext = {
  LexicalEnvironment = <ref. to LexicalEnvironment in memory>,
  VariableEnvironment = <ref. to VariableEnvironment in  memory>,
}

Lexical Environment

공식 ES6 문서에서는 Lexical Environment를 아래와 같이 정의한다.

Lexical Environment는 특정한 변수에 대한 Identifiers의 관계와 ECMAScript 코드의 lexical nesting structure에 기반한 함수를 정의하기 위해 사용되는 특정한 타입이다.
Lexical Environment는 Environment Record와 외부 Lexical Environment에 대해 가능한 null 참조(possibly null reference)로 구성된다.

Lexical Environmentidentifier-variable mapping을 담고 있는 구조라고 보면 된다.
여기서 identifier는 변수, 함수의 이름을 가리키며,
variable은 함수 객체, 배열 객체, 원시값을 포함하는 실제 객체(actual object)를 가리킨다.

아래 코드를 예시로 들어보자.

var a = 20;
var b = 40;

function foo() {
  console.log('bar');
}

위 코드의 Lexical Environment는 아래와 같다.

lexicalEnvironment = {
  a: 20,
  b: 40,
  foo: <ref. to foo function>
}

각각의 Lexical Environment는 3개의 요소(components)를 가진다.
불분명한 해석을 피하기위해 번역은 생략했습니다

  1. Environment Record
  2. Reference to the outer environment
  3. This binding

Environment Record

Environment RecordLexical Environment 내에 변수와 함수의 선언이 저장되는 장소이다.
이 때, 2가지 타입의 Environment Record가 존재한다.

  1. Declarative environment record
  • 이름으로부터 변수와 함수의 선언을 저장한다는 것을 알 수 있다.
    함수 코드(function code)를 위한 Lexical EnvironmentDeclarative environment record를 포함한다.
  1. Object environment record
  • 전역 코드(global code)를 위한 Lexical EnvironmentObject environment record를 포함한다.
    변수, 함수 선언과 별도로, Object environment record 또한 전역 바인딩 객체(global binding object)을 저장한다(브라우저에서의 window 객체).
    따라서, 각각의 바인딩 객체의 속성(binding object's property)(브라우저의 경우, 브라우저로부터 window 객체에 제공하는 속성과 메서드들을 포함한다)을 위해서 새로운 entryrecord에 생성된다.

참고로, 함수 코드(function code)의 경우, Environment record는 함수를 지나는 indexes와 인자(arguments) 사이의 관계(mapping, 필자의 의역입니다)와 함수를 지나는 인자(arguments)의 길이(혹은 개수)를 담고있는 arguments 객체를 포함한다.

아, arguments 객체는 다음과 같다.

function foo(a, b) {
  var c = a + b;
}

foo(2, 3);

// argument object
Arguments: {0: 2, 1: 3, length: 2},

Reference to the Outer Environment

Reference to the Outer Environment는 그것의 외부(outer) Lexical Environment에 접근권(access)을 가지고 있다는 것을 의미한다.
이는 자바스크립트 엔진이 만약 현재(current) Lexical Environment에서 변수를 찾지 못할 경우 외부 Environment에서 변수를 찾을 수도 있다는 것을 의미한다.

This Binding

이 요소(component)에서는 this의 값을 결정하거나 세팅(set)한다.

전역 실행 컨텍스트(global execution context)에서 this의 값은 전역 객체(global object)를 가리킨다(브라우저에서 this는 window 객체를 가리킨다.).

함수 실행 컨텍스트(function execution context)에서 this의 값은 함수가 어떻게 호출됐느냐에 따라 달라진다.
만약, 함수가 객체 참조(object reference)로 호출됐다면, this의 값은 바로 그 객체로 세팅된다.
그렇지 않은 경우에는, this의 값이 전역 객체(global object) 혹은 undefined(엄격 모드(strict mode)에서)로 세팅된다.

아래 예시를 보자.

const person = {
  name: "peter",
  birthYear: 1994,
  calcAge: function () {
    console.log(2018 - this.birthYear);
  },
};

person.calcAge();
// 'calcAge'가 'person' 객체 참조를 통해 호출되었으므로 'this'는 'person'을 가리킨다.

const calculateAge = person.calcAge;
calculateAge();
// 어떠한 객체 참조도 이뤄지지 않았기 때문에 'this'는 전역 window 객체를 가리킨다.

Lexical Environment는 추상적으로 다음과 같을 것이다.

GlobalExectionContext = {
  LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Object",
      // Identifier 바인딩은 여기로 간다.
    }
    outer: <null>,
    this: <global object>
  }
}
FunctionExectionContext = {
  LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Declarative",
      // Identifier 바인딩은 여기로 간다.
    }
    outer: <Global or outer function environment reference>,
    this: <depends on how function is called>
  }
}

Variable Environment

Variable Environment 또한 실행 컨텍스트와 함께 VariableStatements에 의해 생성되는 바인딩을 가지는 Environment Record가 있는 Lexical Environment이다.

방금 말했듯이, Variable Environment 또한 Lexical Environment이다.
따라서, 위에서 언급된 Lexical Environment의 모든 속성(properties)과 요소(components)를 가지게 된다.

ES6에서, Lexical EnvironmentVariable Environment의 한 가지 차이는,
전자는 함수의 선언과, 변수(let, const) 바인딩 저장에 사용되는 반면,
후자는 변수(var) 바인딩 저장에만 사용된다는 것이다.

실행 단계

이 단계에서는 모든 변수의 할당이 끝났고, 코드가 마침내 실행된다.

예시를 살펴보자.

let a = 20;
const b = 30;
var c;

function multiply(e, f) {
  var g = 20;
  return e * f * g;
}

c = multiply(20, 30);

위 코드가 실행될 때,
자바스크립트 엔진은 전역 코드(global code)를 실행하기 위해 전역 실행 컨텍스트를 생성한다.
따라서, 전역 실행 컨텍스트생성 단계(Creation Phase)에서 아래와 같이 보일 것이다.

GlobalExectionContext = {
  LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Object",
      // Identifier 바인딩은 여기로 간다.
      a: < uninitialized >,
      b: < uninitialized >,
      multiply: < func >
    }
    outer: <null>,
    ThisBinding: <Global Object>
  },
  VariableEnvironment: {
    EnvironmentRecord: {
      Type: "Object",
      // Identifier 바인딩은 여기로 간다.
      c: undefined,
    }
    outer: <null>,
    ThisBinding: <Global Object>
  }
}

혹시나, 각 변수들의 값이 왜 저런지 이해가 되지않는다면 필자의 호이스팅에 대한 게시글을 참고해주세요.

실행 단계(Execution Phase) 동안, 변수 할당이 완료된다.
따라서, 전역 실행 컨텍스트실행 단계에서 아래와 같이 보일 것이다.

GlobalExectionContext = {
  LexicalEnvironment: {
      EnvironmentRecord: {
        Type: "Object",
        // Identifier 바인딩은 여기로 간다.
        a: 20,
        b: 30,
        multiply: < func >
      }
      outer: <null>,
      ThisBinding: <Global Object>
    },
  VariableEnvironment: {
      EnvironmentRecord: {
        Type: "Object",
        // Identifier 바인딩은 여기로 간다.
        c: undefined,
      }
      outer: <null>,
      ThisBinding: <Global Object>
    }
  }

함수 multiply(20, 30)의 호출을 마주쳤을 때,
함수 코드를 실행하기 위해 새로운 함수 실행 컨텍스트가 생성된다.
따라서, 생성 단계에서의 함수 실행 컨텍스트는 아래와 같이 보일 것이다.

FunctionExectionContext = {
  LexicalEnvironment: {
      EnvironmentRecord: {
        Type: "Declarative",
        // Identifier 바인딩은 여기로 간다.
        Arguments: {0: 20, 1: 30, length: 2},
      },
      outer: <GlobalLexicalEnvironment>,
      ThisBinding: <Global Object or undefined>,
    },
  VariableEnvironment: {
      EnvironmentRecord: {
        Type: "Declarative",
        // Identifier 바인딩은 여기로 간다.
        g: undefined
      },
      outer: <GlobalLexicalEnvironment>,
      ThisBinding: <Global Object or undefined>
  }
}

이 이후에, 실행 컨텍스트실행 단계(Execution Phase)로 넘어가며,
이는 함수 내에 있는 변수의 할당이 완료되었음을 의미한다.
따라서, 함수 실행 컨텍스트실행 단계에서 아래와 같이 보일 것이다.

FunctionExectionContext = {
  LexicalEnvironment: {
      EnvironmentRecord: {
        Type: "Declarative",
        // Identifier 바인딩은 여기로 간다.
        Arguments: {0: 20, 1: 30, length: 2},
      },
      outer: <GlobalLexicalEnvironment>,
      ThisBinding: <Global Object or undefined>,
  },
  VariableEnvironment: {
      EnvironmentRecord: {
        Type: "Declarative",
        // Identifier 바인딩은 여기로 간다.
        g: 20
      },
      outer: <GlobalLexicalEnvironment>,
      ThisBinding: <Global Object or undefined>
  }
}

함수가 완료된 이후에, 반환된 값은 변수 c에 저장된다.
따라서 전역(global) Lexical Environment가 업데이트된다.
이후에, 전역 코드가 완료되고, 프로그램이 종료된다.

참고로, 생성 단계에서 let, const가 어떠한 값과도 연결되지않는 반면,
varundefined로 세팅되는 것을 볼 수 있다.

이는 생성 단계에서, 변수와 함수 선언을 위한 코드가 스캔될 때,
함수 선언은 그것의 전체가 environment에 저장되는 반면,
변수는 처음에 undefined로 세팅되거나(var의 경우),
uninitialized 상태로 남아있기 때문이다(let, const의 경우).

이게 바로 var가 선언되기 전에 접근할 수 있는 이유이며(undefined일지라도),
let, const가 선언되기 전에 접근하면 참조 에러(ReferenceError)가 발생하는 이유이다.

이 개념이 바로, 호이스팅(hoisting)이라고 불리는 것이다.

참고로, 실행 단계 동안,
자바스크립트 엔진이 소스 코드 내 let 변수가 실제로 선언된 곳에서 그 값을 찾을 수 없다면,
엔진은 let 변수에 undefined를 할당할 것이다.

결론

위에서 살펴본 개념들이 자바스크립트 개발자에게 꼭 필요한 것은 아니지만,
호이스팅, 스코프, 클로저 등을 더 쉽고 깊게 이해할 수 있도록 도와준다.
실제로 필자도 호이스팅, 스코프를 공부하다가 여기까지 왔답니다...또로록...

참고 자료

Sukhjinder Arora님 블로그
catsbi님 블로그
황준일님 블로그

profile
나를 믿는 사람들을, 실망시키지 않도록

0개의 댓글