본문 바로가기

Backend/Javascript

Javascript - 실행 컨텍스트란

- 실행 컨텍스트 ?

 

자바스크립트는 실행 컨텍스트가 활성화되는 시점에 선언된 변수를 상단으로 끌어 올리고 (호이스팅), 외부 환경 정보를 구성하고, this 값을 설정하는 등의 동작을 수행하는데 이 과정에서 다른 언어에선 볼 수 없는 현상들이 발생합니다.

여기서 실행 컨텍스트 (execution context) 는 실행할 코드에 제공할 환경 정보들을 모아놓은 객체로, 동적 언어로서의 자바스크립트 성격에 대해 가장 잘 파악할 수 있는 개념입니다.

 

자바스크립트는 동일한 환경에 있는 코드들을 실행할 때 필요한 환경 정보들을 모아 컨텍스트를 구성하고, 이를 콜 스택에 넣었다가 가장 위에 있는 컨텍스트와 관련 있는 코드들을 실행하는 식으로 전체 코드의 환경과 순서를 보장합니다.

하나의 실행 컨텍스트를 구성할 수 있는 방법으로 전역공간, eval(), 함수 등이 있는데 개발자가 흔히 실행 컨텍스트를 구성하는 방법은 함수를 실행하는 방법 뿐입니다.

 

* 참고 : 이번 포스팅에선 호이스팅 개념 설명도 필요한데, let 과 const 에선 호이스팅이 일어나지 않는다 라고 할 순 없지만 var 와 다르므로 코드 설명 시 이제는 거의 쓰지 않는 var 키워드를 사용하도록 하겠습니다.

 

var a = 1;

function outer() {
  function inner() {
    console.info(a); // undefined
    var a = 3;
  }
  
  inner();
  console.info(a); // 1
}

outer();
console.info(a); // 1

 

위의 코드에서 콜 스택에 실행 컨텍스트가 어떤 순서로 쌓이고, 어떤 순서로 코드 실행에 관여하는지 그림으로 나타내면 아래와 같습니다.

 

 

  1. 제일 먼저 자바스크립트 코드가 실행되면 전역 컨텍스트가 콜 스택에 담깁니다.
  2. 전역 컨텍스트와 관련된 코드들을 순차로 진행하다가 outer() 라인에서 자바스크립트 엔진은 outer 에 대한 환경 정보를 수집해서 outer 실행 컨텍스트를 생성한 후 콜 스택에 담습니다.
  3. 전역 컨텍스트와 관련된 코드 실행을 일시 중단하고 outer 실행 컨텍스트와 관련된 코드를 실행합니다. (즉 outer 함수 내부 코드)
  4. inner() 라인에서 inner 함수의 실행 컨텍스트가 outer 때와 동일하게 콜스택에 담기게 되고, 자바스크립트는 inner 함수 내부 코드를 진행합니다.
  5. 이어서 내부 함수가 종료되면, inner 는 콜스택에서 제거되고, 순차적으로 outer, 전역 컨텍스트가 제거되게 됩니다.

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

 

  • VariableEnvironment : 현재 컨텍스트 내의 식별자들에 대한 정보 + 외부 환경 정보 (선언 시점의 LexicalEnvironment 의 스냅샷이며, 변경 사항은 반영되지 않음)
  • LexicalEnvironment : 처음엔 VariableEnvironment 와 같지만 변경 사항이 실시간으로 반영됨
  • ThisBinding : this 식별자가 바라봐야 할 대상 객체

 

- VariableEnvironment (변수 환경) 

 

실행 컨텍스트를 생성할 때 VariableEnvironment 에 정보를 먼저 담은 다음, 이를 복사해서 LexicalEnvironment 를 만들고 이후엔 LexicalEnvironment 를 주로 활용하게 됩니다.

 

VariableEnvironment 와 LexicalEnvironment 의 내부는 environmentRecordouterEnvironmentReference 로 구성되어 있습니다.

 

- LexicalEnvironment 

 

직역하면 어휘적 환경, 정적 환경이라 할 수 있고 좀 더 의미를 담자면 사전적 환경 이라고 할 수 있습니다.

 

• environmentRecord & 호이스팅

 

environmentRecord 에는 현재 컨텍스트와 관련된 코드의 식별자 정보들이 저장됩니다.

식별자에는 컨텍스트를 구성하는 함수에 지정된 매개변수 식별자, 선언한 함수가 있을 경우 그 함수 자체, var 로 선언된 변수의 식별자 등이 해당됩니다.

컨텍스트 내부 전체를 처음부터 끝까지 훑으며 순서대로 수집합니다.

 

변수 정보를 수집하는 과정을 모두 마쳤어도 아직 실행 컨텍스트가 관여하는 코드들을 실행되기 전 상태이므로, 이는 즉 자바스크립트 엔진이 코드 실행 전 이미 해당 환경에 속한 코드의 변수명들을 모두 알고 있다는 뜻입니다.

