실행 컨텍스트

모든 내용은 책을 기반으로 작성되었습니다.

실행 컨텍스트는 실행할 코드에 제공할 환경 정보들을 모아놓은 객체로 동적 언어로서의 성격을 잘 파악할 수 있는 개념이다.

자바스크립트는 어떤 실행 컨텍스트가 활성화되는 시점에 선언된 변수를 위로 끌어올리고(=호이스팅), 외부 환경 정보를 구성하고, this 값을 설정하는 등의 동작을 수행한다.

실행 컨텍스트는 자바스크립트에서 가장 중요한 핵심 개념 중 하나다. 단순하게 자바스크립트 문법을 아는 것보다 실행 컨텍스트를 이해하는 것이 자바스크립트를 이해하는데 더욱 도움된다.

동일한 환경에 있는 코드를 실행 때 필요한 환경 정보들을 모아 컨텍스트를 구성하고, 이를 콜스택에 쌓아 올렸다가 가장 위에 쌓여있는 컨텍스트와 관련있는 코드들을 실행하는 식으로 전체 코드의 환경과 순서를 보장한다.

여기서 '동일한 환경', 즉 하나의 실행 컨텍스트를 구성할 방법은 무엇이 있을까?

  1. 전역공간
  2. eval()
  3. 함수 실행
// -----  (1)
var a = 1;
function outer() {
  function inner() {
    console.log(a); // ???
    var a = 3;
  }
  inner(); // ------  (2)
  console.log(a) // ??
}
outer(); // ------  (3)
console.log(a); // ??

위의 답은 무엇일까? 또한 콜스택의 변화는 어떻게 이루어질까?

Execution Environment

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

실행 컨텍스트에 담기는 정보

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

VariableEnvironment

LexicalEnvironment와 내용이 같지만 최초 실행 시의 스냅샷을 유지한다는 점이 다르다. 실행 컨텍스트를 생성할 때 VariableEnvironment에 정보를 먼저 담은 다음, 그대로 복사해서 LexicalEnvironment를 만들고, 이후에는 LexicalEnvironment를 주로 사용하게 된다.

VariableEnvironment과 LexicalEnvironment의 내부는 environmentRecord와 outerEnvironment로 구성되어 있다. 초기화 과정 중에는 사실상 완전히 동일하고 이후 코드 진행에 따라 서로 달라진다.

LexicalEnvironment

environmentRecord 호이스팅

environmentRecord에는 현재 컨텍스트와 관련된 코드의 식별자 정보들이 저장된다. 컨텍스트를 구성하는 함수에 지정된 매개변수 식별자, 선언한 함수가 있으면 그 함수 자체, var로 선언된 변수의 식별자에 해당한다.

자바스크립트는 코드가 실행되기 전임에도 불구하고 자바스크립트 엔진은 이미 해단 환경에 속한 코드의 변수명들을 모두 알고 있게 된다. 여기서 호이스팅이라는 개념이 등장한다.

호이스팅이란 끌어올리다라는 의미의 hoist에 ing를 붙여 만든 동명사로, 변수 정보를 수집하는 과정을 더욱 이해하기 쉬운 방법으로 대체한 가장의 개념이다. 실제로는 끌어올리지는 않지만, 편의상 끌어올린 것으로 간주한다.

호이스팅 규칙

environmentRecord에는 매개변수의 이름, 함수 선언, 변수명 등이 담긴다.

function a (x) {
  console.log(x); // ??
  var x;
  console.log(x); // ??
  var x = 2;
  console.log(x); // ??
}
a(1)

위에서 출력이 어떻게 되는지 알아보자. 인자들과 함께 함수를 호출한 경우, arguments에 전달된 인자를 담는 것을 제외하면 다음의 코드 내부에서 변수를 선언한 것과 다른 점이 없다.

실제 엔진이 이렇게 처리하지 않는다.

function a (x) { 
  var x = 1;
  console.log(x); // 1
  var x;
  console.log(x); // 1
  var x = 2;
  console.log(x); // 2
}
a()

위와 같은 상태에서 변수 정보를 수집하는 과정인 호이스팅을 처리하게 되면 아래와 같이 변경된다.

function a (x) { 
  var x;
  var x;
  var x;

  x = 1;
  console.log(x); // ???
  console.log(x); // ???
  x = 2;
  console.log(x); // ???
}
a()

