[Core JavaScript] 02. 실행 컨텍스트

Ethan·2022년 12월 14일
post-thumbnail

본 내용은 정재남 님의 코어 자바스크립트 책을 읽고 정리한 내용을 작성했습니다.

Chapter 02. 실행 컨텍스트

1. 실행 컨텍스트란?

  • 실행할 코드에 제공할 환경 정보들을 모은 객체
  • 자바스크립트의 핵심 개념 중 하나로 호이스팅, 외부 환경 참조, this 값의 설정 등의 동작을 이해하는데 연결 고리와 같은 역할을 한다.

1.1 스택과 큐

  • 스택(Stack) : 데이터의 입력과 출력에 대해 후입선출(LIFO) 구조로 이루어진 자료구조
  • 큐(Queue) : 데이터의 입력과 출력에 대해 선입선출(FIFO) 구조로 이루어진 자료구조

스택과 큐

1.2 실행 컨텍스트

  • 동일한 환경에 있는 코드들을 실행할 때, 필요한 환경 정보들을 모아 컨텍스트(객체)를 구성한다.

    • 동일한 환경(=실행 컨텍스트)을 구성하는 방법 :

      • 전역공간
      • eval function
      • function
      • block
  • 구성 요소
    컨텍스트

    • Variable Environment
      • 현재 컨텍스트 내 식별자들에 대한 정보(환경 레코드) + 외부 환경 정보
      • 선언 시점의 Lexical Environment의 스냅샷
      • 변경 사항이 반영되지 않는다.
    • Lexical Environment
      • 기본적인 구성은 Variable Environment 와 같지만 변경 사항이 실시간으로 반영된다.
    • ThisBinding : this 식별자가 바라봐야할 대상(객체)
  • 이를 콜 스택 Call Stack 이라는 공간에 쌓아올리면서, 실행할 때 가장 위에 있는 컨텍스트부터 실행하는 식으로 전체 코드의 환경 및 순서를 보장한다.

실행 컨텍스트와 콜 스택 동작 예시

// ------------------- (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

컨텍스트 동작2

  1. 처음 자바스크립트 코드를 실행하는 순간 (1)전역 컨텍스트가 콜 스택에 담긴다.

  2. 전역 컨텍스트 내부의 코드를 순차적(위에서 아래로) 실행하다가 (3)의 outer 함수를 호출한다.

    2-1. 자바스크립트 엔진은 outer의 환경 정보를 수집하고, 실행 컨텍스트를 생성한 후 콜 스택에 담는다.

    2-2. 콜 스택의 상단에는 outer 실행 컨텍스트가 놓인 상태가 됐으므로 전역 컨텍스트의 진행 흐름은 일시중단하고, 상단의 outer 함수 내부의 코드를 순차적으로 실행하게 된다.

  3. 이후 outer 함수 내부의 코드 중 (2)에서 inner 함수를 호출하게 된다.

    3-1. 마찬가지로 inner의 환경 정보를 이용해 실행 컨텍스트를 생성하고 콜 스택에 담는다.

    3-2. inner 실행 컨텍스트가 가장 상단의 컨텍스트가 되어 코드의 진행 흐름을 가져가게 된다.

  4. inner 함수의 범위까지 실행하고 나면, 함수 실행이 종료되면서 inner 실행 컨텍스트가 콜 스택에서 제거된다.

    4-1. 그 아래에 있던 outer 실행 컨텍스트가 코드의 진행 흐름을 다시 가져가면서 이전 중단 지점인 (2)에서 부터 이어서 실행한다.

  5. outer 함수의 범위까지 실행하면, 이 또한 outer 실행 컨텍스트가 콜 스택에서 제거된다.

    6-1. 전역 컨텍스트만 남은 상태가 되어, 이전 중단 지점 (3)에서 부터 이어서 실행한다.

  6. 전역의 코드까지 모두 실행하고 나면 전역 컨텍스트도 제거된다.

  7. 최종적으로 콜 스택이 빈 상태가 되고 프로그램이 종료된다.


2. Variable Environment

