[코어 자바스크립트] 02.실행 컨텍스트

·

8 min read

실행 컨텍스트

실행 컨텍스트(execution context) 는 실행할 코드에 제공할 환경 정보들을 모아넣은 객체 ( JavaScript 가 왜 동적 언어인지 성격을 가장 잘 파악할 수 있는 개념 )

  • 동일한 환경에 있는 코드들을 실행할 때 필요한 환경 정보들을 모아 컨텍스트를 구성하고, 이를 콜 스택(call stack) 에 쌓아올렸다가, 스택 자료구조처럼 가장 위에 쌓여있는 컨텍스트와 관련 있는 코드들을 실행하는 식으로 전체 코드의 환경과 순서를 보장
  • 동일한 환경 = 하나의 실행 컨텍스트를 구성할 수 있는 방법
    • 전역 공간
    • eval() 함수
    • 일반 함수
  • 이 중 우리가 흔히 실행 컨텍스트를 구성하는 방법은 함수를 실행 하는 것뿐
// 예제
// ------------------------------- (1)
var a = 1;
function outer() {
  function inner() {
    console.log(a); // undefined
    var a = 3;
  }
  inner();  // ------------------- (2)
  console.log(a); // 1
}
outer();  // --------------------- (3)
console.log(a); // 1;

image.png

실행 컨텍스트가 활성화될 때 자바스크립트 엔진은 해당 컨텍스트에 관련된 코드들을 실행하는 데 필요한 환경 정보들을 수집해서 실행 컨텍스트 객체에 저장한다.

  • 단, 이 객체는 자바스크립트 엔진이 활용할 목적으로 생성할 뿐 개발자가 코드를 통해 확인할 수는 없다.
    • VariableEnviroment
      • 현재 컨텍스트 내의 식별자들에 대한 정보 + 외부 환경 정보
      • 선언 시점의 LexicalEnviroment 의 스냅샷으로, 변경 사항은 반영되지 않음.
    • LexicalEnviroment
      • 처음에는 VariableEnviroment 와 같지만 변경 사항이 실시간으로 반영됨.
    • ThisBinding
      • this 식별자가 바라봐야 할 대상 객체

VariableEnviroment

VariableEnviroment 에 담기는 내용 LexicalEnviroment 와 같다. 하지만, 최초 실행 시의 스냅샷을 유지한다는 점만 다르다.

  • 실행 컨텍스트를 생성할 때 VariableEnviroment 에 정보를 먼저 담는다.
  • 이를 그대로 복사해서 LexicalEnviroment 를 만든다.
  • 이후에는 LexicalEnviroment 를 주로 활용한다.

즉, 초기화 과정 중에는 VariableEnviroment 과 LexicalEnviroment 의 내부는 동일하다.

이후 코드 진행에 따라 LexicalEnviroment 내부가 변경되어 달라지는 것이다.

LexicalEnviroment

예) "현재 컨텍스트 내부에는 a,b,c 식별자들이 있고 그 외부 정보는 d 를 참조하도록 구성되어 있다."

라는, 컨텍스트를 구성하는 환경 정보들을 사전에서 접하는 느낌으로 모아놓은 것

enviromentRecord

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

다음이 enviromentRecord 에 저장되는 정보들이다. 이는 컨텍스트 내부 전체를 처음부터 끝까지 쭉 훑어나가며 순서대로 수집한다.

  • 함수에 지정된 매개변수 식별자.
  • 선언한 함수가 있을 경우 그 함수 자체.
  • 선언된 변수의 식별자.

변수 정보를 수집하는 과정을 모두 마쳤더라도 아직 실행 컨텍스트가 관여할 코드들은 실행되기 이전 상태이다.

  • 코드가 실행되기 전임에도 불구하고 자바스크립트 엔진은 이미 해당 환경에 속한 코드의 변수명들을 모두 알고 있게 되는 셈이다.
  • 엔진의 실제 동작 방식 대신에 “자바스크립트 엔진은 식별자들을 최상단으로 끌어올려놓은 다음 실제 코드를 실행한다.” 라고 생각할 수 있다. → 호이스팅(Hosting)

호이스팅(Hositing) & 규칙

“끌어올리다” 의미로, 변수 정보를 수집하는 과정을 더욱 이해하기 쉬운 방법으로 대체한 가상의 개념

  • 즉, 자바스크립트 엔진이 실제로 끌어올리지는 않지만 편의상 끌어올린 것으로 간주하는 것

변수에 대한 호이스팅

function a() {
  var x = 1;      // 수집 대상 1
  console.log(x); // 1 ------ (1)

  var x;          // 수집 대상 2
  console.log(x); // 1 ------ (2)  << undefined 가 아님 ! 🔎

  var x = 2;      // 수집 대상 3
  console.log(x); // 2 ------ (3)
}

a(1);

