CoreJS - 실행 컨텍스트

SANGKU OH·2020년 11월 24일
2

CoreJavascript

목록 보기
4/10
post-thumbnail

실행 컨텍스트

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

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

그래서 실행 컨텍스트란?

본격적으로 실행 컨텍스트를 살펴보기에 앞서 스택(stack) & 큐(queue)의 개념을 먼저 살펴보자

스택

스택은 출입구가 하나뿐인 깊은 우물같은 데이터 구조다!
비어있는 스택에 순서대로 데이터 a, b, c, d를 저장했다면, 꺼낼 때는 반대로 d, c, b, a의 순서로 꺼낼 수 밖에 없다.

한편 큐는 양쪽 모두 열려있는 파이프를 떠올리면 됩니다.
종류에 따라 양쪽 모두 입력과 출력이 가능한 큐도 있으나 보통은 한쪽은 입력만, 다른 한쪽은 출력만을 담당하는 구조를 말한다.
이 경우 비어있는 큐에 순서대로 데이터를 a, b, c, d를 저장했다면 꺼낼 때도 역시 a, b, c, d의 순서로 꺼낼 수 밖에 없다.

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

여기서 '동일한 환경', 즉 하나의 실행 컨텍스트를 구성할수 있는 방법으로 전역공간, eval()함수, 함수 등이 있다. 자동으로 생성되는 전역공간과 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

처음 자바스크립트 코드를 실행하는 순간(1) 전역 컨텍스트가 콜 스택에 담긴다.
전역 컨텍스트라는 개념은 일반적으로 실행 컨텍스트와 특별히 다를 것이 없다. 최상단의 공간은 코드 내부에서 별도의 실행 명령이 없어도 브라우저에서 자동으로 실행하므로 자바스크립트 파일이 열리는 순간 전역으로 컨텍스트가 활성화된다고 이해하면 된다!

콜 스택에는 전역 컨텍스트 외에 다른 덩어리가 없으므로 전역 컨텍스트와 관련된 코드들을 순차로 진행하다가 (3)에서 outer를 호출하면 자바스크립트 엔진은 outer에 대한 환경 정보를 수집해서 outer 실행 컨텍스트를 생성한 후 콜 스택에 담는다.
콜 스택의 맨 위에 outer 실행 컨텍스트가 놓인 상태가 됐으므로 전역 컨텍스트와 관련된 코드의 실행을 일시 중단하고 대신 outer 실행 컨텍스트와 관련된 코드, 즉 outer 함수 내부의 코드를 순차로 실행한다.

다시 (2)에서 inner 함수의 실행 컨텍스트가 콜 스택의 가장 위에 담기면 outer 컨텍스트와 관련된 코드의 실행을 중단하고 inner 함수 내부의 코드를 순서대로 진행한다.

inner 함수 내부에서 a 변수에 값을 3을 할당하고 나면 inner 함수의 실행이 종료되면서 inner 실행 컨텍스트가 콜 스택에서 제거된다. 그러면 아래에 있던 outer 컨텍스트가 콜 스택의 맨 위에 존재하게 되므로 중단했던 (2)의 다음 줄부터 이어서 실행한다.
a 변수의 값을 출력하고 나면 outer 함수의 실행이 종료되어 outer 실행 컨텍스트가 콜 스택에서 제거되고, 콜 스택에는 전역 컨텍스트만 남아 있게 된다. 그런 다음, 실행을 중단했던(3)의 다음 줄 부터 이어서 실행한다.
a 변수의 값을 출력하고 나면 전역 공간에 더는 실행할 코드가 남아있지 않아 전역 컨텍스트도 제거되고, 콜 스택에는 아무것도 남지 않는 상태로 종료가 된다!

스택 구조를 잘 생각해보면 한 실행 컨텍스트가 콜 스택의 맨 위에 쌓이는 순간이 곧 현재 실행할 코드에 관여하게 되는 시점임을 알 수 잇다. 기존의 컨텍스트는 새로 쌓인 컨텍스트보다 아래에 위치할 수 밖에 없다.
이렇게 어떤 실행 컨텍스트가 활성화 될 때 자바스크립트 엔진은 해당 컨텍스트에 관련된 코드들을 실행하는데 필요한 환경 정보들을 수집해서 실행 컨텍스트 객체에 저장한다.

