자바스크립트 실행 컨텍스트에 대해

지니·2025년 1월 26일

지난 글에서 자바스크립트가 어떻게 동작하는지 알아보기 위해, 자바스크립트를 실행시키는 엔진과 실행 환경인 런타임 환경에 대한 내용을 정리해 봤다.

💡 실행 컨텍스트?
코드가 실행될 때 제공할 환경 정보를 갖고 있는 객체이다. 예를 들어 특정 코드 블록에서 사용 가능한 식별자 정보 등이 포함된다.

그 글에서 위와 같이 실행 컨텍스트를 언급했었는데, 이번 스터디에서 마침 실행 컨텍스트 주제로 발표를 하게 돼서 관련 내용을 좀 더 자세히 공부해 봤다. 🧐


실행 컨텍스트란?

실행 컨텍스트는 코드가 실행되기 위해 필요한 환경 정보를 담고 있는 개념으로, 자바스크립트 엔진이 이를 객체 형태로 관리한다. 실행 컨텍스트는 다음과 같은 환경 정보를 포함한다.

  • 변수(전역변수, 지역변수, 매개변수, 객체 프로퍼티)
  • 함수 선언
  • 변수의 유효 범위(Scope)
  • this 바인딩 정보

실행 컨텍스트의 생성

자바스크립트 코드에서 실행 컨텍스트가 생성되는 유형은 다음 세 가지로 구분된다.

  • 전역 코드: 처음 실행될 때 자동으로 전역 컨텍스트가 생성됨
  • 함수 코드: 함수 실행 시 해당 함수에 대한 실행 컨텍스트가 생성됨
  • eval() 함수: 단, 안전성과 성능 문제로 사용이 권장되지 않음

따라서 eval() 함수는 제외하고 봤을 때, 자바스크립트 코드의 실행 흐름을 실행 컨텍스트 중심으로 생각해 보면 다음과 같이 정리할 수 있다.

  1. 전역 컨텍스트 생성: 코드 실행 시작 시 전역 실행 컨텍스트가 생성됨.
    (브라우저 환경에서 window 객체에 접근하는 것도 이러한 전역 컨텍스트를 통해 가능하다.)
  2. 함수 실행 시 컨텍스트 생성: 특정 함수가 실행될 때 해당 함수에 대한 실행 컨텍스트가 생성됨.
  3. 콜 스택 관리:
    • 생성된 실행 컨텍스트는 콜 스택(Call Stack)에 쌓인다.
    • 코드 실행이 끝나면 실행 컨텍스트가 콜 스택에서 제거된다.

현재 실행 중인 코드에 대한 환경 정보를 실행 컨텍스트에 담고, 코드를 실행하는 동안 콜 스택에 넣어 두고, 코드 실행이 끝나면 사용 중이던 실행 컨텍스트를 콜 스택에서 제거한다고 이해할 수 있을 것 같다. 이를 통해 실행 컨텍스트는 코드의 실행 환경과 실행 순서를 보장해 준다.

실행 컨텍스트의 구성 요소

이제 코드가 필요로 하는 환경 정보가 실행 컨텍스트 내부에 어떻게 담겨 있는지 좀 더 자세히 알아보자. 실행 컨텍스트는 다음 세 가지 요소로 구분된다.

  1. VariableEnvironment
  2. LexicalEnvironment
    3. ThisBinding

‼️ ThisBinding에 대해
ThisBinding은 현재 코드에서 this 키워드가 어떤 객체를 가리키는지에 대한 정보를 담는다. ES6에서는 VariableEnvironment, LexicalEnvironment 안에 포함되었다는 글을 보기도 했는데 해당 내용은 공식적인 자료를 통해 확인하지는 못했다.
이번 글에서는 렉시컬 환경을 통한 변수 참조에 좀 더 초점을 맞췄기 때문에, 아래로는 VariableEnvironment와 LexicalEnvironment에 대해서만 살펴본다.

렉시컬 환경

렉시컬 환경은 특정 변수, 함수와 식별자를 연결해 준다. 렉시컬 환경은 다시 environment record와 outer environment reference로 구성된다.

environment record에는 식별자 바인딩이 기록되어 있고, outer environment reference는 중첩된 구조의 렉시컬 환경을 만들어 준다. 그림의 맨 위 전역 렉시컬 환경부터 하위로 내려가면서 확인하면, outer environment reference가 링크드 리스트처럼 상위 렉시컬 환경을 가리키고 있는 걸 알 수 있다.

이를 통해서 스코프 체인을 사용할 수 있게 된다. 특정 식별자를 참조하고자 할 때 우선 현재 실행 컨텍스트의 렉시컬 환경 안에 있는 environment record를 탐색하고, 만약 존재하지 않는다면 outer environment reference를 통해 상위의 렉시컬 환경에 있는 environment record를 탐색하는 것이다.

그림에서 최상위에 있는 전역 컨텍스트의 렉시컬 환경은 outer environment reference가 null임을 확인할 수 있는데, 실제로도 전역 컨텍스트까지 탐색한 뒤에도 식별자가 발견되지 않으면 참조 에러가 발생할 것이다.