위와 같이 변경하고 실제로 코드를 돌리게 되면 결과는 1, 1, 2가 나오게 된다.

함수 선언을 추가해서 예제를 보자

function a () {
  console.log(b); // ???
  var b = 'bbb'; 
  console.log(b); // ???
  function b () { }
  console.log(b); // ???
}
a();

a 함수를 실행하는 순간 a 함수의 실행 컨텍스트가 생성된다. 이때 변수명과 함수 선언의 정보를 위로 끌어올린다.

변수는 선언부와 할당부를 나누어 선언부만 끌어올리지만 함수 선언은 함수 전체를 끌어올린다.

function a () {
  var b; // 변수는 선언부만 끌어올린다.
  function b () { }; // 함수 선언은 전체를 끌어올린다.

  console.log(b); // ???
  b = 'bbb'; 
  console.log(b); // ???
  console.log(b); // ???
}
a();

호이스팅이 끝난 상태에서의 함수 선언문은 함수명으로 선언한 변수에 함수를 할당한 것처럼 여길 수 있다.

function a () {
  var b; // 변수는 선언부만 끌어올린다.
  var b = function b () { }; // 함수 선언은 전체를 끌어올린다.

  console.log(b); // ???
  b = 'bbb'; 
  console.log(b); // ???
  console.log(b); // ???
}
a();

만약 호이스팅의 개념을 알고 있지 않다면, 에러 / 'bbb' / 'b'함수라고 생각했을 것이다. 그러나 우리는 이제 호이스팅으로 답이 'b'함수 / 'bbb' / 'bbb'라는 것을 알 수 있게 되었다.

함수 선언문과 함수 표현식

함수를 정의하는 방법은 두 가지가 있다. 함수 선언문, 함수 표현식이다.

함수 선언문은 function 정의부만 존재하고 별도의 할당 명령이 없는 것을 의미한다. 함수 표현식은 정의한 function을 별도의 변수에 할당하는 것을 말한다.

함수 선언문의 경우 반드시 함수명이 정의돼 있어야 하지만, 함수 표현식은 없어도 된다.

function a () {}

var b = function () { }

var c = function d() { }

한때 디버깅할때 익명 함수표현식보다 기명 함수표현식이 좋을때가 있었다.

이제 예제를 통해서 함수 선언문과 함수 표현식의 실질적인 차이를 살펴보자.

console.log(sum(1,2)); // ???
console.log(multiply(3,4)); // ???


function sum (a, b) {
  return a + b;
}

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

실행 컨텍스트의 LexicalEnvironment는 두 가지 정보를 수집한다. 여기서는 environmentRecord의 정보 수집 과정에서 발생하는 호이스팅이다.

이제 호이스팅이 되었을 경우 어떻게 표현이 가능한지 살펴보자.

var sum = function () {
  return a + b;
}

var multiply;

console.log(sum(1,2));
console.log(multiply(1,2));

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

함수 선언문은 전체를 호이스팅한다. 그러나 함수 표현식은 변수 선언부만 호이스팅했다. 함수도 하나의 값으로 취급할 수 있다는 것이 바로 이런 것이다. 함수를 다른 변수에 값으로써 '할당'한 것이 곧 함수 표현식이다.

함수 선언문의 위험성

console.log(sum(3,4));

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

var a = sum(1, 2);

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

var c = sum(1, 2);
console.log(c);

이렇게 실행했다면 우리는 호이스팅이라는 개념을 알기 때문에 하위에 선언한 함수가 적용되어 값이 출력된다는 것을 예측할 수 있다.

그러나 실제로 위에서 선언한 함수를 인지하지 못하고 하위에서 다시 sum을 선언해서 개발하는 경우가 생긴다. 그렇게 되면 자바스크립트는 실행 컨텍스트를 만드는 과정에서 호이스팅이 발생하는 것을 인지하지 못하고 개발하여 이전에 개발된 것까지 버그가 발생하게 된다.

이렇게 되면 정말 최악의 경우 모든 기능이 마비될 수 있다.

그러나 이를 함수 표현식으로 정의했다면 원래의 의도대로 동작하게 되면서, 디버깅도 손쉽게 되었을 것이다.

스코프, 스코프 체인, outerEnvironmentReference

