JavaScript에서 함수는 어떻게 실행될까? (feat. 실행 컨텍스트)
실행 컨텍스트란 무엇인가?
JavaScript 엔진이 코드를 실행할 때, 자신의 코드가 어떤 환경에서 실행되는지 어떻게 알 수가 있을까요? 예를 들어 아래 사항을 어떻게 파악할 수 있을까요?
- 지금 코드가 어떤 스코프에서 실행되고 있는지
- 코드 안
a변수의 현재 값이 무엇인지 this가 무엇을 가리키는지
이러한 정보가 없으면 엔진은 코드를 제대로 실행할 수 없습니다. 그래서 JavaScript 엔진은 코드를 실행할 때 실행 환경을 하나 만듭니다. 바로 이 환경을 Execution Context(실행 컨텍스트) 라고 합니다.
Execution Context(실행 컨텍스트)는 코드 실행에 필요한 정보를 저장하는 실행 환경입니다.
실행 컨텍스트는 코드가 실행되는 순간 생성됩니다.
실행 컨텍스트의 종류
Execution Context는 세 가지 종류가 있습니다.
| 종류 | 설명 |
|---|---|
| Global Execution Context | 프로그램 시작 시 생성 |
| Function Execution Context | 함수 호출 시 생성 |
| Eval Execution Context | eval 실행 시 생성 |
실제로 우리가 작성하는 대부분의 코드는 Function Execution Context 안에서 실행됩니다. 그래서 이번 글에서는 함수 실행 컨텍스트를 중심으로 살펴보겠습니다.
함수 실행 컨텍스트의 구조
함수를 호출하면 Function Execution Context가 생성됩니다. 함수 실행 컨텍스트의 큰 구조를 먼저 살펴볼까요?
Function Execution Context (함수 실행 컨텍스트)
│
├─ VariableEnvironment
│ ├─ EnvironmentRecord (실제 변수 저장)
│ │ a → parameter
│ │ b → var
│ └─ outer reference (외부 스코프 참조)
│
├─ LexicalEnvironment
│ ├─ EnvironmentRecord (실제 변수 저장)
│ │ c → let
│ └─ outer reference (외부 스코프 참조)
├─ PrivateEnvironment (class 관련)
└─ thisBinding여기서 가장 중요한 부분은 두 가지입니다.
LexicalEnvironment(이하 렉시컬 환경)VariableEnvironment(이하 변수 환경)
둘 다 아래 구조로 변수와 상위 스코프에 대한 정보를 저장합니다.
EnvironmentRecord(환경 레코드): 식별자(변수, 함수 등의 이름)와 그에 대응하는 값을 저장합니다.outer: 상위 스코프에 대한 정보를 저장하고 있습니다.
하지만 렉시컬 환경과 변수 환경은 미묘하게 다릅니다. 각 차이는 무엇일까요? 그리고 구조가 비슷한데 왜 두 개로 관리되는 걸까요?
변수 환경
먼저 VariableEnvironment를 살펴보겠습니다. 사실 변수 환경 VariableEnvironment은 렉시컬 환경 이전부터 있던 환경으로, 특히 ES6 이전의 변수 처리 방식과 관련이 있습니다.
변수 환경은 ES6 이전에 함수 스코프 기반 변수의 초기 환경입니다. 변수 환경이 관리하는 내용은 아래와 같습니다.
var- 함수 선언 (function declaration)
- 매개변수 (parameter)
아래 코드를 함께 살펴볼까요?
function foo(a) {
var b = 1;
function bar() {}
}위 코드에서는 아래와 같은 변수 환경이 생성됩니다.
VariableEnvironment
└─ EnvironmentRecord (환경 레코드)
a → parameter
b → var
bar → function이후에 블록 스코프 기반의 let, const 등이 나오면서, 이를 효율적으로 관리할 새로운 환경이 필요하게 되었습니다. 그래서 등장하게 된 것이 렉시컬 환경입니다.
렉시컬 환경
LexicalEnvironment는 현재 스코프의 실제 변수 환경입니다. 렉시컬 환경은 블록 스코프 기반의 환경을 처리합니다. JavaScript 엔진은 변수를 찾을 때 가장 먼저 이 환경을 확인합니다.
letconst- block scope
- catch scope
- class scope
예를 들어 이런 코드가 있습니다.
function foo() {
let a = 1
const b = 2
}위 코드는 아래와 같은 렉시컬 환경을 생성합니다.
LexicalEnvironment
└─ EnvironmentRecord (환경 레코드)
a → let
b → const블록 스코프가 함수 안에 생긴다면?
위 함수에서 아래와 같이 block scope가 생성될 때가 생긴다면 어떻게 될까요?
function foo() {
let a = 1
{
let b = 2
}
}블록이 생기면, 해당 블록에 대한 렉시컬 스코프 LexicalEnvironment만 새로 생성하여 관리합니다.
이 때 생성되는 렉시컬 스코프를 좀 더 정확하게 블록 렉시컬 환경이라 부릅니다.
LexicalEnvironment (foo 함수 렉시컬 환경)
a → 1
LexicalEnvironment (블록 렉시컬 환경)
b → 2렉시컬 환경은 let, const, block뿐만 아니라 catch, class 스코프 등에 대한 환경도 저장한다고 했는데요.
정확히는 아래와 같이 렉시컬 환경의 환경 레코드에 저장되게 됩니다.
LexicalEnvironment
├─ EnvironmentRecord (환경 레코드) -> 상황에 따라 달라진다.
└─ outer reference (상위 스코프 참조)렉시컬 환경의 환경 레코드는 하나의 형태만 있는 것이 아닙니다. 어떤 상황에서 생성되느냐에 따라 내부 Record 타입이 조금씩 달라집니다.
| EnvironmentRecord 종류 | 언제 생성되는가 |
|---|---|
| DeclarativeEnvironmentRecord | block scope (let, const) |
| FunctionEnvironmentRecord | 함수 실행 시 |
| GlobalEnvironmentRecord | 전역 코드 실행 시 |
| ObjectEnvironmentRecord | with 문 사용 시 |
| ModuleEnvironmentRecord | ES Module 실행 시 |
변수 환경은 왜 남아있는가?
ECMAScript의 스펙에 따르면, 아직 변수 환경에 대한 개념이 스펙에 정의 되어있습니다. 이는 spec 호환성 + 초기 상태 보존 때문입니다. spec에서는 다음 개념을 유지합니다.

글을 마치며
지금까지 실행 컨텍스트, 그 중에서도 함수 실행 컨텍스트에 대해 자세히 알아봤습니다. 하지만 여기까지의 내용은 ECMAScript spec 기준의 설명입니다.
스펙은 JavaScript의 동작 규칙을 정의하지만, 이 구조들이 실제로 어떤 객체로 만들어지고 메모리에 어떻게 올라가는지는 설명하지 않습니다. 그렇다면 실제 JavaScript 엔진에서는 실행 컨텍스트가 어떤 구조로 구현되어 있을까요? 그리고 어떻게 메모리에 올라가게 될까요?
다음 글에서는 V8 엔진 기준으로 실행 컨텍스트가 실제로 어떻게 만들어지는지 살펴보겠습니다.