이 객체는 자바스크립트 엔진이 활용할 목적으로 생성할 뿐 개발자가 코드를 통해 확인할 수는 없다!
여기에는 아래와 같은 정보가 담기게 되는데..

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

VariableEnvironment

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

VariableEnvironment와 LexicalEnvironment의 내부는 environmentRecord와 outer-EnvironmentReference로 구성되어 있다.
초기화 과정 중에는 사실상 완전히 동일하고 이후 코드 진행에 따라 서로 달라지게 될 것이므로 자세한 내용은 아래에서 좀 더 살펴보자

LexicalEnvironment

사전적인 환경.
예를 들어 백과사전에서 '바나나'를 검색하면 가장 먼저 '칼로리가 가장 높고 당질이 많은 알칼리성 식품으로..'라는 문구가 등장한다. 이와 같은 느낌으로 이해하면 되는데, 즉 "현재 컨텍스트의 내부에는 a, b, c와 같은 식별자들이 있고 그 외부 정보는 D를 참조하도록 구성돼 있다."라는, 컨텍스트를 구성하는 환경 정보들을 사전에서 접하는 느낌으로 모아놓은 것이다.

environmentRecord와 호이스팅

environmentRecord에는 현재 컨텍스트와 관련된 코드의 식별자 정보들이 저장된다.
컨텍스트를 구성하는 함수에 지정된 매개변수 식별자, 선언한 함수가 있을 경우 그 함수 자체, var로 선언된 변수의 식별자등이 식별자에 해당한다.
컨텍스트 내부 전체를 처음부터 끝까지 쭉 훑어나가며 순서대로 수집한다.

여기서 중요한 사실은, 변수 정보를 수집하는 과정을 모두 마쳤더라도 아직 실행 컨텍스트가 관여할 코드들은 실해오디기 전의 상태이다.
코드가 실행되기 전임에도 불구하고 자바스크립트 엔진은 이미 해당 환경에 속한 코드의 변수명들을 모두 알고 있게 되는 셈이다! 그렇다면 엔진의 실제 동작 방식 대신에 '자바스크립트 엔진은 식별자들을 최상단으로 끌어올려놓은 다음 실제 코드를 실행한다.' 라고 생각하더라도 코드를 해석하는 데는 문제될 것이 전혀 없을 것이다!

여기서 호이스팅이라는 개념이 등장한다!

호이스팅

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

호이스팅 규칙

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

function a (x) { // 수집 대상 1(매개변수)
  console.log(x);// (1)
  var x;         // 수집 대상 2(변수 선언)
  console.log(x);// 수집 대상 3(변순 선언)
  var x = 2;
  console.log(x);// (3)
}
a(1)

우선 호이스팅이 되지 않았을 때(1), (2), (3)에서 어떤 값들이 출력될지를 예상해보자
(1)에는 함수 호출시 전달한 1이 출력
(2)는 선언된 변수 x에 할당한 값이 없으므로 undefined 출력되고
(3)에서는 2가 출력

실제로는
arguments에 전달된 인자를 담는 것을 제외하면 아래의 코드처럼 코드 내부에서 변수를 선언한 거소가 다른 점이 없다.
특히 LexicalEnvironment 입장에서는 완전히 같다. 즉 인자를 함수 내부의 다른 코드보다 먼저 선언 및 할당이 이뤄진 것으로 간주할 수 있습니다.

매개변수를 변수 선언/할당과 같다고 간주해서 변환한 상태

function a() {
  var x = 1; // 수집 대상 1(매개변수 선언)
  console.log(x); // (1)
  var x; // 수집대상 2(변수 선언)
  console.log(x); // (2)
  var x = 2; // 수집 대상 3(변수 선언)
  console.log(x); // (3)
}
a();