스코프란 식별자에 대한 유효범위다. 어떤 경계 A의 외부에서 선언한 변수의 A의 외부뿐 아니라 A의 내부에서도 접근이 가능하지만, A의 내부에서 선언한 변수는 오직 A의 내부에서만 접근할 수 있다.

ES5까지의 자바스크립트는 전역공간을 제외하면 오직 함수에 의해서만 스코프가 생성된다. '식별자의 유효범위'를 안에서부터 바깥으로 차례로 검색해나가는 것을 스코프 체인이라고 한다. 그리고 이를 가능하게 하는 것이 LexicalEncironment의 두 번재 수집 자료인 outerEnvironmentReference다.

스코프 체인

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

여러 스코프에서 동일한 식별자를 선언한 경우에는 무조건 스코프 체인 상에서 가장 먼저 발견된 식별자에만 접근 가능하다.

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

  inner();
  console.log(a); // ???
}

outer();
console.log(a); // ???

IMG_2794

스코프 체인 상에 있는 변수라고 해서 무조건 접근 가능한 것은 아니다. a를 전역과 inner 내부에 선언했는데 inner 함수 내부에서 a에 접근하려고 하려고 하면 무조건 스코프 체인 상의 첫 번째 인자인 LexicalEnvironment부터 검색할 수밖에 없다. inner 스코프의 LexicalEnvironment에 a 식별자가 존재하므로 스코프 체인 검색을 더 진행하지 않고 inner 함수의 a를 반환한다. 이를 이용해서 사용하는 기술이 변수 은닉화다.

전역변수와 지역변수

  • 전역변수 : 전역 공간에 선언한 변수.
  • 지역변수 : 함수 내부에서 선언한 변수.

우리가 여태 보았던 것처럼 함수를 선언하는 방법은 여러 가지지만 표현식으로 선언하는 것과 가급적 전역변수 사용을 최소화하고자 노력하는 것이 안정성을 위해서 좋다.

정리

실행 컨텍스트는 실행할 코드에 제공할 환경 정보들을 모아놓은 객체이다. 실행 컨텍스트는 전역 공간에서 자동으로 생성되는 전역 컨텍스트와 eval 및 함수 실행에 의한 컨텍스트가 있다.

실행 컨텍스트가 활성화되는 시점에 VariableEnvironment, LexicalEnvironment, ThisBiding의 세 가지 정보를 수집한다.

실행 컨텍스트를 생성할 때는 VariableEnvironment와 LexicalEnvironment가 동일한 내용으로 구성되지만 LexicalEnvironment는 함수 실행 도중에 변경되는 사항이 즉시 반영된다.

LexicalEnvironment는 매개변수, 변수의 식별자, 선언한 함수의 함수명 등을 수집하는 environmentRecord와 바로 직전 컨텍스트의 LexicalEnvironment 정보를 참조하는 outerEnvironment로 구성되어 있다.

호이스팅

호이스팅은 코드 해석을 좀 더 수월하게 하기 위해 environmentRecord의 수집 과정을 추상화한 개념이다. 실행 컨텍스트가 관여하는 코드 집단의 최상단으로 이들을 '끌어올린다'고 해석하는 것이다.

변수 선언과 값 할당이 동시에 이뤄진 문장은 '선언부'만을 호이스팅하고, 할당 과정은 원래 자리에 남아있게 되는데, 여기서 함수선언문과 함수표현식의 차이가 발생한다.

스코프

스코프는 변수의 유효범위를 말한다. outerEnvironmentReference는 해당 함수가 선언된 위치의 LexicalEnvironment를 참조한다. 코드 상에서 어떤 변수에 접근하려고 하면 현재 컨텍스트의 LexicalEnvironment를 탐색해서 발견되면 그 값을 반환하고, 발견하지 못할 경우 다시 outerEnvironment에 담긴 LexicalEnvironment를 탐색하는 과정을 거친다. 전역 컨텍스트까지 가서 찾지 못하면 undefined를 반환한다.

전역 컨텍스트의 LexicalEnvironment에 담긴 변수를 전역변수라 하고, 그 밖의 함수에 의해 생성된 실행 컨텍스트의 변수들은 모두 지역변수이다. 안전한 코드 구성을 위해 가급적 전역변수의 사용은 최소화하는 것이 좋다.