실행 컨텍스트

정승현·2025년 3월 16일

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

실행 컨텍스트는 자바스크립트의 코드가 실행하는데 필요한 정보를 갖고 있다.
이 실행 컨텍스트는 코드가 실행될 때 생성되며 여기서 코드란 전역 코드, 함수코드, eval, 모듈 코드를 말한다.

일단 처음에는 전역코드가 가장 먼저 실행된다. 그리고 이때 전역 컨텍스트를 생성하고 전역코드를 위에서 아래로 순차적으로 실행되며, 실행 과정에서 함수 호출문을 만나면 새로운 실행 컨텍스트가 생성되고 해당 함수의 첫부분부터 순차적으로 실행된다.

var point1 = 10;

function sum() {
  var point2 = 20;
  function print() {
    console.log(point2);
    console.log(point1);
  }
  print();
  console.log('sum', (point1 + point2))
  
}
sum();
console.log('game over');

해당 코드의 실행 결과는 다음과 같다.

20
10
sumPoint 30
game over

위 코드가 실행될 때 실행 컨텍스트가 어떻게 스택에 쌓이고 제거되는지 아래와 같이 그림으로 표현해 보았다.

자바스크립트의 전반적인 코드 흐름을 알아봤으니, 이제 실행 컨텍스트가 무엇이며 어떻게 동작하는지 자세하게 알아보자.

  • 동일한 환경에 있는 코드들을 실행할 때 필요한 환경 정보들을 모아 객체로 만들어 컨텍스트를 구성한다. 그리고 객체들은 콜 스택에 저장된다. 그럼 콜 스택은 FILO(First In Last Out) 구조이기 때문에 객체들의 순서를 보장 받고 환경 또한 보장 할 수 있다.

  • 실행 컨텍스트가 실행될 때 자바스크립트 엔진이 수집하는 환경 정보들은 VariableEnvironment, LexicalEnvironment, ThisBinding이 있다.

    VariableEnvironment에 담기는 내용은 LexicalEnvironment와 같지만 최초 실행 시의 스냅샷을 유지한다는 점이 다릅니다.’ p40

    → 어차피 VariableEnvironment의 정보들을 LexicalEnvironment가 복사해서 이후에는 LexicalEnvironment를 주로 활용하면 처음부터 LexicalEnvironment에 정보들을 담으면 되지 않을까?

    정확한 차이는 뭘까?

    • VariableEnvironment 와 LexicalEnvironment 내부에 있는 environmentRecord와 outer - Environment - Reference가 다르다 !
      - variableEnvironment
      - 변경 사항은 반영되지 않는다.
      - 환경 기록 : var로 선언된 변수, 함수 선언식으로 선언된 함수가 저장된다.
      - 외부 환경 참조 : 상위 스코프의 LexicalEnvironment 참조

      - `LexicalEnvironment`
          - 변경 사항이 실시간으로 반영된다.
          - 환경 기록 : let, const로 선언된 변수가 저장된다.
          - 외부 환경 참조 : VariableEnvironment 참조

      → 이 둘은 var로 선언한 변수의 스코프와 let,const로 선언한 변수의 스코프의 차이, 변수가 생성되는 과정 (동작 원리) 에서 차이를 보인다.

      더 자세히 알아보자 !

    • var, let, const의 스코프 차이

      var로 선언된 변수는 함수 스코프를 따른다.

      function a() {
        if (true) {
            var apple = 'apple';
        }
        console.log(apple)
      }
      a() // apple

      → 이렇게 var로 선언된 변수는 함수 스코프를 따르므로 함수 스코프에 변수가 저장된다.

      var로 선언된 변수와 함수 선언식은 실행 컨텍스트가 실행될 때 VariableEnvironment에 저장된다.

      let, const로 선언된 변수는 블록 스코프를 따른다.

      function a() {
        if (true) {
            let apple = 'apple';
        }
        console.log(apple)
      }
      a() // Uncaught ReferenceError: apple is not defined

      → 하지만 let, const로 선언된 변수는 블록 스코프를 따르기 때문에 if 절을 빠져나오는 순간 if절의 블록 스코프는 종료된다. 함수 스코프에는 저장되지 않았다.

      그렇기 때문에 함수 내에 여러개의 블록 스코프가 있다면 여러개의 LexicalEnvironment 또한 필요하다.

    • var, let, const의 변수 생성 차이

      우선 변수는 1. 선언 단계 (변수를 선언한다) , 2. 초기화 단계 (변수의 값을 undefined로 초기화시킨다) , 3. 할당 단계 (변수의 값을 undefined에서 특정 값으로 할당한다) 를 거치게 된다.

      • var의 변수 생성 방식

        • var로 선언된 변수는 1, 2단계를 한꺼번에 마치게 된다.
      • let, const의 변수 생성 방식
        - let과 const로 선언된 변수는 1단계만 마치게 된다.

        → 저 둘의 가장 큰 차이인 TDZ (1단계와 2단계 사이에 발생한다) 의 영향 여부에 의해 동작 원리 자체가 바뀌게 된다.

      • TDZ

        • TDZ는 변수의 선언 단계와 초기화 단계 사이에 존재한다.
        • let과 const는 변수를 선언할 때 1단계만을 거치므로 let과 const로 선언된 변수는 TDZ에 머물게 된다. 초기화 단계를 거치지 않고 let, const를 참조하려하면 ReferenceError: apple is not defined 이라는 에러가 발생된다.
        • TDZ도 블록 스코프를 따르기 때문에 블록 스코프를 벗어나면 TDZ 또한 사라진다.