enviromentRecord 는 현재 실행될 컨텍스트의 대상 코드 내에 어떤 식별자들이 있는지에만 관심이 있고, 각 식별자에 어떤 값이 할당될 것인지는 관심이 없다.

  • 따라서, 변수를 호이스팅할 때 변수명만 끌어올리고 할당 과정은 원래 자리에 그대로 남겨둔다. (매개변수도 마찬가지.)
function a() {
  var x; // 수집 대상 1의 변수 선언 부분
  var x; // 수집 대상 2의 변수 선언 부분
  var x; // 수집 대상 3의 변수 선언 부분

  x = 1; // 수집 대상 1의 할당 부분
  console.log(x); // 1 ------ (1)

  console.log(x); // 1 ------ (2)  << undefined 가 아님 ! 🔎

  x = 2; // 수집 대상 3
  console.log(x); // 2 ------ (3)
}

a(1);

함수 선언의 대한 호이스팅

function a() {
  console.log(b); // [Function: b] ------- (1)

  var b = "bbb";  // 수집 대상 1(변수 선언)
  console.log(b); // bbb ----------------- (2)

  function b() {} // 수집 대상 2(함수 선언)
  console.log(b); // bbb ----------------- (3)
}

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

  console.log(b); // [Function: b] ------- (1)

  b = "bbb";      // 변수의 할당부는 원래 자리에 남겨둔다.
  console.log(b); // bbb ----------------- (2)

  console.log(b); // bbb ----------------- (3)
}

a();
function a() {
  var b;
  var b = function b() {};

  console.log(b); // [Function: b] ------- (1)

  b = "bbb"; // 수집 대상 1(변수 선언)
  console.log(b); // bbb ----------------- (2)

  console.log(b); // bbb ----------------- (3)
}

a();

함수 선언문과 함수 표현식

둘 다 함수를 새롭게 정의할 때 쓰이는 방식이다.

  • 함수 선언문(function declaration)

    • 반드시 함수명이 정의되어 있어야 한다. ( 기명 함수 표현식 )
  • 함수 표현식(function expression)

    • 함수명이 없어도 된다. (= 익명 함수 표현식 )
function a() { ... }  // 함수 선언문 = 함수명 a 가 곧 변수명
a();  // 실행 O

var b = function() { ... }  // (익명)함수 표현식 = 변수명 b가 곧 함수명
b();  // 실행 O

var c = function d() { ... }  // 기명 함수 표현식 = 변수명은 c, 함수명은 d
c();  // 실행 O
d();  // 에러 !

함수 선언문과 함수 표현식에 대한 호이스팅 비교 예제

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

// sum 함수 (by. 함수 선언문)
function sum(a, b) {
  return a + b;
}

// multiply 함수 (by. 함수 표현식)
var multiply = function (a, b) {
  return a * b;
};
// sum 함수 (by. 함수 선언문) -> 함수 전체가 호이스팅됨.
var sum = function sum(a, b) {
  return a + b;
};

// multiply 함수 (by. 함수 표현식) -> 변수 선언부만 호이스팅됨.
var multiply;

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

// multiply 함수 (by. 함수 표현식)
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);

// ..생략..

위와 같이 상단에 선언한 함수와 이후 코드 어딘가에 동일한 이름으로 다시 선언한 경우가 있다고 가정하자.

  • 전역 컨택스트가 활성화될 때 전역공간에 선언된 함수들이 모두 가장 위로 끌어올려진다.
  • 동일한 변수명에 서로 다른 값을 할당한 경우 나중에 할당한 값이 먼저 할당한 값을 덮어씌우게 된다. (= 오버라이딩, override)

이것을 함수 표현식을 사용한다면 비교적 조기에 에러를 발견하거나 안전하게 사용 가능하다.

// ..생략..

