자바스크립트의 실행 컨텍스트(Execute Context)와 실행 스택(Execute Stack) 이해하기

Dean H. Park·2020년 7월 13일
8

JS

목록 보기
5/8
post-thumbnail

이 글의 원문 저작권은 Sukhjinder Arora에게 있습니다.

만약 당신이 자바스크립트 개발자이거나 되길 원하는 사람이라면, 반드시 자바스크립트의 내부 동작 원리를 알아야 한다. 실행 컨텍스트와 실행 스택의 이해는 호이스팅, 스코프 그리고 클로저와 같은 자바스크립트 컨셉을 이해하기 위한 필수이다.

확실한 실행 컨텍스트와 실행 스택의 이해는 당신을 더 나은 자바스크립트 개발자로 만들어 줄 것이다. 그러니 더이상 왈가왈부 하지말고 시작해 보자 :)

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

간단히 말해서, 실행 컨텍스트란 자바스크립트 코드가 평가 및 실행되는 환경의 추상적인 개념이다. 자바스크립트에서 동작하는 어느 코드이건, 실행 컨텍스트 안에서 동작한다.

실행 컨텍스트의 종류

자바스크립트에는 총 3가지의 실행 컨텍스트가있다.

  • 전역 실행 컨텍스트
    실행 컨텍스트의 기본이자 기초이다. 함수 밖에 있는 코드는 전역 실행 컨텍스트에 있다. 브라우저의 경우 window 객체를 생성하여, 이를 글로벌 객체로 설정한다. 프로그램에는 오직 한개의 전역 실행 컨텍스트만 있을 수 있다.

  • 함수 실행 컨텍스트
    함수가 실행될 때마다, 해당 함수에 대한 완전 새로운 실행 컨텍스트가 만들어진다. 각 함수는 고유의 실행 컨텍스트를 갖지만, 함수가 실행되거나 call 될 때만 생성된다. 함수 실행 컨텍스트의 수는 제한이 없다. 새로운 실행 컨텍스트가 생성될 때마다, (이 글 뒷 부분에 언급될) 정의된 순서에 따라 일련의 단계를 거친다.

  • Eval 실행 컨텍스트
    eval 함수 내부에서 실행되는 코드도 고유의 실행 컨텍스트를 갖고 있다. 하지만, eval이 자바스크립트 개발자들에게 쓰이는 일이 잘 없는 관계로, 여기서는 논의하지 않을 것이다.

실행 스택 (Execute Stack) 이란?

다른 프로그래밍 언어에서 "호출 스택 (Calling Stack)"이라고도 불리는 실행 스택은 후입선출(Last in, First out, LIFO) 구조의 스택이고, 코드 실행 시 생성되는 모든 실행 컨텍스트가 저장되는데 사용된다.

자바스크립트 엔진이 작성된 스크립트를 처음 마주할 때, 전역 실행 컨텍스트를 생성하고, 현재 실행 스택에 push 한다. 엔진이 함수 호출을 발견 할 때마다, 함수를 위한 새로운 실행 컨텍스트를 생성하고 스택의 꼭대기에 push 한다.

엔진은 스택 최상단에 있는 함수의 실행 컨텍스트를 실행한다. 이 함수가 완료되면 해당 함수의 실행 스택이 현 스택에서 pop 되고 제어는 바로 그 아래 컨텍스트에 도달한다.

아래 예제를 통해 이해해보자.

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');


위 코드가 브라우저에서 로드될 때, 자바스크립트 엔진은 전역 실행 컨텍스트를 생성하고 현재 스택에 push한다. first() 호출이 발생할 때, 자바스크립트엔진은 이 함수에 대한 새 실행 컨텍스트를 생성하고 현재 실행 스택의 가장 위에 push 한다.

first() 안에 있는 second() 함수가 호출되면, 자바스크립트 엔진은 이 함수의 실행 컨텍스트를 생성하고 현재 스택의 가장 위에 push 한다.

second() 함수가 끝나면, 이 함수의 실행 컨택스트는 현재 스택에서 pop되고, 제어는 그 아래 first() 실행 컨택스트에 닿는다.

first()가 끝나면, 스택에서 이 함수의 실행 스택이 제거되고 전역 실행 컨텍스트로 제어가 닿는다. 모든 코드가 실행되고 나면, 자바스크립트 엔진은 현재 스택에서 전역 실행 컨텍스트를 제거한다.

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

지금까지 우리는 자바스크립트 엔진이 어떻게 실행 컨텍스트를 관리하는지 살펴보았고, 이제는 실행 컨텍스트가 자바스크립트 엔진으로 부터 어떻게 생성되는지 알아볼 것이다.

실행 컨텍스트는 1)생성 단계2)실행 단계로 생성된다.

생성 단계

