실행 컨텍스트는 JavaScript 엔진이 코드를 실행할 때 만드는 추상화된 환경입니다. 함수를 호출하거나 스크립트를 시작할 때마다 새로운 실행 컨텍스트가 생성되며, 이 안에서 변수가 저장되고 스코프가 결정되며 this 값이 설정됩니다. 실행 컨텍스트를 이해하지 못하면 호이스팅, 클로저, this 바인딩 같은 JavaScript의 독특한 동작 방식을 정확히 설명할 수 없습니다. 이 개념은 JavaScript 내부 동작 원리를 이해하는 핵심 토대이며, 디버깅과 코드 예측 가능성을 크게 향상시킵니다.
핵심 특징
- 추상화된 실행 환경: 코드가 실행되는 논리적 공간으로, 변수, 함수, 스코프, this 등 모든 실행 정보를 관리합니다
- 생성 시점의 명확성: 전역 코드 실행, 함수 호출, eval 실행 시 각각 새로운 실행 컨텍스트가 생성됩니다
- 스택 구조 관리: 실행 컨텍스트는 콜스택에 쌓이며, 가장 위에 있는 컨텍스트가 현재 실행 중인 코드를 나타냅니다
- 내부 구성 요소: Variable Environment, Lexical Environment, ThisBinding 세 가지 핵심 컴포넌트로 구성됩니다
- 라이프사이클: 생성 단계와 실행 단계로 나뉘며, 각 단계에서 변수와 함수에 대한 처리 방식이 다릅니다
실무에서의 영향
실행 컨텍스트를 이해하면 JavaScript에서 자주 발생하는 예상치 못한 동작을 정확히 예측하고 설명할 수 있습니다. 예를 들어, 왜 함수 선언은 호출 전에 사용할 수 있지만 함수 표현식은 그렇지 않은지, 왜 화살표 함수의 this는 일반 함수와 다르게 동작하는지, 왜 클로저가 외부 변수를 기억할 수 있는지 등을 명확히 이해할 수 있습니다. 특히 비동기 코드나 이벤트 핸들러를 다룰 때 this 바인딩 문제로 인한 버그를 사전에 방지할 수 있으며, 복잡한 스코프 체인 문제를 디버깅할 때 근본 원인을 빠르게 파악할 수 있습니다. 실행 컨텍스트는 단순히 이론적 개념이 아니라, 실무에서 마주치는 대부분의 JavaScript 동작 패턴을 설명하는 핵심 메커니즘입니다. 이를 정확히 이해하면 코드 품질과 유지보수성이 크게 향상되며, 팀원들에게 복잡한 JavaScript 동작을 명확히 설명할 수 있는 능력을 갖추게 됩니다.
핵심 개념
실행 컨텍스트의 정의와 역할
입문
실행 컨텍스트는 JavaScript 코드가 실행될 때 필요한 모든 정보를 담고 있는 ‘작업 공간’이에요. 이해하기 쉽게 알아볼까요!
📦 실행 컨텍스트가 뭔가요? 여러분이 책상에서 숙제를 한다고 생각해보세요. 책상 위에는 공책, 펜, 지우개, 교과서가 놓여 있죠. 이 책상이 바로 ‘작업 공간’이에요. JavaScript도 마찬가지예요! 코드를 실행할 때 필요한 변수, 함수, 설정값들을 모아둔 ‘보이지 않는 책상’이 있는데, 이게 바로 실행 컨텍스트예요.
🎯 왜 필요한가요? 여러 과목 숙제를 동시에 한다고 상상해보세요. 수학 숙제를 할 때는 수학 교과서와 공책이 필요하고, 영어 숙제를 할 때는 영어 교과서가 필요해요. 만약 책상을 정리하지 않고 모든 책을 섞어놓으면 헷갈리겠죠? JavaScript도 여러 함수를 실행할 때 각각의 ‘작업 공간’을 분리해서 관리해야 해요. 그래야 변수가 섞이지 않고 정확하게 동작하거든요!
🏠 어떤 정보를 담고 있나요? 실행 컨텍스트는 세 가지 중요한 정보를 담고 있어요. 첫째, 변수와 함수들(책상 위 물건들), 둘째, 어디서 변수를 찾을지 알려주는 규칙(어느 서랍에 뭐가 있는지), 셋째, ‘this’라는 특별한 값(현재 누가 이 책상을 사용하는지)이에요. 이 세 가지가 있어야 코드가 제대로 실행될 수 있어요!
⏰ 언제 만들어지나요? 프로그램이 시작되면 자동으로 ‘전역 실행 컨텍스트’라는 큰 책상이 하나 만들어져요. 그리고 함수를 호출할 때마다 그 함수만을 위한 새로운 작은 책상이 추가로 만들어져요. 함수 실행이 끝나면 그 책상은 치워지고, 다시 이전 책상으로 돌아가요.
중급
실행 컨텍스트(Execution Context)는 JavaScript 코드가 실행되는 추상화된 환경으로, 코드 실행에 필요한 모든 정보를 포함하는 논리적 구조입니다.
실행 컨텍스트의 핵심 역할 JavaScript 엔진은 코드를 실행하기 전에 실행 컨텍스트를 생성하여 다음 정보를 관리합니다:
- 변수와 함수 선언
- 스코프 체인(변수 검색 경로)
- this 바인딩
// 전역 코드 실행 시 전역 실행 컨텍스트 생성
var globalVar = '전역 변수';
function greet() {
console.log('Hello');
}
// 전역 컨텍스트에서 globalVar와 greet 함수 관리
함수 실행 컨텍스트 함수가 호출될 때마다 새로운 함수 실행 컨텍스트가 생성됩니다. 각 함수는 독립적인 실행 환경을 가지므로 변수 충돌이 방지됩니다.
function outer() {
var outerVar = 'outer';
function inner() {
var innerVar = 'inner';
console.log(outerVar); // outer - 외부 변수 접근 가능
}
inner(); // inner 함수 실행 컨텍스트 생성
}
outer(); // outer 함수 실행 컨텍스트 생성
실행 컨텍스트의 생성 시점
- 전역 코드 실행: 프로그램 시작 시 자동 생성
- 함수 호출: 함수 호출마다 새로운 컨텍스트 생성
- eval() 실행: eval 코드 실행 시 생성 (권장하지 않음)
심화
실행 컨텍스트는 ECMAScript 명세의 실행 의미론(Execution Semantics)을 구현하는 추상 메커니즘으로, 코드 실행의 격리성(Isolation)과 스코프 해결(Scope Resolution)을 보장합니다.
ECMAScript 명세 기반 실행 컨텍스트 정의 ECMAScript 2023, Section 9.4 (Execution Contexts)에 따르면, 실행 컨텍스트는 세 가지 핵심 컴포넌트를 포함하는 명세 타입(Specification Type)입니다:
- LexicalEnvironment: 식별자 해결(Identifier Resolution)을 위한 어휘적 환경. let, const, 함수 선언의 바인딩을 관리합니다.
- VariableEnvironment: var 선언과 함수 선언의 초기 바인딩을 저장. 생성 시점에 LexicalEnvironment와 동일한 값으로 초기화되지만, with 문이나 catch 절에서 LexicalEnvironment가 임시로 변경될 때 원본 환경을 보존합니다.
- ThisBinding: this 값의 바인딩. 함수 호출 방식(메서드 호출, 생성자 호출, 일반 호출)에 따라 결정됩니다.
실행 컨텍스트 스택과 실행 흐름 제어 JavaScript 엔진은 실행 컨텍스트 스택(Execution Context Stack, Call Stack)을 사용하여 실행 흐름을 관리합니다. ECMAScript 명세 Section 9.4.1 (GetActiveScriptOrModule)에서 정의된 바와 같이, 스택의 최상단 컨텍스트가 현재 실행 중인 코드(Running Execution Context)를 나타냅니다.
스택 동작 메커니즘:
- PUSH: 함수 호출 시 새로운 컨텍스트를 스택에 추가
- POP: 함수 반환 시 현재 컨텍스트를 스택에서 제거
- 재귀 호출 시 스택 깊이 제한(브라우저마다 다름, Chrome: ~10,000)
V8 엔진의 실행 컨텍스트 구현 V8 엔진에서는 실행 컨텍스트를 Context 객체로 구현하며, 다음과 같은 최적화를 적용합니다:
Context Chaining: 중첩 함수의 경우 외부 컨텍스트에 대한 참조를 체인으로 연결하여 스코프 체인을 구현합니다. 각 Context 객체는 이전 컨텍스트를 가리키는 포인터를 유지합니다.
Inline Caching: 동일한 컨텍스트에서 반복적으로 접근되는 변수의 위치를 캐싱하여 탐색 비용을 줄입니다. 첫 번째 접근 시 O(n) 탐색, 이후 O(1) 접근으로 최적화됩니다.
Context Snapshot: TurboFan 컴파일러는 자주 실행되는 함수의 컨텍스트 상태를 스냅샷으로 저장하여 재생성 비용을 제거합니다.
전역 실행 컨텍스트와 함수 실행 컨텍스트
입문
JavaScript에는 두 가지 종류의 ‘작업 공간’이 있어요. 큰 공용 책상과 개인 책상이라고 생각하면 쉬워요!
🌍 전역 실행 컨텍스트란? 학교 도서관에 있는 큰 공용 책상을 생각해보세요. 이 책상은 도서관이 문을 열 때 항상 있고, 누구나 사용할 수 있어요. JavaScript의 전역 실행 컨텍스트도 마찬가지예요! 프로그램이 시작되면 자동으로 만들어지는 ‘기본 작업 공간’이에요. 여기에 선언한 변수나 함수는 프로그램 어디서든 사용할 수 있어요.
👤 함수 실행 컨텍스트란? 이번엔 여러분만의 개인 책상을 떠올려보세요. 이 책상은 여러분이 필요할 때만 꺼내서 사용하고, 작업이 끝나면 치워두죠. 함수 실행 컨텍스트가 바로 이거예요! 함수를 호출할 때마다 그 함수만을 위한 새로운 ‘개인 작업 공간’이 생겨요. 함수 안에서 만든 변수는 그 함수에서만 볼 수 있고, 함수 실행이 끝나면 사라져요.
🔄 둘의 차이점은? 공용 책상은 도서관에 딱 하나만 있지만, 개인 책상은 필요할 때마다 여러 개를 꺼낼 수 있어요. 마찬가지로 전역 실행 컨텍스트는 프로그램당 하나만 있지만, 함수 실행 컨텍스트는 함수를 호출할 때마다 새로 만들어져요. 함수를 10번 호출하면 개인 작업 공간이 10개나 생기는 거예요!
📚 왜 구분하나요? 만약 모든 학생이 공용 책상 하나만 사용한다면 어떻게 될까요? 물건이 뒤섞여서 엉망이 될 거예요! 그래서 각자 개인 책상을 주는 거죠. JavaScript도 마찬가지예요. 함수마다 독립적인 공간을 주면 변수가 섞이지 않아서 안전하게 코드를 실행할 수 있어요.
중급
JavaScript는 두 가지 주요 실행 컨텍스트를 구분하여 코드 실행 환경을 관리합니다.
전역 실행 컨텍스트(Global Execution Context) 프로그램 시작 시 자동으로 생성되는 최상위 실행 컨텍스트입니다:
- 프로그램당 하나만 존재
- 전역 객체(window 또는 global)와 연결
- 전역 변수와 함수를 관리
- 프로그램 종료 시까지 유지
// 전역 실행 컨텍스트에서 실행
var globalVar = 'I am global';
function globalFunction() {
console.log('Global function');
}
// globalVar와 globalFunction은 전역 컨텍스트에 저장됨
console.log(window.globalVar); // 'I am global' (브라우저 환경)
함수 실행 컨텍스트(Function Execution Context) 함수 호출마다 생성되는 독립적인 실행 환경입니다:
- 함수 호출마다 새로운 컨텍스트 생성
- 함수의 매개변수와 지역 변수 관리
- 외부 스코프에 대한 참조 유지
- 함수 실행 완료 시 소멸
function first() {
var firstVar = 'first';
console.log('첫 번째 함수');
}
function second() {
var secondVar = 'second';
console.log('두 번째 함수');
}
first(); // first 함수 실행 컨텍스트 생성 → 실행 → 소멸
second(); // second 함수 실행 컨텍스트 생성 → 실행 → 소멸
컨텍스트 간의 관계 함수 실행 컨텍스트는 항상 전역 실행 컨텍스트 위에 쌓이며, 중첩 함수의 경우 여러 함수 컨텍스트가 계층적으로 쌓입니다.
// 전역 컨텍스트
var global = 'global';
function outer() {
// outer 함수 컨텍스트
var outerVar = 'outer';
function inner() {
// inner 함수 컨텍스트
var innerVar = 'inner';
console.log(global, outerVar, innerVar);
// 모든 상위 컨텍스트의 변수 접근 가능
}
inner(); // inner 컨텍스트 생성
} // outer와 inner 컨텍스트 모두 소멸
outer(); // outer 컨텍스트 생성
심화
전역 실행 컨텍스트와 함수 실행 컨텍스트는 생성 메커니즘과 생명주기(Lifecycle) 측면에서 근본적인 차이를 가지며, 이는 ECMAScript 명세의 초기화 과정에서 명확히 구분됩니다.
전역 실행 컨텍스트의 초기화 메커니즘 ECMAScript 2023, Section 9.6 (InitializeHostDefinedRealm)에 따르면, 전역 실행 컨텍스트는 Realm 초기화 과정에서 생성되며 다음 단계를 거칩니다:
- Global Environment Record 생성: Object Environment Record(전역 객체 바인딩)와 Declarative Environment Record(let/const 바인딩)를 결합한 복합 환경 레코드 생성
- Global Object 생성: 브라우저의 window, Node.js의 global 등 호스트 환경이 제공하는 전역 객체 생성
- 내장 객체 바인딩: Object, Array, Function 등 표준 내장 객체를 전역 환경에 바인딩
- 전역 코드 평가: 스크립트 최상위 수준의 선언문을 평가하여 전역 환경에 바인딩
전역 실행 컨텍스트의 특수성은 Object Environment Record와 Declarative Environment Record를 동시에 사용한다는 점입니다. var 선언과 함수 선언은 Object Environment Record에 바인딩되어 전역 객체의 프로퍼티로 접근 가능하지만, let/const 선언은 Declarative Environment Record에만 바인딩되어 전역 객체의 프로퍼티가 되지 않습니다.
함수 실행 컨텍스트의 생성 과정 ECMAScript 명세 Section 10.2 (Function Declaration Instantiation)에서 정의된 함수 컨텍스트 초기화는 다음과 같이 진행됩니다:
- Function Environment Record 생성: 함수의 지역 변수와 매개변수를 관리할 환경 레코드 생성
- 외부 환경 참조 설정: [[Environment]] 내부 슬롯을 통해 외부 렉시컬 환경 참조 (클로저의 근간)
- this 바인딩 결정: 함수 호출 방식에 따라 this 값 결정 (일반 호출: undefined/global, 메서드 호출: 호출 객체)
- 매개변수 바인딩: 형식 매개변수를 실제 인수 값으로 초기화
- arguments 객체 생성: 일반 함수의 경우 유사 배열 객체 생성 (화살표 함수는 제외)
생명주기 차이와 메모리 관리 전역 실행 컨텍스트는 프로그램 종료 시까지 유지되지만, 함수 실행 컨텍스트는 함수 반환 시 즉시 콜스택에서 제거됩니다. 그러나 클로저가 존재하는 경우 함수 컨텍스트의 Environment Record는 가비지 컬렉션 대상에서 제외되어 메모리에 유지됩니다.
V8 엔진의 최적화:
- Context Allocation: 짧은 생명주기의 함수 컨텍스트는 스택 메모리에 할당하여 할당/해제 비용 최소화
- Escape Analysis: 클로저 캡처 여부를 분석하여 힙 할당 필요성 판단. 클로저가 없으면 스택 할당으로 최적화
- Context Specialization: 동일한 함수를 반복 호출하는 경우 컨텍스트 생성 코드를 인라인화하여 오버헤드 제거 (벤치마크: 함수 호출당 ~50ns 절감)
콜스택과 실행 컨텍스트 관리
입문
콜스택은 실행 컨텍스트(작업 공간)들을 차곡차곡 쌓아두는 ‘접시 더미’예요. 재미있게 알아볼까요!
🍽️ 콜스택이 뭔가요? 점심시간에 식판을 쌓아둔 모습을 떠올려보세요. 식판을 쌓을 때는 맨 위에 올려놓고, 가져갈 때도 맨 위에서부터 가져가죠. 이걸 ‘후입선출(나중에 들어간 게 먼저 나옴)‘이라고 해요. JavaScript의 콜스택도 똑같아요! 함수를 호출하면 그 함수의 ‘작업 공간’을 맨 위에 올려놓고, 함수가 끝나면 맨 위에 있는 작업 공간을 치워요.
📚 어떻게 쌓이나요? 프로그램이 시작되면 큰 접시(전역 실행 컨텍스트)가 바닥에 놓여요. 그 다음 함수를 호출하면 그 위에 새 접시(함수 실행 컨텍스트)를 올려요. 함수 안에서 또 다른 함수를 호출하면 그 위에 또 접시를 올려요. 마치 접시 쌓기 게임처럼 점점 높아지는 거예요!
⬇️ 어떻게 내려오나요? 함수가 일을 마치면 맨 위에 있는 접시를 치워요. 그러면 그 아래에 있던 접시가 맨 위가 되죠. 그 함수도 끝나면 또 치우고, 이렇게 계속하다 보면 결국 맨 바닥의 큰 접시만 남게 돼요. 이게 바로 함수가 끝나고 다시 이전 작업으로 돌아가는 과정이에요!
🚨 너무 높이 쌓으면? 접시를 너무 높이 쌓으면 무너지겠죠? JavaScript도 마찬가지예요! 함수가 자기 자신을 계속 호출하면 콜스택이 너무 높아져서 “스택 오버플로우”라는 오류가 나요. 마치 접시 더미가 무너진 것처럼 프로그램이 멈춰버리는 거예요.
🔍 왜 중요한가요? 콜스택 덕분에 JavaScript는 여러 함수를 호출해도 어느 함수가 먼저 실행 중이었는지, 어디로 돌아가야 하는지 정확히 기억할 수 있어요. 마치 책갈피를 여러 개 끼워두고 나중에 정확한 페이지로 돌아가는 것처럼요!
중급
콜스택(Call Stack)은 실행 컨텍스트를 LIFO(Last In, First Out) 방식으로 관리하는 자료구조입니다. JavaScript 엔진은 코드 실행 순서를 제어하기 위해 콜스택을 사용합니다.
콜스택의 동작 원리
- PUSH: 함수 호출 시 새로운 실행 컨텍스트를 스택에 추가
- POP: 함수 반환 시 현재 실행 컨텍스트를 스택에서 제거
- 실행: 스택 최상단의 컨텍스트가 현재 실행 중인 코드
function first() {
console.log('first 함수 시작');
second();
console.log('first 함수 종료');
}
function second() {
console.log('second 함수 시작');
third();
console.log('second 함수 종료');
}
function third() {
console.log('third 함수 실행');
}
first();
// 콜스택 변화:
// 1. [전역 컨텍스트]
// 2. [전역 컨텍스트, first]
// 3. [전역 컨텍스트, first, second]
// 4. [전역 컨텍스트, first, second, third]
// 5. [전역 컨텍스트, first, second] (third 종료)
// 6. [전역 컨텍스트, first] (second 종료)
// 7. [전역 컨텍스트] (first 종료)
스택 오버플로우(Stack Overflow) 콜스택의 크기에는 제한이 있으며, 재귀 호출이 무한히 반복되면 스택 오버플로우 오류가 발생합니다.
function recursiveFunction() {
recursiveFunction(); // 무한 재귀
}
// recursiveFunction(); // RangeError: Maximum call stack size exceeded
올바른 재귀 사용 재귀 함수는 반드시 종료 조건(Base Case)을 가져야 합니다.
function countdown(n) {
if (n <= 0) {
console.log('완료!');
return; // 종료 조건
}
console.log(n);
countdown(n - 1);
}
countdown(5);
// 콜스택 최대 깊이: 5 (제한된 재귀)
비동기 코드와 콜스택 비동기 함수(setTimeout, Promise)는 콜스택을 차단하지 않고 이벤트 루프를 통해 실행됩니다.
console.log('1');
setTimeout(() => {
console.log('2'); // 콜스택이 비워진 후 실행
}, 0);
console.log('3');
// 출력 순서: 1, 3, 2
// setTimeout 콜백은 콜스택이 비워질 때까지 대기
심화
콜스택은 ECMAScript 명세에서 정의된 실행 컨텍스트 스택(Execution Context Stack)의 실제 구현체로, 동기 코드 실행의 결정론적 순서(Deterministic Order)를 보장하는 핵심 메커니즘입니다.
ECMAScript 명세의 실행 컨텍스트 스택 ECMAScript 2023, Section 9.4 (Execution Contexts)에서 정의된 실행 컨텍스트 스택은 추상적 자료구조이며, 다음 연산을 지원합니다:
- PushExecutionContext(EC): 새로운 실행 컨텍스트를 스택에 추가하고 현재 실행 컨텍스트(Running Execution Context)로 설정
- PopExecutionContext(): 현재 실행 컨텍스트를 스택에서 제거하고, 그 아래 컨텍스트를 활성화
- SuspendExecutionContext(EC): 제너레이터/async 함수에서 현재 컨텍스트를 일시 중단 (스택에서 제거하지 않고 비활성화)
- ResumeExecutionContext(EC): 중단된 컨텍스트를 다시 활성화
일반 함수 호출은 PushExecutionContext와 PopExecutionContext만 사용하지만, 제너레이터와 async 함수는 Suspend/Resume을 통해 실행을 중단하고 재개할 수 있습니다.
V8 엔진의 콜스택 구현 V8 엔진은 콜스택을 실제 메모리 스택으로 구현하며, 다음과 같은 최적화를 적용합니다:
Stack Frame Layout: 각 함수 호출은 스택 프레임(Stack Frame)을 할당받으며, 다음 정보를 포함합니다:
- Return Address: 함수 종료 후 돌아갈 코드 위치
- Frame Pointer: 이전 스택 프레임의 시작 주소
- Local Variables: 함수의 지역 변수 메모리
- Function Context: 실행 컨텍스트 객체에 대한 참조
Inline Caching을 통한 스택 프레임 최적화: TurboFan 컴파일러는 자주 호출되는 함수의 스택 프레임 크기를 미리 계산하여 할당 오버헤드를 제거합니다.
스택 크기 제한과 최적화 브라우저와 Node.js는 콜스택 크기를 제한하여 무한 재귀로부터 시스템을 보호합니다:
- Chrome/V8: 약 10,000-15,000 프레임 (시스템 메모리에 따라 변동)
- Firefox/SpiderMonkey: 약 50,000 프레임
- Safari/JavaScriptCore: 약 65,536 프레임
재귀 최적화 기법:
- Tail Call Optimization (TCO): ECMAScript 2015 명세에 포함되었으나 대부분의 엔진이 미구현. Safari의 JavaScriptCore만 부분적으로 지원.
- Trampoline Pattern: 재귀를 반복문으로 변환하여 스택 사용 회피
- Continuation-Passing Style (CPS): 콜백을 통해 재귀 깊이 제한
콜스택과 이벤트 루프의 상호작용 ECMAScript 명세는 콜스택만 정의하며, 이벤트 루프는 HTML Living Standard와 Node.js 명세에서 별도로 정의됩니다. 이벤트 루프는 콜스택이 비워졌을 때(Empty Call Stack) 태스크 큐에서 다음 작업을 가져와 실행합니다.
콜스택과 마이크로태스크 큐의 우선순위:
- 현재 실행 중인 동기 코드 완료 (콜스택 비우기)
- 마이크로태스크 큐 전체 실행 (Promise 핸들러)
- 매크로태스크 큐에서 하나 실행 (setTimeout, setInterval)
- 렌더링 (필요 시)
- 1번으로 반복
이 메커니즘으로 인해 Promise는 setTimeout보다 항상 먼저 실행되며, 이는 JavaScript의 비동기 실행 순서를 결정짓는 핵심 원리입니다.
실행 컨텍스트의 내부 구성 요소
입문
실행 컨텍스트(작업 공간) 안에는 세 가지 중요한 서랍이 있어요. 각 서랍이 무엇을 담고 있는지 알아볼까요!
📦 변수 환경(Variable Environment) - 물건 보관함 책상에 물건을 보관하는 서랍이 있다고 생각해보세요. 펜, 지우개, 공책 같은 물건들을 여기에 넣어둬요. JavaScript의 변수 환경도 똑같아요! 여러분이 만든 변수와 함수를 여기에 보관해요. 나중에 필요할 때 이 서랍을 열어서 꺼내 쓰는 거죠.
🔍 렉시컬 환경(Lexical Environment) - 찾기 규칙 도서관에서 책을 찾을 때 분류 번호를 따라가죠? ‘과학 책은 500번대, 역사 책은 900번대’처럼 규칙이 있어요. 렉시컬 환경은 변수를 찾는 규칙이에요! “이 변수는 여기 있고, 없으면 바깥 함수에서 찾고, 거기도 없으면 전역에서 찾아봐”라는 규칙을 저장해둬요.
👤 this 바인딩 - 현재 주인공 연극을 할 때 “이 장면의 주인공은 누구?”를 알아야 하죠. JavaScript의 ‘this’도 비슷해요! 지금 실행 중인 코드에서 “주인공이 누구인지” 알려주는 거예요. 함수를 어떻게 호출했느냐에 따라 주인공이 달라져요.
🎯 왜 세 가지로 나눴나요? 책상 서랍을 용도별로 나누면 정리하기 쉽죠? 연필은 연필 칸, 지우개는 지우개 칸에 넣듯이요. JavaScript도 변수 저장, 변수 찾기, 주인공 결정을 각각 따로 관리하면 코드가 더 정확하게 실행될 수 있어요!
⏰ 언제 만들어지나요? 함수를 호출하면 JavaScript는 먼저 이 세 가지 서랍을 준비해요. 그리고 나서 함수 안의 코드를 실행하기 시작하죠. 마치 요리하기 전에 재료를 미리 꺼내두는 것과 같아요!
중급
실행 컨텍스트는 세 가지 핵심 컴포넌트로 구성되며, 각 컴포넌트는 코드 실행에 필요한 특정 정보를 관리합니다.
1. Variable Environment (변수 환경) var 선언과 함수 선언의 초기 바인딩을 저장합니다:
- var로 선언된 변수
- 함수 선언문
- 함수 매개변수
- arguments 객체
function example(param) {
var localVar = 'local';
function innerFunc() {
console.log('inner');
}
// Variable Environment에 저장됨:
// - param (매개변수)
// - localVar (var 변수)
// - innerFunc (함수 선언)
// - arguments 객체
}
2. Lexical Environment (렉시컬 환경) let/const 선언과 스코프 체인을 관리합니다:
- let, const로 선언된 변수
- 외부 환경 참조 (스코프 체인)
- 실행 중 변수 바인딩 추가 가능
function outer() {
let outerVar = 'outer';
function inner() {
let innerVar = 'inner';
console.log(outerVar); // 외부 환경 참조를 통해 접근
}
inner();
// inner의 Lexical Environment는 outer 환경을 참조
}
3. ThisBinding (this 바인딩) 현재 실행 컨텍스트의 this 값을 결정합니다:
- 일반 함수 호출: undefined (strict mode) 또는 전역 객체
- 메서드 호출: 호출한 객체
- 생성자 호출: 새로 생성된 객체
- 화살표 함수: 상위 스코프의 this
const obj = {
name: 'Object',
regularMethod: function() {
console.log(this.name); // 'Object' - obj가 this
},
arrowMethod: () => {
console.log(this.name); // undefined - 상위 스코프의 this
}
};
obj.regularMethod();
obj.arrowMethod();
컴포넌트 간의 관계
- Variable Environment와 Lexical Environment는 생성 시점에 동일한 값으로 시작
- with 문이나 catch 절 실행 시 Lexical Environment만 임시로 변경
- ThisBinding은 함수 호출 방식에 따라 독립적으로 결정
심화
실행 컨텍스트의 세 가지 컴포넌트는 ECMAScript 명세의 환경 레코드(Environment Record) 추상화를 기반으로 구현되며, 각각 명확히 정의된 의미론적 역할을 수행합니다.
Variable Environment의 명세 정의 ECMAScript 2023, Section 9.1 (Environment Records)에 따르면, Variable Environment는 VariableStatement (var 선언)에 의해 생성된 바인딩을 저장하는 Lexical Environment입니다. 실행 컨텍스트 생성 시점에 LexicalEnvironment와 동일한 값으로 초기화되지만, 이후 독립적으로 관리됩니다.
Variable Environment의 불변성(Immutability): 일단 생성된 후 Variable Environment 자체는 변경되지 않습니다. with 문이나 catch 절로 인해 LexicalEnvironment가 임시로 변경되더라도, Variable Environment는 원본 환경을 보존하여 var 선언의 스코프를 보장합니다.
Lexical Environment의 계층 구조 Lexical Environment는 Environment Record와 외부 환경 참조(Outer Environment Reference)로 구성됩니다:
LexicalEnvironment = {
EnvironmentRecord: {
Type: Declarative | Object | Function | Global | Module,
Bindings: Map<Identifier, Value>
},
OuterEnvironment: LexicalEnvironment | null
}
스코프 체인 해결 알고리즘 (GetIdentifierReference):
- 현재 Environment Record에서 식별자 검색
- 발견되면 해당 바인딩 반환
- 발견되지 않으면 OuterEnvironment에서 재귀적으로 검색
- OuterEnvironment가 null이면 ReferenceError 발생
V8 엔진의 최적화: V8은 스코프 체인을 Context 객체의 연결 리스트로 구현하며, Inline Caching을 통해 자주 접근되는 변수의 위치를 캐싱합니다. 첫 번째 접근 시 O(n) 탐색 후, 이후 접근은 O(1)로 최적화됩니다.
ThisBinding의 결정 메커니즘 ECMAScript 명세 Section 10.2.1.1 (PrepareForOrdinaryCall)에서 정의된 this 바인딩 규칙:
- 일반 함수 호출: ResolveThisBinding() 추상 연산이 undefined 반환 (strict mode), 또는 전역 객체 반환 (non-strict mode)
- 메서드 호출: MakeSuperPropertyReference 알고리즘이 호출 객체를 this로 바인딩
- 생성자 호출: OrdinaryCreateFromConstructor가 새 객체를 생성하고 this로 바인딩
- call/apply/bind: 명시적으로 this 값 설정
- 화살표 함수: [[ThisMode]]가 ‘lexical’로 설정되어 상위 스코프의 this 상속
화살표 함수의 특수성: 화살표 함수는 자체 ThisBinding을 생성하지 않습니다. 대신 [[Environment]] 슬롯을 통해 상위 렉시컬 스코프의 this를 참조합니다. 이는 ECMAScript 명세 Section 10.2.10 (FunctionDeclarationInstantiation)에서 [[ThisMode]]가 ‘lexical’인 경우 ThisBinding 단계를 건너뛰도록 명시되어 있습니다.
Environment Record 타입별 특성 ECMAScript는 다섯 가지 Environment Record 타입을 정의합니다:
- Declarative Environment Record: 함수 스코프의 기본 타입, let/const/var/function 바인딩 관리
- Object Environment Record: with 문과 전역 객체 바인딩에 사용, 일반 객체의 프로퍼티를 환경 레코드처럼 접근
- Function Environment Record: 함수의 최상위 스코프, ThisBinding과 [[NewTarget]] 관리
- Global Environment Record: Declarative + Object Record의 복합 타입, var는 Object Record에, let/const는 Declarative Record에 바인딩
- Module Environment Record: ES6 모듈의 최상위 스코프, import 바인딩은 불변(Immutable)
성능 특성: Declarative Environment Record는 맵(Map) 자료구조로 구현되어 O(1) 바인딩 접근이 가능하지만, Object Environment Record는 일반 객체의 프로퍼티 접근 메커니즘을 사용하므로 프로토타입 체인 탐색 등으로 인해 상대적으로 느립니다. 이것이 전역 스코프에서 let/const를 권장하는 성능상의 이유 중 하나입니다.