console.log(sum(3, 4);  // Uncaught Type Error: sum is not a function

// ..생략..

var sum = function (x, y) {  // 함수 선언문 -> 함수 표현식
    return x + y;
}

// ..생략..

var a = sum(1, 2);

// ..생략..

var sum = function (x, y) {  // // 함수 선언문 -> 함수 표현식
    return x + ' + ' + y + ' = ' + (x + y);
}

// ..생략..

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

// ..생략..
  • 함수 선언문 대신 함수 표현식으로 함수를 선언한다면
    • 함수 선언 이전에 함수를 호출하는 부분에서 에러가 검출 되므로 더 빠른 타이밍에 디버깅 가능

원활한 협업을 위해서는 전역공간에 함수를 선언하거나 동명의 함수를 중복 선언하는 경우는 없어야만 한다. 만약, 동명의 함수가 있더라도 모든 함수가 함수 표현식 으로 정의되어 있다면 함수 선언문으로 선언되었을 때와 비교적 빨리 디버깅이 가능해진다.

스코프, 스코프체인, outerEnviromentReference

스코프(Scope) : 식별자에 대한 유효범위

  • 자바스크립트 ES5 까지는 전역공간을 제외하면 오직 함수에 의해서만 스코프가 형성되었다.
    • ES6 부터는 블록 에서도 스코프 경계가 발생하게 함으로써 다른 언어와 비슷한 스코프 형성이 가능해짐
    • 단, var 변수가 아닌 let, const, class, strict mode 에서의 함수 선언 등에 대해서만 범위로서의 역할을 수행
    • ES6 에서는 둘을 구분하기 위해 함수 스코프(var), 블록 스코프(let, const ...) 라는 용어를 사용

스코프 체인(Scope Chanining) : 이러한 식별자의 유효범위를 안에서부터 바깥으로 차례로 검색해나가는 것

  • 이것을 가능케 하는 것이 바로 LexicalEnviroment 의 두 번째 수집 자료인 outerEnviromentReference

outerEnviromentReference

  • 현재 호출된 함수가 선언될 당시의 LexicalEnviroment 를 참조한다.
  • “선언하다." 라는 행위가 실제로 일어날 수 있는 시점 = 콜 스택 상에서 어떤 실행 컨텍스트가 활성화된 상태일 때
    • 함수를 선언(정의)하는 행위 자체도 하나의 코드에 지나지 않으며, 모든 코드는 실행 컨텍스트가 활성화 상태일 때 실행되기 때문
  • outerEnviromentReference 는 연결리스트(Linked List) 형태를 띈다.
    • 선언 시점의 LexicalEnviroment 를 계속 찾아 올라가면 마지막엔 전역 컨텍스트의 LexicalEnviroment 가 있을 것이다.
  • 또한, 각 outerEnviromentReference 는 오직 자신이 선언된 시점의 LexicalEnviroment 만 참조하고 있으므로 가장 가까운 요소부터 차례대로만 접근할 수 있고 다른 순서로 접근하는 것은 불가능
    • 이런 구조적 특성 덕분에 여러 스코프에서 동일한 식별자를 선언한 경우에는 무조건 스코프 체인 상에서 가장 먼저 발견된 식별자에만 접근 가능

변수 은닉화(variable shadowing) : 내부 함수에 선언된 식별자와 내부 함수 외에 선언된 식별자(like. 전역변수) 이름이 같을 때, 스코프 체인 상 가장 가까운 위치 상의 식별자로 접근하므로, 외부에 존재하는 식별자에는 접근할 수 없는 셈인 것

전역변수와 지역변수

  • 전역변수(global variable) = 전역 공간에서 선언한 변수는 전역 변수
  • 지역변수(local variable) = 함수 내부에서 선언한 변수는 무조건 지역변수

정리

  • 실행 컨텍스트는 실행할 코드에 제공할 환경 정보들을 모아놓은 객체
    • 실행 컨텍스트 객체는 활성화되는 시점에 VariableEnviroment, LexicalEnviroment, ThisBinding 의 세 가지 정보를 수집
  • 실행 컨텍스트를 생성할 때는 VariableEnviroment 과 LexicalEnviroment 가 동일한 내용으로 구성되지만 LexicalEnviroment 는 함수 실행 도중에 변경되는 사항이 즉시 반영되는 반면 VariableEnviroment 는 초기 상태를 유지
  • VariableEnviroment 와 LexicalEnviroment 는 매개변수명, 변수의 식별자, 선언한 함수의 함수명 등을 수집하는 enviromentRecord 와 바로 직전 컨텍스트의 LexicalEnviroment 정보를 참조하는 outerEnviromentReference 로 구성
  • 호이스팅(Hosting)은 코드 해석을 좀 더 수월하게 하기 위해 enviromentRecord 의 수집 과정을 추상화한 개념
    • 실행 컨텍스트가 관여하는 코드 집단의 최상단으로 이들을 “끌어올린다” 고 해석
    • 변수 선언과 값 할당이 동시에 이뤄지는 문장은 선언부만 호이스팅 하고 할당 과정은 원래 자리에 남아있게 된다.
    • 여기서 함수 선언문함수 표현식의 차이가 발생
  • 스코프(Scope)는 변수의 유효범위를 의미한다.
    • outerEnviromentReference 는 해당 함수가 선언된 위치의 LexicalEnviroment 를 참조
    • 코드 상에서 어떤 변수에 접근하려고 하면 현재 컨텍스트의 LexicalEnviroment 를 탐색해서 발견되면 그 값을 반환
    • 발견하지 못하면 다시 outerEnviromentReference 에 담긴 LexicalEnviroment 를 탐색하는 과정을 반복
    • 전역 컨텍스트의 LexicalEnviroment 까지 탐색해도 해당 변수를 찾지 못하면 undefined 를 반환
  • 전역 컨텍스트의 LexicalEnviroment 에 담긴 변수를 전역변수, 그 밖의 함수에 의해 생성된 실행 컨텍스트의 변수들을 모두 지역변수
    • 안전한 코드 구성을 위해 가급적 전역변수 사용은 지양하고 지역변수를 사용하는 것이 좋다.