스코프 체인

현재 호출될 당시의 LexicalEnvironment를 참조한다.

책에서는 ‘호출될’에 초점을 맞추고 있다.

1 var apple = 'apple1'
2 var fruits = function () {
	3 console.log(apple) // apple1
5 }
6 fruits()
7 console.log(apple) // apple1

→ 위 함수를 보면 우선 실행 컨텍스트가 실행되면 먼저 전역 객체에 변수 apple과 함수 fruits가 저장되어 콜스택에 저장된다. 그리고 6번째 줄로 갔을 때 fruits 함수를 호출하고 있으므로 JS엔진은 2번째 줄로 이동하고 fruits 함수를 탐색한다. 3번째 줄에서 apple을 출력하고 있지만 fruits 함수 내부에서는 apple을 정의하고 있지 않다. 그래서 null, undefined를 생각했지만 fruits 함수의 outerEnvironmentReference는 그 전의 LexicalEnvironment를 참조하고 있으므로 찾아보면 fruits 함수 전에 있는 LexicalEnvironment는 전역 객체이므로 전역 객체 내에서 apple을 찾는다. 만약 있으면 apple에 할당된 값을 반환하고 없으면 undefined를 반환한다. 여기서는 있으므로 apple1을 반환한다. 그리고 fruites 함수는 종료되고 7번째 줄로 이동한다.

→ 스코프 체인은 더 넓은 환경에 있는 변수나 함수는 더 좁은 환경에 있는 변수나 함수에 접근하지 못하지만 더 좁은 환경에 있는 변수나 함수는 더 넓은 환경에 있는 변수나 함수를 참조하고 있기에 접근할 수 있다. 그 범위를 스코프 체인이라 한다.


함수 선언문의 위험

  • 전역 실행 컨텍스트가 활성화될 때 전역공간에 선언된 함수들이 모두 끌어올려진다. (호이스팅) 그럼 함수 선언문일 경우에는 함수 자체가 호이스팅 되므로 동일한 함수명을 가진 함수가 여러개 있다면 가장 나중에 선언된 함수에 전에 있던 함수들이 모두 덮어씌여진다. 이렇게 되면 의도된 반환값이 아닌 다른 값이 반환될 수 있기에 함수 선언문보다는 함수 표현식을 권장한다.

  • 암묵적 형변환

    심지어 sum 함수의 결과를 활용하는 다른 함수에서도 숫자 대신 문자열을 넘겨받았음에도 암묵적 형변환에 따라 오류 없이 통과됩니다. p52

    → 왜 javascript에서 암묵적 형변환이 이루어질까?

    • JS엔진은 산술 연산자, 느슨한 비교 연산자, 이중 부정을 사용할 때 자동으로 암묵적 형변환이 이루어 진다.
      • 우선 산술 연산자를 사용했을 때 암묵적 형변환은 ‘+’와 ‘-*/’의 경우로 나뉜다.
        • 서로 다른 자료형을 ‘+’로 연결하게되면 JS엔진은 Boolean < Number< String의 순서로 우선 순위를 부여한다.
          typeof (1 + 'string') = // String
          typeof (1 + true) =  // Number
          typeof ('string' + true) =  // String
        • 서로 다른 자료형을 ‘-*/’로 연결하게되면 모두 Number로 부여된다.
          typeof (1 - 'string') = // NaN
          typeof (1 * true) =  // 1
          typeof ('string' / true) =  // NaN
      • 느슨한 비교 연산자(==)을 사용했을 때
        • 피연산자에 String과 Boolean 타입이 있을 경우 String, Boolean은 Number 타입으로 바꿔진다.
          1 == '1' // true
          1 == true // true
          '1' == true // true
        • 피연산자에 Null과 undefined가 하나라도 있을 경우 false를 반환한다.
          1 == null //false
          '1' == undefined // false
          true == null // false
          null == undefined // true
      • 이중 부정 연산자(!!)를 사용했을 때 Js엔진은 해당 자료형을 강제적으로 Boolean 타입으로 바꾼다.
        • 값이 실제로 있는지 검증하기 위해
profile
성장하는 프론트엔드 개발자입니다 :)

0개의 댓글