Variable Environment

  • Environment Record + Outer Environment Reference 의 조합
  • 실행 컨텍스트를 생성할 때, 처음 실행 했을 때의 정보를 유지하기 위해 원본(Snap Shot)을 유지한다.
  • 대신 컨텍스트가 실행하는 동안 변경되는 식별자 정보 등에 대해 활용할 것으로 Lexical Environment 에 원본의 정보를 그대로 복사해서 전달하게 된다. ( = 초기화 됐을 당시 이 둘의 데이터 값은 거의 동일하다 )

3. Lexical Environment

Lexical Environment

3-1. environmentRecord와 호이스팅

3-1-1. environment Record

  • 현재 컨텍스트와 관련된 코드의 식별자 정보들이 저장된다. (평가 단계)
    • 식별자
      • 함수에 지정된 매개변수 식별자
      • 선언한 함수가 있을 경우 해당 함수 자체
      • var로 선언된 변수의 식별자 등
  • 컨텍스트 내부 전체를 처음부터 끝까지 쭉 훑어나가며 순서대로 해당 환경의 정보를 수집한다.
  • 그렇게 코드를 실행하기 전부터 이미 해당 환경에 속한 코드의 변수명들을 모두 알고 있는 상태가 된다.

💡 전역 실행 컨텍스트의 environment Record 에 대해

전역 실행 컨텍스트의 경우 자바스크립트 구동 환경이 별도로 제공해주는 객체(전역 객체)를 활용해서 컨텍스트를 생성하게 된다. 그래서 전역 객체에는 브라우저의 window, 노드의 global 객체 등이 있다. 이들은 자바스크립트의 호스트 객체로 분류된다.

3-1-2. 호이스팅 Hoisting

코드 실행 단계에서 선언하기 전에 그 식별자 값에 접근할 수 있는 원리

  • “끌어 올리다” 라는 의미 그대로 자바스크립트 엔진이 마치 “식별자 정보들을 최상단으로 끌어올려 놓은 다음 실제 코드를 실행한다.” 라는 이해하기 쉬운 비유적 표현으로 대체한 개념이다.
  • 자바스크립트 엔진이 실제로 식별자들을 끌어올리는 행위를 하는 것은 아니고, 그저 평가 단계를 통해 식별자 정보를 한 번 스크리닝하고 그 정보들을 기억하고 있는 상태이기 때문에 코드 실행 단계에서 선언하기도 전에 식별자 값에 접근할 수 있는 것이다.

3-1-3. 호이스팅 규칙 - 변수 호이스팅과 함수 호이스팅

매개변수와 변수에 대한 호이스팅 - 원본 코드

아래와 같은 코드가 있다고 할 때, 해당 함수의 코드 평가 방식이 어떻게 일어나는지 알아보자.

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);
  1. 우선 호이스팅이 발생하기 전의 원본 상태에서 값을 예상 하자면 (1)의 값은 매개변수 1을 받아 그대로 출력할 것이라고 예상된다.
  2. (2)의 경우 바로 위에 수집 대상인 새로운 x를 바라보게 된다고 하면 할당한 값이 없으므로 undefined를 예상하게 된다.
  3. (3) 은 2를 할당하고 있기 때문에 2가 출력될 것이라고 예상된다.

매개변수와 변수에 대한 호이스팅(2) - 매개변수를 변수 선언/할당과 같다는 전제로 변환된 코드

  • Environment Record 는 현재 a 실행 컨텍스트의 대상 코드 안에 어떤 식별자들이 있는지를 체크하는 것이지, 각 식별자에 어떤 값이 할당되어 들어오는지는 관심이 없기 때문에 할당 과정은 그 자리에 둔다.