이 상태에서 변수 정보를 수집하는 과정, 즉 호이스팅을 처리해보자.
environmentRecord
현재 실행될 컨텍스트의 대상 코드 내에 어떤 식별자들이 있는지에만 관심이 있고, 각 식별자에 어떤 값이 할당될 것인지는 관심이 없다. 따라서 변수를 호이스팅할 때 변수명만 끌어올리고 할당 과정은 원래 자리에 그대로 남겨둡니다. 매개변수의 경우도 마찬가지입니다. environmentRecord의 관심사에 맞춰 수집 대상 1, 2, 3을 순서대로 끌어 올리고 나면 다음과 같은 형태로 바뀐다.

function a () {
  var x; // 수집 대상 1의 변수 선언 부분
  var x; // 수집 대상 2의 변수 선언 부분  
  var x; // 수집 대상 3의 변수 선언 부분
  
  x = 1; // 수집 대상 1의 할당 부분
  console.log(x); // (1)
  console.log(x); // (2)
  x = 2; // 수집 대상 3의 할당 부분
  console.log(x); // (3)
}
a(1);

호이스팅이 끝났으니 실제 코드를 실행할 차례입니다(스코프체인 및 수집 할당 this 할당 과정은 추후 논의할 것이므로 여기서는 생략!)

  1. 변수 x를 선언한다. 이 때 메모리에서는 저장할 공간을 미리 확보하고, 확보한 공간의 주솟값을 변수 x에 연결해둔다.
  2. 다시 변수 x를 선언한다. 이미 선언된 변수 x가 있으므로 무시
  3. 다시 변수 x를 선언한다. 이미 선언된 변수 x가 있으므로 무시
  4. x에 1을 할당하라고 한다. 우선 숫자 1을 별도의 메모리에 담고, x와 연결된 메모리 공간에 숫자 1을 가리키는 주솟값을 입력한다.
  5. 각 x를 출력하라고 요청한다. (1), (2) 모두 1이 출력된다.
  6. x에 2를 할당하라고 한다. 숫자 2를 별도의 메모리에 담고, 그 주솟값을 든 채로 x와 연결된 메모리 공간으로 간다. 여기에는 숫자 1을 가리키는 주솟값이 들어있는데, 이걸 2의 주솟값으로 대치한다. 이제 변수 x는 숫자 2를 가리키게 된다.
  7. 마지막으로 x를 출력 하라고 요청한다. 2가 출력되고, 이제 함수 내부의 모든 코드가 실행됐으므로 실행 컨텍스트가 콜 스택에서 제거된다.

함수 선언을 추가한 예제를 하나 더 살펴보자!

함수 선언의 호이스팅 - 원본코드

function a() {
  console.log(b); // (1)
  var b = 'bbb'; // 수집 대상 1(변수 선언)
  console.log(b); // (2)
  function b() { }// 수집 대상 2(함수 선언)
  console.log(b) // (3)
}
a();
  1. 함수를 실행하는 순간 a 함수의 실행 컨텍스트가 생성된다.
  2. 이때 변수명과 함수 선언의 정보를 위로 끌어올린다(수집한다).
  3. 변수는 선언부와 할당부를 나누어 선언부만 끌어올리는 반면 함수 선언은 함수 전체를 끌어 올린다.
  4. 수집 대상 1과 2를 순서대로 끌어올리고 나면 다음과 같은 형태로 변환된다.

함수 선언의 호이스팅 - 호이스팅을 마친 상태

function a() {
  var b;	    // 수집 대상 1. 변수는 선언부만 끌어올립니다.
  function b() { } // 수집 대상 1. 변수는 선언부만 끌어올립니다.
  
  console.log(b);   // (1)
  b = 'bbb';        // 변수의 할당부는 원래 자리에 남겨 둔다.
  console.log(b);   // (2)
  console.log(b);   // (3)
}
a();

해석의 편의를 위해 한 가지만 더 바꿔보자!
호이스팅이 끝난 상태에서 함수 선언문은 함수명으로 선언한 변수에 함수를 할당한 것 처럼 여길 수 있다!

