TIL 96 | 코어자바스크립트(4) 실행 컨텍스트

hyounglee·2021년 1월 1일
1

JavaScript

목록 보기
37/46

시대의 명작 코어 자바스크립트를 읽고 자바스크립트 마스터에 도전합니다...

실행 컨텍스트

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

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

동일한 환경, 즉 하나의 실행컨텍스트를 구성할 수 있는 방법으로는 전역공간, eval() 함수, 함수, 블록 {} 이 있다.

예제

// ------------------- (1) 전역 컨텍스트
var a = 1;
function outer() {
  function inner() {
    console.log(a); // undefined
    var a = 3;
  }
  inner(); // -------- (3)  inner 함수 컨텍스트
  console.log(a); // 1
}
outer(); // ---------- (2) outer 함수 컨텍스트
console.log(a); // 1
  1. 전역 컨텍스트는 자바스크립트 파일이 열리는 순간 자동으로 활성화된다.
  2. 콜 스택에는 전역 컨텍스트 외에 다른 덩어리가 없으므로 코드들을 순차로 진행하다가 (3)에서 outer 함수를 호출하면 자바스크립트 엔진은 outer에 대한 환경 정보를 수집해서 outer 실행 컨텍스트 생성 후 콜 스택에 담는다.
  3. outer 내부에서 호출된 inner 함수도 마찬가지로 outer 컨텍스트 실행이 중단되며 inner 함수가 콜 스택의 가장 위에 담긴다.
  4. inner 함수 내부에서 a 변수에 3을 할당하고 나면 inner 함수의 실행이 종료되면서 inner 실행 컨텍스트가 콜 스택에서 제거된다.
  5. a 변수의 값을 출력하고 나면 outer 함수의 실행이 종료되어 outer 실행 컨텍스트가 콜스택에서 제거된다.
  6. 전역 공간에 더는 실행할 코드가 남아있지 않아 전역 컨텍스트도 제거된다.

수집 정보

  • VariableEnvironment
    • environmentRecord (snapshot)
    • outerEnvironmentReference (snapshot)
  • LexicalEnvironment
    • environmentRecord
    • outerEnvironmentReference
  • ThisBinding : this 식별자가 바라봐야 할 대상 객체

VariableEnvironment

현재 컨텍스트 내의 식별자들에 대한 정보와 외부 환경 정보가 담긴다. 여기까지는 LexicalEnvironment와 같지만 선언 시점의 스냅샷만을 유지한다는 점이 다르다. 실행 컨텍스트를 생성할 때 VariableEnvironment에 먼저 정보를 담고, 그대로 LexicalEnvironment에 복사 한 후 주로 LexicalEnvironment를 활용한다.

LexicalEnvironment

처음에는 VariableEnvironment와 같지만 변경사항이 실시간으로 반영된다.

1. environmentRecord

environmentRecord에는 현재 컨텍스트와 관련된 코드의 식별자 정보들이 저장된다. 컨텍스트 내부 전체를 처음부터 쭉 훑어나가며 순서대로 수집하게 된다.

전역 실행 컨텍스트는 변수 객체를 생성하는 대신 자바스크립트 구동 환경이 별도로 제공하는 객체, 즉 전역 객체를 활용한다. 전역 객체는 브라우저의 window, Node.js의 global 객체 등이 있다. 이들은 자바스크립트 내장 객체가 아닌 호스트 객체로 구분된다.

변수 정보를 수집하는 과정이 모두 끝나도 아직 실행 컨텍스트가 관여할 코드들은 실행되기 이전의 상태이다. 코드가 실행되기 전에도 이미 해당 환경에 속한 코드의 변수명을 모두 아는 상태가 된다. "자바스크립트 엔진은 식별자들을 최상단으로 끌어올려놓은 다음 실제 코드를 실행한다." 고 생각할 수 있는데 이것이 **호이스팅(hoisting)**이다.

호이스팅 규칙

environmentRecord에는 매개변수의 이름, 함수 선언, 변수명 등이 담긴다고 했다. 아래 예제를 통해 호이스팅에 대해 알아보자.

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

a(1);

호이스팅을 생각해서 아래와 같이 표현할 수 있다.

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, undefined, 2가 아닌 1, 1, 2가 나온다. 호이스팅은 식별자(변수명)만을 끌어올리고 어떤 값이 할당될 것인지는 관심이 없다. 변수 x가 다시 선언되어도 이미 선언된 변수 x가 있으므로 이를 무시하고 1을 할당해버린다.

function a() {
  console.log(b); // (1)
  var b = 'bbb'; // 수집 대상 1 (변수 선언)
  console.log(b); // (2)
  function b() {} // 수집 대상 2 (함수 선언)
  console.log(b); // (3)
}

a();