위의 말은, 자바스크립트 엔진은 식별자들을 최상단으로 끌어올려 놓은 다음 실제 코드를 실행한다 라고 자바스크립트 엔진 동작 방식을 받아들여도 무방합니다. 이 개념이 호이스팅 입니다.

실제로 끌어 올리는건 아니지만, 자바스크립트 엔진이 끌어 올린 것으로 간주하는 것입니다.

 

• 호이스팅 규칙

 

function a(x) { // (1)
  console.info(x); // 10
  var x; // (2)
  console.info(x); // 10
  var x = 2; // (3)
  console.info(x); // 2
}

a(10);

---------------------

function a() {
  var x; // (1)
  var x; // (2)
  var x; // (3)
  
  x = 10;
  console.info(x); // 10
  console.info(x); // 10
  x = 2;
  console.info(x); // 2
}

a(10);

 

실제로 자바스크립트 엔진이 코드를 이렇게 변환하진 않지만, 위 코드가 호이스팅 되면 아래와 같이 변한다고 보시면 될 것 같습니다.

environmentRecord 는 현재 실행될 컨텍스트의 대상 코드 내에 식별자들엔 관심이 있지만, 각 식별자에 할당되는 값에는 관심이 없습니다. 따라서 변수를 호이스팅할 때 변수명만 끌어올리고 나머지 과정은 그대로 둡니다.

그래서 아래와 같은 형태가 되고, 이 상태에서 코드를 실행하면 10 10 2 가 출력되게 됩니다.

 

var x; 로 선언한 이후의 console.info(x); 가 undefined 로 나오지 않을까 생각하신 분들에게 이 설명이 도움되었으면 합니다.

호이스팅에 대해 다른 예시를 보도록 하겠습니다.

 

function a() {
  console.info(b);
  var b = 'bbb';
  console.info(b);
  function b() { }
  console.info(b);
}

a();

--------------------

function a() {
  var b;
  function b() { }
  
  console.info(b); // [Function: b]
  b = 'bbb';
  console.info(b); // bbb
  console.info(b); // bbb
}

a();

 

호이스팅 시 변수는 선언부와 할당부를 나눠 선언부만 끌어올리지만, 함수 선언은 함수 전체를 끌어 올립니다. 따라서 위와 같은 형태가 되고, 출력 결과물도 [Function: b] 'bbb' 'bbb' 가 되는 것입니다.

 

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

 

function tmp() { } // 함수 표현식

var tmp = function () { } // 함수 선언식

 

위에서 쭉 예시로 들었던 함수 코드가 함수 선언문 스타일이고, 함수 표현식은 변수에 함수를 할당하는 스타일입니다.

함수를 정의하는데 이 두 방법은, 호이스팅에서 차이를 보입니다.

 

console.info(sum(1, 2));
console.info(multiply(3, 4));

function sum (x, y) {
  return x + y;
}

var multiply = function (x, y) {
  return x * y;
};

------------------------

var sum = function sum (x, y) {
  return x + y;
};
var multiply;

console.info(sum(1, 2)); // 3
console.info(multiply(3, 4)); // TypeError: multiply is not a function

multiply = function (a, b) {
  return a * b;
};

 

위에서 호이스팅은 함수 선언 시 함수 전체를 끌어 올린다고 했습니다. 하지만 함수 표현식에선 변수 선언부만 호이스팅 됩니다.

따라서 sum 은 함수 호출이 선언 전에 이뤄져도 문제가 없지만, multiply 는 에러를 발생시킵니다.

 

언뜻보기엔 그럼 함수 선언문이 더 좋은거 아니야? 라고 할 수 있지만 함수 표현식을 쓰는게 좋습니다.

우선 자연스러운 부분을 보자면, 당연히 '선언을 한 후에 호출할 수 있다' 가 맞을 것입니다.

또한 코드 작성시 함수 선언식으로 작성된 기존 함수를 놓치고 같은 함수명으로 다시 함수 선언을 했을 때, 호이스팅되는 함수 선언 때문에 덮어씌워진 함수로 인해 심각한 장애를 초래할 수 있습니다. (물론 요즘의 환경에서 이런일은 거의 일어나지 않습니다)

 

상대적으로 함수 표현식이 함수 선언식보다 안전하므로, 함수 표현식을 사용할 것을 권장하는 것입니다.

 

- 스코프, 스코프 체인, outerEnvironmentReference

 

스코프란 식별자에 대한 유효범위입니다.

ES5 까지의 자바스크립트는 전역 공간을 제외하면 오직 함수에 의해서만 스코프가 생성됩니다.

이러한 식별자의 유효범위를 안에서부터 바깥으로 차례로 검색해나가는 것을 스코프 체인이라 합니다.

이 부분은 LexicalEnvironment 의 outerEnviromentReference 에 의해 가능합니다.

 

• 스코프 체인

 

outerEnvironmentReference 는 현재 호출된 함수가 선언될 당시의 LexicalEnvironment 를 참조합니다.