function a(x) { 
	var x = 1; // 수집 대상 1(매개변수 선언)
	console.log(x); // (1)

	var x; // 수집 대상 2(변수 선언)
	console.log(x); // (2)

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

매개변수와 변수에 대한 호이스팅(3) - 호이스팅을 마친 상태

01 function a(x) { 
02 	  var x; // 수집 대상 1의 변수 선언 부분
03 	  var x; // 수집 대상 2의 변수 선언 부분
04 	  var x; // 수집 대상 3의 변수 선언 부분
05
06	  x = 1; // var x; // 수집 대상 1의 할당 부분
07	  console.log(x); // (1)
08	  console.log(x); // (2)
09	  x = 2; // 수집 대상 3의 할당 부분
10	  console.log(x); // (3)
11 }
12 a(1);
  1. 2번째 줄 : 변수 x를 선언하고, 메모리에 저장할 공간을 미리 확보하고, 확보한 공간에 주소 값을 변수 x에 연결한다.
  2. 3~4번째 줄 : 변수 x를 다시 선언하지만 이미 선언된 변수 x가 있으므로 무시된다.
  3. 6번째 줄 : x에 1을 할당하므로 숫자 1을 데이터 영역의 메모리에 담고, x와 연결된 메모리의 공간에 숫자 1을 가리키는 주소 값을 전달한다.
  4. 7~8번째 줄 : 각 x를 출력한다.
    • (1)과 (2) 모두 1이 출력된다.
  5. 9번째 줄 : x에 2를 할당한다. 숫자 2를 데이터 영역의 메모리에 담고, 전과 같은 과정을 거쳐 최종적으로 변수 x에 숫자 2를 가리키게 된다.
  6. 10번째 줄 : x를 출력한다.
    • (3)에서는 2를 재할당 했기 때문에 2가 출력된다.

💡 arguments에 대해

  • 실행 컨텍스트를 생성하는 시점에 만들어지는 정보 중 하나로 지정한 매개변수의 개수에 관계 없이 호출 시 전달한 인자를 모두 저장하는 매개가 되어준다.
  • 하지만 이는 유사 배열 객체로 배열처럼 활용하기 위해서는 별도의 작업이 필요로 하고, 함수 내부에서 매개 변수의 값을 바꾸면 arguments 값도 따라 바뀌는데, 이렇게 되면 인자를 모두 저장한 데이터의 개념에서 벗어나기 때문에 ES6에서 사용하는 나머지 파라미터 Rest Parameter 를 이용해 arguments를 온전히 대체할 수 있는 방법이 생겼다.

3-1-4. 함수 선언문과 함수 표현식

선언문과 표현식 모두 함수를 새롭게 정의하는 방식으로 호이스팅이 적용되는 부분에서 차이가 있다.

함수 선언문

  • function 정의부만 있고, 별도로 할당하는 부분은 없는 함수 정의 표기법

함수 표현식

  • function 정의부와 별도의 변수에 할당하는 함수 정의 표기법
  • 익명과 기명으로 나뉘어 있지만 실질적으로 사용하는 것도 그렇고 일반적으로 익명 함수 표현식을 말한다.

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

01 // 함수 선언문 -> 함수명 a가 곧 변수명
02 function a() { ... }
03 a(); // a 함수 실행
04 
05 // (익명) 함수 표현식 -> 변수명 b가 곧 함수명
06 var b = function () { ... }
07 b(); // b 함수 실행
08 
09 // (기명) 함수 표현식 -> 변수명은 c, 함수명은 d
10 var c = function d() { ... }
11 c(); // c 함수 실행
12 d(); // 에러

12번째 줄 - 기명 함수 표현식의 경우 외부에서는 함수명으로 함수를 호출 할 수 없기 때문에 에러가 발생한다.

  • 기명 함수로 작성된 함수명은 해당 함수 내부에서만 접근 가능하다.
  • 최근에는 쓸 일이 크게 없지만 함수 내부에서 재귀함수를 호출하는 용도 등으로 사용할 수도 있다.

함수 선언문과 함수 표현식에 대한 호이스팅 - 원본 코드

  • 실행 컨텍스트의 Lexical Environment 에서 Environment Record 의 정보를 수집하는 과정에서 발생하는 호이스팅이 함수 선언문과 함수 표현식에 어떻게 적용되는지 알아보자.
console.log(sum(1, 2));
console.log(multiply(3, 4));

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

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

함수 선언문과 함수 표현식에 대한 호이스팅(2) - 호이스팅을 마친 상태

함수 선언문은 정의부 전체를 호이스팅하는 반면 함수 표현식은 변수 선언부만 호이스팅한다.

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

var multiply; // 변수는 선언부만 끌어올린다.

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

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

코드 평가 단계

  1. sum을 위한 메모리 공간을 확보하고 확보된 공간의 주소 값을 변수 sum에 연결한다.
  2. multiply를 위한 메모리 공간을 확보하고, 그 공간의 주소 값을 변수 multiply에 연결한다.

코드 실행 단계

  1. sum 함수를 또 다른 메모리 공간에 저장(참조 타입)하고, 그 주소 값을 앞서 선언한 변수 sum의 공간에 할당한다.
  2. sum 함수를 호출한다.
    • sum의 정의부가 실행되어 반환 값 3을 출력한다.
  3. multiply 함수를 호출한다.
    • 현재 값이 할당되어 있지 않기 때문에 호출할 함수가 없어 에러 메세지를 호출하고 프로그램을 종료한다.

함수 선언문의 위험성

  • 함수 선언문은 이렇게 호이스팅으로 인해 선언하기도 전에 함수를 호출할 수 있다는 것에 있어 어디서든 함수를 호출 하더라도 에러를 일으키지 프로그램을 동작시킬 수 있다는 장점도 있다.
  • 하지만 문맥을 읽어나가는 흐름에 있어서 사람이 이해하기에 부자연스러운 것이고 호이스팅으로 인해 식별자 명에 의한 충돌이 나게 되는 경우 가장 나중에 선언된 것이 이전의 것을 덮어 씌우기 때문에 예상하지 못한 코드의 동작 등이 발생할 수 있다는 문제점이 발생한다.
  • 원활한 협업을 위해서는 전역공간의 함수 선언과 동명의 함수로 중복 선언되는 일은 없어야 하며, 함수 표현식을 사용하는 것이 선언문 방식보다 더 코드의 흐름을 이해하기 쉽고 호이스팅으로 생기는 문제를 예방할 수 있다.

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

3-2-1. 스코프 Scope

스코프

  • 식별자에 대한 유효범위
  • 경계 A의 외부에서 선언한 변수는 A의 외부와 내부 모두 접근이 가능하지만 A의 내부에서 선언한 변수는 오직 A의 내부에서만 접근할 수 있는 개념이다.

3-2-2. 스코프 체인 Scope Chain

  • 내부에서 식별자를 찾을 수 없을 때, 해당 컨텍스트 안에서부터 바깥으로 차례로 탐색해가며 식별자를 찾아나가는 개념이다.
  • Outer Environment Reference를 통해 현재 호출된 함수가 선언될 당시의 Lexical Environment 를 참조한다.
    • A 함수 내부에 B 함수를 선언한 형태라면, 함수 B의 Outer Environment Reference는 함수 A의 Lexical Environment를 참조하는 형태로 계속해서 선언 시점의 환경을 거슬러 올라가면 최종적으로 전역 컨텍스트의 Lexical Environment를 참조하게된다. ( Outer Environment Reference는 연결 리스트와 같은 자료구조를 취하고 있다. )
    • 가장 가까운 요소부터 차례대로 접근할 수 있고 다른 순서로 접근하는 것은 불가능하다.

3-3-3. 변수 은닉화 Variable Shadowing

  • 여러 스코프에서 동일한 식별자를 선언한 경우, 스코프 체인 상에서 가장 먼저 발견된 식별자에만 접근 가능하다. 이렇게 동일한 이름의 다른 스코프의 식별자의 접근이 차단되는 것을 변수 은닉화 라고 한다.

스코프 체인 예시

  • 전역 → outer → inner 컨텍스트로 이동할 수록 점차 규모가 작아지지만 스코프 체인을 타고 이동해서 접근 가능한 변수의 수는 늘어난다.
01 var a = 1;
02 var outer = function () {
03	var inner = function () {
04		console.log(a);
05		var a = 3;
06 };
07	inner();
08	console.log(a);
09 };
10 outer();
11 console.log(a);
  1. 시작하게 되면 전역 컨텍스트가 활성화 된다.
    • 전역 컨텍스트 Environment Record : { a, outer } 식별자를 저장한다.
    • 전역 컨텍스트 Outer Environment Reference : 전역은 선언 시점이 없으므로 아무것도 담지 않는다.
    • 전역 컨텍스트 thisBinding : 전역 객체
  2. 1번째, 2~9번째 줄 : 전역 스코프의 식별자 a에 1을 할당하고, outer에 익명 함수 정의부를 할당한다.
  3. 10번째 줄 : outer 함수를 호출한다.
    • 전역 컨텍스트의 코드는 10번째 줄에서 임시 중단된다.
    • outer 함수 호출에 의해 2번째 줄로 다시 이동한다.
  4. 2번째 줄 : outer 실행 컨텍스트가 활성화된다.
    • outer 컨텍스트 Environment Record : { inner } 식별자를 저장한다.
    • outer 컨텍스트 Outer Environment Reference
      • outer 함수가 선언될 당시의 Lexical Environment가 담긴다.
      • outer 함수가 전역 공간에 선언 됐으므로 전역 컨텍스트의 Lexical Environment를 참조 복사 하며, [ GLOBAL, { a, outer } ] 와 같은 식으로 표기된다.
        • 첫 번째 인자는 실행 컨텍스트의 이름
        • 두 번째는 Environment Record 객체
      • outer 컨텍스트 thisBinding : 전역 객체
  5. 3번째 줄 : outer 스코프에 있는 변수 inner에 익명 함수 정의부를 할당한다.
  6. 7번째 줄 : inner 함수를 호출한다.
    • outer 실행 컨텍스트의 코드는 7 번째 줄에서 임시 중단된다.
    • inner 함수 호출에 의해 3번째 줄로 이동한다.
  7. 3번째 줄 : inner 실행 컨텍스트가 활성화된다.
    • inner 실행 컨텍스트 Environment Record : { a } 식별자를 저장한다.
    • inner 실행 컨텍스트 Outer Environment Reference
      • inner 함수가 선언될 당시의 Lexical Environment가 담긴다.
      • inner 함수가 outer 함수 내부에서 선언 됐으므로 outer 컨텍스트의 Lexical Environment 를 참조 복사 하며, [ outer, { inner } ]로 표기된다.
    • inner 컨텍스트 thisBinding : 전역 객체
  8. 4번째 줄 : 식별자 a에 접근한다.
    • 현재 활성화 상태인 inner 컨텍스트의 Environment Record 에서 a를 탐색한다.
    • inner 컨텍스트에 a 식별자를 저장해두었지만 할당된 값은 없다. ( undefined 출력 )
  9. 5번째 줄 : inner 스코프에 있는 변수 a에 3을 할당한다.
  10. 6번째 줄 : inner 함수 실행이 종료된다.
    • inner 실행 컨텍스트가 콜 스택에서 제거되고, 아래 스택에 있던 outer 실행 컨텍스트가 활성화된다.
    • 앞서 중단했던 7번째 줄의 다음(8번째 줄)으로 이동한다.
  11. 8번째 줄 : 식별자 a에 접근한다.
    • 현재 활성화된 상태인 outer 컨텍스트의 Environment Record 에서 a를 탐색한다.
    • 현재 outer 컨텍스트에서 찾을 수 없다.
      • outer 컨텍스트의 Outer Environment Reference 에 담긴 Environment Record 를 탐색한다.
      • 전역 Lexical Environment 에 a가 있으며, 그 값에 저장되어 있는 값 1을 가져온다. ( 1 출력 )
  12. 9번째 줄 : outer 함수 실행이 종료된다.
    • outer 실행 컨텍스트가 콜 스택에서 제거되고, 아래 스택에 있던 전역 실행 컨텍스트가 활성화된다.
    • 앞서 중단했던 10번째 줄의 다음(11번째 줄)으로 이동한다.
  13. 11번째 줄 : 식별자 a에 접근한다.
    • 현재 활성화된 상태인 전역 컨텍스트의 Environment Record 에서 a를 탐색한다.
    • 전역 컨텍스트에 저장된 a 식별자의 할당된 값 1을 가져온다. ( 1 출력 )
    • 모든 코드의 실행이 완료되고, 전역 컨텍스트가 콜 스택에서 제거되며 프로그램이 종료된다.

3-3-4. 전역변수와 지역변수

전역변수

  • 전역 스코프에서 선언한 변수를 말한다.
  • 코드의 전체적인 안전성을 고려한다면 가급적 전역변수 사용을 최소화하고자 노력하는 것이 좋다.
    • 전역 변수 사용을 최소화하는데 도움을 주는 도구
      • 즉시 실행함수(IIFE), 네임스페이스, 샌드박스 패턴 등이 있고, 그 밖에도 모듈 시스템 등이 있다.

지역변수

  • 함수 몸체 내부 또는 블록 내부에서 선언한 변수를 말한다.

4. this

  • 실행 컨텍스트의 thisBinding에는 this로 지정된 객체가 저장된다.
  • 실행 컨텍스트가 활성화 당시에 this가 지정되지 않은 경우 this에는 전역 객체가 저장된다.
  • 그 외에는 함수를 호출하는 방법에 따라 this에 저장되는 대상이 달라진다.

5. 정리

5-1. 실행 컨텍스트

  • 실행할 코드에 제공할 환경 정보들을 모아 놓은 객체

    • 전역 컨텍스트
    • eval 함수에 의한 컨텍스트
    • 함수 실행에 의한 컨텍스트
    • (ES6이상) 블록에 의한 컨텍스트
  • 실행 컨텍스트 객체는 활성화 되는 시점에 다음과 같은 정보를 수집한다.

    • Variable Environment
    • Lexical Environment
    • This Binding

5-2. Variable Environment와 Lexical Environment

  • 생성 초기에는 Variable Environment와 Lexical Environment 가 동일한 내용으로 구성 된다.

    • Lexical Environment 는 함수 실행 도중 변경되는 사항을 즉시 반영
    • Variable Environment 는 초기 상태를 유지
  • Variable Environment와 Lexical Environment 의 구성 요소

    • Environment Record ( 환경 레코드 )
      • 매개변수명
      • 변수의 식별자
      • 선언한 함수의 함수명
    • Outer Environment Reference ( 외부 환경 참조 )
      • 바로 이전 컨텍스트에 대한 정보를 가지고 있으며, Lexical Environment 정보를 참조한다.

5-3 Hoisting 호이스팅

  • Environment Record의 수집 과정을 비유적인 표현(=끌어올린다)으로 추상화한 개념
  • 활성화된 컨텍스트에서 해당 코드 집단의 최상단으로 선언부를 평가한다.
  • 이것으로 인해 변수 키워드(var, let, const)와 함수 선언문과 표현식에 차이가 발생한다.

5-4. 스코프와 스코프 체인

  • 스코프

    • 변수의 유효범위
  • 스코프 체인

    • 변수의 유효범위 내에 해당 변수를 찾는데, 찾지 못했을 경우 자신의 상위 스코프에 있는 컨텍스트를 탐색해서 값을 탐색하는 것으로 찾을 때까지 계속해서 해당 스코프의 상위로 거슬러 올라가 탐색한다.
    • 해당 식별자를 찾을 때까지 Outer Environment Reference에 담긴 이전 Lexical Environment 를 참조해서 탐색하며 이 과정은 전역 컨텍스트까지 반복한다.
      • 만약 값을 찾지 못하면 찾지 못했다는 is not defined 에러를 보여주며 프로그램을 종료한다.
  • 전역 변수와 지역변수

    • 전역 변수 : 전역 컨텍스트의 Lexical Environment에 담긴 변수
    • 지역 변수 : 그 밖의 함수, 문(반복, 조건) 등에 의해 생성된 실행 컨텍스트의 변수

5-5. this

  • 자기 자신을 가리키는 자기 참조 변수
  • 실행 컨텍스트 활성화하는 시점에 지정된 this가 저장된다.
  • 함수를 호출하는 방법에 따라 값이 달라지며, 지정되지 않는 경우는 전역 객체가 저장된다.

참조

코어 자바스크립트

0개의 댓글