실행 컨텍스트는 생성단계에서 생성된다. 생성 단계에서 다음과 같은 일이 발생한다.

  • 렉시컬 환경 컴포넌트가 생성된다.
  • 변수 환경 컴포넌트가 생성된다.

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

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

렉시컬 환경 (Lexical Environment)

ES6 공식 사이트 에서는 다음과 같이 정의 한다.

렉시컬 환경은 ECMAScript 코드의 렉시컬 중첩 구조에 기초하여 특정 변수 및 함수에 대한 식별자의 연관성을 정의하는 데 사용되는 사양이다.

간단히 말하면, 렉시컬 환경은 생성자-변수 매핑을 보유한 구조이다.
(여기서 식별자는 변수/함수의 이름을 말하며, 변수는 실제 객체[함수 객체 및 배열 객체 포함] 또는 원시 값에 대한 참조다.)

예제:

var a = 20;
var b = 40;
function foo() {
  console.log('bar');
}

위 예제의 렉시컬 환경은 다음과 같다.

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

각 렉시컬 환경은 총 3개의 컴포넌트를 갖는다.

  1. 환경 레코드 (Environment Record)
  2. 외부환경 참조 (Reference to the outer environment)
  3. This 바인딩 (This Binding)

환경 레코드

환경 레코드는 렉시컬 환경 내에 변수와 함수 선언이 저장되는 곳이다.

환경 레코드는 두가지 타입이 있다.

  • 렉시컬 환경 레코드
    이름에서 알수 있듯, 변수와 함수 선언 저장을 뜻한다. 함수 코드를 위한 렉시컬 환경은 렉시컬 환경 레코드를 포함한다.

  • 객체 환경 레코드
    전역 코드의 렉시컬 범위는 객체적 환경 레코드를 포함한다. 변수, 함수 선언과 별개로, 객체 환경 레코드는 전역 바인딩 객체(브라우저는 윈도우 객체)를 저장한다. 그래서 각 바인딩 객체의 프로퍼티에 대해 (브라우저의 경우, 브라우저에서 윈도우 오브젝트에 제공하는 속성 및 메서드를 포함한다), 레코드에 새로운 항목이 생성된다.

Note — 함수 코드의 경우, 환경 레코드에는 함수에 전달된 index~arguments 간의 매핑과 함수에 전달된 arguments의 length(number)가 포함된 arguments 객체도 포함되어 있다.

argument 객체를 아래 함수로 예를 들어보자.

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

foo(2, 3);

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

외부 환경 참조

외부 환경 참조는 외부 렉시컬 환경에 대한 접근을 의미한다. 이는 자바스크립트 엔진이 현재 렉시컬 환경에서 변수들을 찾을 수 없다면, 외부 환경의 내부에서 찾을 수 있다는 것을 뜻한다.


This 바인딩 (This Binding)

이 컴포넌트에서, this의 값은 결정되거나 set 된 것이다.

전역 실행 컨텍스트에서, this의 값은 전역 객체를 참조한다.
(브라우저일 시, 윈도우 객체 참조)

함수 실행 컨텍스트에서, this의 값은 함수가 어떻게 호출되는지에 달렸다. 만약 객체 참조로부터 호출되면, this의 값은 해당 객체로 set 되고, 그렇지않을 경우, 전역 객체 또는 undefined(strict 모드)로 set 된다.

예제:

const person = {
  name: 'peter',
  birthYear: 1994,
  calcAge: function() {
    console.log(2018 - this.birthYear);
  }
}
person.calcAge(); 
// 'this' 는 'person'을 참조한다.
//왜냐하면, 'calcAge'는 'person' 객체 참조로 호출됐기 때문이다.

const calculateAge = person.calcAge;
calculateAge();
// 'this' 는 전역 윈도우 객체를 참조한다.
// 왜냐하면, 아무럼 객체 참조도 주어지지 않았기 때문이다.

추상적인 렉시컬 환경은 다음과 같다.

pseudocode:
GlobalExectionContext = {
  LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Object",
      // Identifier bindings go here
    }
    outer: <null>,
    this: <global object>
  }
}
FunctionExectionContext = {
  LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Declarative",
      // Identifier bindings go here
    }
    outer: <Global or outer function environment reference>,
    this: <depends on how function is called>
  }
}

변수 환경 (Variable Environment)

이것은 (실행 컨텍스트 내부의 변수 선언문으로 부터 생성된) 바인딩을 보유한 환경 레코드인 렉시컬 환경이기도 하다.

상기 내용처럼, 변수 환경은 렉시컬 환경이기도 한데, 그래서 모든 프로퍼티와 위에 정의된 렉시컬 환경의 컴포넌트들을 갖고 있다.

ES6에서, 렉시컬 환경 컴포넌트와 변수 환경 컴포넌트의 차이점은, 전자는 함수 선언과 변수(let and constance) 바인딩을 저장하는 데 사용되며, 후자는 변수(var) 바인딩만 저장하는 데 사용된다.