두번째 예제를 보면 undefined, 'bbb', b 함수가 나올 것 같지만, 실제로는 b함수, 'bbb', 'bbb'라는 전혀 쌩뚱맞은 값이 나온다. 호이스팅이 적용된다면 아래와 같이 작동할 것이다.

function a() {
  var b; // ------------------- 수집 대상 1(변수 선언) 의 선언 부분
  function b() {} // ---------- 수집 대상 2(함수 선언) 의 전체

  console.log(b); // ---------- (1)
  b = 'bbb'; // --------------- 수집 대상 1의 할당 부분
  console.log(b); // ---------- (2)
  console.log(b); // ---------- (3)
}
a();

식별자는 선언부만을 끌어올렸으나, 함수는 전체를 끌어올리게 된다. 때문에 자바스크립트에서는 함수를 선언한 위치와는 무관하게 해당 함수를 실행할 수 있다. 약간 혼란스러움..

함수 선언문과 함수 표현식

자바스크립트에서는 함수를 선언하는 세가지 방식이 있다.

1. 함수 선언문
function 정의부만 존재하고 별도의 할당 명령이 없는 것

function a() { /* ... */ } // 함수 선언문. 함수명 a가 곧 변수명
// var a = function a() { /* ... */ } 와 동일
a(); // 실행 가능

2. 함수 표현식
정의한 function을 별도로 변수에 할당하는 것. 일반적인 (익명) 함수 표현식과 기명 함수 표현식이 있다. 기명 함수 표현식은 거의 사용하지 않는다.

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

var c = function d() { /* ... */ } // (기명) 함수 표현식. 변수명은 c, 함수명은 d
c(); // 실행 가능
d(); // 실행 불가능.

기명 함수 표현식 주의할 점
외부에서는 함수명으로 함수를 호출할 수 없다. 함수명은 오직 함수 내부에서만 접근할 수 있다. 과거에는 디버깅 때문에 유리한 측면이 있었으나, 현재는 모든 브라우저들이 익명 함수 표현식의 변수명을 함수의 name 프로터피에 할당한다.

실질적인 차이를 알아보자.

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;
};

함수 선언문은 전체를 호이스팅하지만, 함수 표현식은 변수 선언문만 호이스팅하기 때문에 multiply(3, 4)는 에러가 발생한다. 보통 위에서 아래로 코드를 읽는 것이 익숙하기 때문에, 함수 선언문은 협업을 하는데 있어서 큰 문제를 발생시킬 수도 있다. 때문에 상대적으로 함수표현식이 안전하다. 원할한 협업을 위해서는 전역 공간에 함수를 선언하거나, 동명의 함수를 중복 선언하는 것을 지양해야 하지만, 모든 함수가 함수 표현식으로 정의되어 있다면 문제는 발생하지 않는다.

2. 스코프, 스코프 체인, outerEnvironmentReference

스코프란 식별자에 대한 유효범위이다. 대부분의 프로그래밍 언어는 블록 레벨 스코프를 따르지만, 자바스크립트는 함수 레벨 스코프를 따른다. 함수 레벨 스코프란 함수 코드 블록 내에서 선언된 변수는 함수 코드 블록 안에서만 유효함을 의미한다. 하지만 ES6 부터 도입된 let, const를 사용하면 블록 레벨 스코프를 사용할 수 있다.

스코프 체인(scope chain)

식별자의 유효범위를 안에서부터 바깥으로 차례로 검색해나가는 것을 의미한다. 이를 가능케 하는 것이 outerEnvironmentReference이다.

outerEnvironmentReference는 오직 자신이 선언된 시점LexicalEnvironment만 참조하고 있으므로 가장 가까운 요소부터 차례로 접근할 수 있다. 이러한 구조적 특성 덕분에 여러 스코프에서 동일한 식별자를 선언한 경우에는 무조건 스코프 체인 상에서 가장 먼저 발견된 식별자에만 접근 가능하게 된다.

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

(1)에서는 undefined가 찍히는데, a 식별자가 inner 스코프의 LexicalEnvironment에 존재하므로 스코프 체인 검색을 더 이상 진행하지 않고 즉시 inner LexicalEnvironment 상의 a를 반환하기 때문이다. 즉, inner 함수 내부에서 a 변수를 선언했기 때문에 전역 공간에서 선언한 동일한 이름의 a 변수에는 접근할 수 없는 것인데 이를 변수 은닉화라고 한다.

전역변수와 지역변수

전역변수: 전역 컨텍스트의 LexicalEnvironment에 담긴 변수
지역변수: 그 밖의 함수에 의해 생성된 실행 컨텍스트의 변수

코드 안정성을 위해서 가급적 전역변수 사용을 최소화하는 것이 좋다.

profile
(~˘▾˘)~♫❝ 쉽게만 살아가면 재미없어 빙고 .ᐟ ❞•*¨*•.¸¸♪

0개의 댓글