function a(){
  var b;
  var b = function b () { }; // <- 바뀐 부분
  
  console.log(b) // (1)
  b = 'bbb';
  console.log(b); // (2)
  console.log(b); // (3)
}
a();
  1. 변수 b를 선언한다. 이 때 메모리에서는 저장할 공간을 미리 확보하고, 확보한 공간의 주솟값을 변수 b에 연결해둔다.
  2. 다시 변수 b를 선언하고 함수 b를 선언된 변수 b에 할당하려고 한다. 이미 선언된 변수 b가 있으므로 선언 과정은 무시한다. 함수는 별도의 메모리에 담길 것이고, 그 함수가 저장된 주솟값을 b와 연결된 공간에 저장할 것이다. 이제 변수 b는 함수 b를 가리키게 된다.
  3. 변수 b에 할당된 함수 b를 출력한다(1).
  4. 변수 b에 'bbb'를 할당명령을 내린다. b와 연결된 메모리 공간에는 함수가 저장된 주솟값이 담겨있었는데 이걸 문자열 'bbb'가 담긴 주솟값으로 덮어쓴다. 이제 변수 b는 문자열 'bbb'를 가리키게 된다.
  5. (2)와 (3) 모두 'bbb'가 출력되고, 이제 함수 내부의 모든 코드가 실행됐으므로 실행 컨텍스트가 콜 스택에서 제거된다.

호이스팅을 고려하지 않는 상태에서 예상하기로는 (1)에러 또는 undefined, (2)'bbb', (3)b 함수가 나오리라 생각했지만 실제로는 (1)b 함수, (2)'bbb', (3)'bbb'라는 전혀 다른 결과가 나온다.

함수 선언문과 함수 표헌식

호이스팅을 다루는 김에 함께 알아두면 좋을 내용!

함수 선언문

function의 정의부만 존재하고 별도의 할당 명령이 없는 것을 의미!
반드시 함수명이 정의되어 있어야 한다.

함수 표현식

정의한 function을 별도의 변수에 할당하는 것을 의미!
함수명의 정의되어 있지 않아도 된다.

함수를 정의하는 세 가지 방식

function a () { /*...*/ } // 함수 선언문, 함수명 a가 곧 변수명.
a() // 실행 OK.

let b = function() { /*...*/ } // (익명) 함수 표현식. 변수명 b가 곧 함수명.
b() // 실행 OK.

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

함수 선언문과 함수 표현식 - 원본

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

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

let multiply = function (a, b){ // 함수 표현식 multiply
  return a * b;
}

함수 선언문과 함수 표현식 - 호이스팅을 마친 상태

let sum = function sum (a, b) { // 함수 선언문은 전체를 호이스팅 한다.
  return a + b;
}


let multiply;  // 변수는 선언부만 끌어올립니다.

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

multiply = function(a, b) { // 변수의 할당부는 원래 자리에 남겨둡니다.
  return a * b;
};

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

호이스팅이 끝났으니 내부 코드를 차례대로 실행해보자

  1. 메모리 공간을 확보하고 확보된 공간의 주솟감을 변수 sum에 연결한다.
  2. 또 다른 메모리 공간을 확보하고 그 공간의 주솟값을 변수 multiply에 연결한다.
  3. sum 함수를 또 다른 메모리 공간에 저장하고, 그 주솟값을 앞서 선언한 변수 sum의 공간에 할당한다. 이로써 변수 sum은 함수 sum을 바라보는 상태가 된다.
  4. sum을 실행한다. 정상적으로 실행되어 3이 나올 것이다.
  5. 현재 multiply에는 값이 할당돼 있지 않다. 비어있는 대상을 함수로 여겨 실행하라고 명령한 것. 따라서 'multiply is not a function'이라는 에러 메시지가 출력된다. 뒤의 코드는 에러로 인해 실행되지 않은 채 런타임이 종료된다.
profile
Prof.Google을 통해 필요한 정보를 이 곳에 insert 🐸

1개의 댓글

comment-user-thumbnail
2020년 12월 15일

Hello, I enjoy reading all of your article. I like to write a little comment to support you.JOKER123

답글 달기