실행 단계

현 단계에서 모든 변수들은 할당이 끝나고 코드는 마침내 실행 된다.

예제:

위 컨셉을 이해하기 위해 아래 코드를 살펴보자.

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

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

c = multiply(20, 30);

위 코드가 실행되면, 자바스크립트 엔진은 전역 코드를 실행하기 위해 전역 실행 컨텍스트를 생성한다. 전역 실행 컨텍스트는 생성 단계 동안 하기와 같이 보일 것이다.

GlobalExectionContext = {
  LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Object",
      // Identifier bindings go here
      a: < uninitialized >,
      b: < uninitialized >,
      multiply: < func >
    }
    outer: <null>,
    ThisBinding: <Global Object>
  },
  VariableEnvironment: {
    EnvironmentRecord: {
      Type: "Object",
      // Identifier bindings go here
      c: undefined,
    }
    outer: <null>,
    ThisBinding: <Global Object>
  }
}

실행 단계 동안, 변수 할당은 마무리 된다. 전역 실행 컨텍스트는 실행 단계 동안 하기와 같이 보일 것이다.

GlobalExectionContext = {
LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Object",
      // Identifier bindings go here
      a: 20,
      b: 30,
      multiply: < func >
    }
    outer: <null>,
    ThisBinding: <Global Object>
  },
VariableEnvironment: {
    EnvironmentRecord: {
      Type: "Object",
      // Identifier bindings go here
      c: undefined,
    }
    outer: <null>,
    ThisBinding: <Global Object>
  }
}

함수 multiply(20, 30) 호출이 발생되면, 새로운 함수 실행 컨텍스트가 함수 코드를 실행하기 위해 생성된다. 그렇게 함수 실행 컨텍스트는 생성 단계 동안 아래와 같이 보일 것이다.

FunctionExectionContext = {
LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Declarative",
      // Identifier bindings go here
      Arguments: {0: 20, 1: 30, length: 2},
    },
    outer: <GlobalLexicalEnvironment>,
    ThisBinding: <Global Object or undefined>,
  },
VariableEnvironment: {
    EnvironmentRecord: {
      Type: "Declarative",
      // Identifier bindings go here
      g: undefined
    },
    outer: <GlobalLexicalEnvironment>,
    ThisBinding: <Global Object or undefined>
  }
}

이후, 실행 컨텍스트는 실행 단계를 지나가는데, 이는 함수 내부의 변수 할당이 끝났다는 것을 뜻한다. 실행 단계 동안 함수 실행 컨텍스트는 아래와 같이 보일 것이다.

FunctionExectionContext = {
LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Declarative",
      // Identifier bindings go here
      Arguments: {0: 20, 1: 30, length: 2},
    },
    outer: <GlobalLexicalEnvironment>,
    ThisBinding: <Global Object or undefined>,
  },
VariableEnvironment: {
    EnvironmentRecord: {
      Type: "Declarative",
      // Identifier bindings go here
      g: 20
    },
    outer: <GlobalLexicalEnvironment>,
    ThisBinding: <Global Object or undefined>
  }
}

이후 함수는 완료되고, 리턴값은 c 내부에 저장된다. 그리고 전역 렉시컬 환경은 업데이트 된다. 그 후, 전역 코드는 완료되고 프로그램은 종료된다.


Note — 눈치챘겠지만, 변수로 선언된 letconst는 생성 단계동안 관계된 어느 값도 갖고있지 않다. 하지만, 변수로 선언된 varundefined로 set 돼있다.

그 이유는 다음과 같다.

생성 단계에서는 코드에서 변수 및 함수 선언을 스캔하고, 함수 선언은 환경에 전체적으로 저장되며, 변수는 처음에 정의되지 않은(var의 경우) 또는 초기화되지 않은(let, constance의 경우) 상태로 설정된다.

정의된 변수가 선언되기 전에 (undefined 로) var 정의 변수에 액세스할 수 있지만 선언되기 전에 letconst 변수에 액세스할 때 참조 에러가 발생하는 이유다.

이를 호이스팅 (Hoisting)이라고 한다.


Note — 실행 단계 동안, 만약 자바스크립트 엔진이 소스 코드에 선언된 실제 위치에서 let 값을 찾을 수 없는 경우, undefined 값을 할당한다.


결론

어떻게 자바스크립트 프로그램이 내부적으로 실행되는지 논의해 보았다. 멋진 자바스크립트 개발자가 되기 위해 이 모든 개념을 배울 필요는 없지만, 위의 개념을 제대로 이해하면 호이스트, 스코프, 클로져와 같은 다른 개념들을 더 쉽고 깊게 이해할 수 있도록 도와줄 것이다.

profile
Hi, I'm dean. Front-end developer who likes UI/UX Design.

0개의 댓글