예를 들어 A 함수 내부에 B 함수를 선언하고 다시 B 함수 내부에 C 함수를 선언한 경우, 함수 C 의 outerEnvironmentReference 는 함수 B 의 LexicalEnvironment 를 참조하고, 함수 B 의 outerEnvironmentReference 는 함수 A 의 LexicalEnvironment 를 참조합니다.

 

이처럼 outerEnvironmentReference 는 연결리스트 형태를 띄며, 계속 타고 올라가다보면 전역 컨텍스트의 LexicalEnvironment 가 그 마지막에 있을 것입니다. 각 outerEnvironmentReference 는 오직 자신이 선언된 시점의 LexicalEnvironment 만 참조하고 있어 가장 가까운 요소부터 차례로 접근이 가능하며 다른 순서로 접근하는건 불가능합니다.

 

이러한 구조적 특성 덕분에 여러 스코프에서 동일한 식별자를 선언한 경우 무조건 스코프 체인 상에서 가장 먼저 발견된 식별자에만 접근 가능합니다. 아래 코드 예시를 보겠습니다.

 

var a = 1;
var outer = function () {
  var inner = function () {
    console.info(a); // undefined
    var a = 3;
  };
  
  inner();
  console.info(a); // 1
};

outer();
console.info(a); // 1

 

  1. 전역 컨텍스트가 활성화되고, 전역 컨텍스트의 environmentRecord 에 { a, outer } 식별자를 저장합니다. outerEnvironmentReference 에는 아무것도 담기지 않습니다.
  2. a 에 1, outer 에 함수를 할당합니다.
  3. outer 함수를 호출하고, outer 실행 컨텍스트가 활성화 됩니다.
  4. outer 실행 컨텍스트의 environmentRecord 에는 { inner } 식별자를 저장합니다. outer 실행 컨텍스트의 outerEnvironmentReference 에는 outer 함수가 선언될 당시의 LexicalEnvironment 가 담깁니다. 전역 공간에서 선언되었기에 그대로 참조 복하사여 [ GLOBAL, { a, outer } ] 가 됩니다. (실행 컨텍스트의 이름, environmentRecord 객체 순서)
  5. inner 에 함수를 할당합니다.
  6. inner 함수를 호출하고, inner 실행 컨텍스트가 활성화 됩니다.
  7. inner 실행 컨텍스트의 environmentRecord 에 { a } 식별자를 저장합니다. outerEnvironmentReference 에는 inner 함수가 선언될 당시의 LexicalEnvironment 가 담깁니다. [ outer, { inner }]
  8. 4번 라인에서 식별자 a 에 접근합니다. inner 컨텍스트의 environmentRecord 에서 a 를 검색하는데 식별자는 있지만 할당된 값은 없으므로 undefined 를 출력합니다.
  9. 변수 a 에 3을 할당합니다.
  10. inner 함수가 종료되고 다시 outer 실행 컨텍스트가 활성화 됩니다.
  11. 9번 라인에서 a 에 접근합니다. 현재 활성화된 실행 컨텍스트의 environmentRecord 에서 a 를 찾는데 없으므로 outerEnvironmentReference 에 있는 environmentRecord 로 넘어가서 검색을 합니다. 전역 컨텍스트의 environmentRecord 에 a 가 있고 1이 할당되어 있으니 여기선 1을 출력합니다.
  12. outer 함수가 종료되고 전역 컨텍스트가 활성화 됩니다.
  13. 13번 라인에서 a 에 접근합니다. 전역 컨텍스트의 environmentRecord 에서 a 를 검색하고, 바로 찾아서 1을 출력합니다.
  14. 모든 코드 실행이 완료되었습니다.

정리하자면, 전역 공간에서는 전역 스코프에서 생성된 변수에만 접근할 수 있습니다. outer 함수 내부에서는 outer 및 전역 스코프에서 생성된 변수에 접근할 수 있지만 inner 스코프 내부에서 생성된 변수에는 접근할 수 없습니다. inner 함수 내부에서는 inner, outer, 전역 스코프에 모두 접근할 수 있습니다.

 

위 과정을 다시 보면 inner 스코프의 LexicalEnvironment 에 a 식별자가 존재하므로 스코프 체인 검색을 더 진행하지 않고 즉시 inner LexicalEnvironment 의 a 를 반환합니다. inner 함수 내부에서 a 변수를 선언했기에 전역 공간에서 선언한 동일한 이름의 a 변수에는 접근할 수 없는 셈이고, 이를 변수 은닉화 라고 합니다.

 

* 참고 : ES6 에선 스코프가 조금 다릅니다. 그 내용은 다른 포스팅에서 정리해보겠습니다.

 

이러한 예시들을 보다보면 결국 전역 환경의 문제점이 드러납니다. 어디서든 접근이 가능한 스코프이므로 어떤 함수에서든 전역 환경에 있는 변수의 변경을 시도할 수 있습니다. 이처럼 코드의 안전성을 위해 전역 환경에서의 함수, 변수 사용은 최소화하는 것이 좋습니다.