VariableEnvironment와 LexicalEnvironment의 차이

"렉시컬 환경"이라는 용어가 중복해서 등장하다 보니 헷갈렸는데 🙄 나는 위에서 언급한 "렉시컬 환경"은 개념으로, 지금 언급하는 VariableEnvironmentLexicalEnvironment는 이 개념에 해당하는 실행 컨텍스트의 실제 구성 요소로 이해했다. (추상 클래스와 자식 클래스의 관계 같은 느낌...?)

그럼 왜 이 둘은 따로 존재할까? 같은 렉시컬 환경이면 하나만 있으면 되지 않을까? 뭐 하러 사람 헷갈리게 비슷한 걸 여러 개 만들었을까

VariableEnvironment의 environment record에는 var로 생성한 변수의 초기값을 저장하고, 이를 복사해 LexicalEnvironment의 environment record를 초기화한다. 그리고 let, const로 생성한 변수를 LexicalEnvironment의 environment record에 저장한 뒤, 코드 실행 과정에서 생기는 변경 사항을 반영하여 최신 상태로 업데이트 한다.

이 내용을 통해 예상할 수 있듯, 두 구성 요소는 varlet, const를 통한 변수 선언의 차이에 의해 분리된 것으로 보인다.

varlet, const의 차이 (1): 변수 생성 방식

변수 생성 방식은 (1) 선언(식별자를 등록한다), (2) 초기화(식별자를 위한 메모리를 할당한다), (3) 할당(실제로 값을 할당한다)의 세 단계로 나눠볼 수 있다.

이때 var로 생성한 변수는 선언과 초기화가 함께 이루어진다. 이로 인해서 변수를 실제 선언하는 코드 이전에 변수를 참조해도 undefined의 초기값이 반환되는 호이스팅이 발생하는 것이기도 한다.

반면 let, const로 생성한 변수는 선언만 먼저 이루어지고, '초기화되지 않은 상태'의 식별자로 유지된다. 따라서 실제 선언문 이전에 접근한다면 참조 에러가 발생하는데, 이 구간을 TDZ(Temporal Dead Zone)이라고 부른다.

varlet, const의 차이 (2): 블록 스코프 지원

var로 생성한 변수는 블록 스코프를 지원하지 않는다. 조건문이나 반복문의 코드 블록 단위로 변수를 생성할 수 없는 것이다. 따라서 var로 선언한 변수의 스코프는 전역, 혹은 함수 단위로 생성되는 특정 실행 컨텍스트와 대응시킬 수 있다.

반면 let, const로 생성한 변수는 블록 스코프를 지원한다. 아래 코드를 확인해 보자.

function foo(x) {
  var a = "a";
  if(x) {
    let b = "true";
  } else {
    let b = "false";
  }
  return a;
}
  • 함수 foo는 실행 컨텍스트를 가진다.
  • var로 생성된 a는 함수 foo안에서 유효하다.
  • let으로 생성된 b는 조건문의 코드블록 안에서 유효하다.

따라서 b의 경우, 함수 영역이 아닌 코드블록 단위에서 환경 정보로써 관리되어야 할 필요가 생긴다.

이렇게 변수 선언에 있어 서로 상이한 방식이 지원되고 있다 보니, 렉시컬 환경의 동작 방식에 있어서도 통일이 불가능했고 결과적으로 VariableEnvironment LexicalEnvironment가 구분된 것으로 보인다.

렉시컬 환경의 생성과 관리

알아본 내용을 바탕으로, 렉시컬 환경이 어떻게 구성되고 사용되는지를 정리하면 다음과 같다.

  1. 컨텍스트 생성 시점

    • VariableEnvironment에서 var 변수를 선언 및 초기화 (undefined 할당)
    • outerEnvironmentReference가 상위 컨텍스트의 LexicalEnvironment를 참조
    • VariableEnvironment의 초기 정보를 LexicalEnvironment로 복사
    • LexicalEnvironmentlet, const 변수 선언 (초기화되지 않음 → TDZ 적용)
  2. 코드 실행 중

    • LexicalEnvironment에서 변수 및 함수 정보를 최신 상태로 관리
    • 식별자를 참조할 때, 현재 실행 컨텍스트의 LexicalEnvironment를 먼저 확인하고, 없으면 outerEnvironmentReference를 따라 상위 컨텍스트를 탐색

이렇게 전역 컨텍스트에 대한 글을 정리해 봤다. 사실 엔진의 구체적인 내부 동작이나, 스펙의 변경 의도 같은 건 찾아보는 게 좀 어려웠는데 😅 그래도 이번 내용을 공부하면서 변수 생성과 참조가 실제로 어떻게 동작하는지 이해하는 데 도움이 된 것 같다! ✏️

참조

https://es5.github.io/#x10.2
https://m.blog.naver.com/dlaxodud2388/222655214381
https://iamsjy17.github.io/javascript/2019/06/10/js33_execution_context.html (이미지 출처)

0개의 댓글