Variable Environment와 Lexical Environment의 차이는?

실행 컨텍스트 내 두 환경 컴포넌트의 역할 차이와 var, let/const가 각각 다르게 처리되는 원리를 학습합니다

중급 15분 Variable Environment Lexical Environment var let/const

JavaScript 실행 컨텍스트는 두 가지 핵심 환경 컴포넌트를 포함하고 있습니다. Variable Environment와 Lexical Environment입니다. 이 두 환경은 변수와 함수 선언을 저장하고 관리하는 역할을 하지만, 각각 다른 목적과 동작 방식을 가지고 있습니다. 특히 var 키워드와 let/const 키워드가 서로 다른 환경에서 처리되는 방식을 이해하면, JavaScript의 스코프와 호이스팅 동작을 훨씬 깊이 있게 파악할 수 있습니다. 이 차이를 모르면 예상치 못한 변수 참조 오류나 스코프 문제에 직면할 수 있으며, 특히 블록 스코프와 함수 스코프가 혼재된 복잡한 코드에서 디버깅이 매우 어려워질 수 있습니다.

📋 핵심 차이점

  • Variable Environment: 함수 실행 시점의 초기 변수 상태를 유지하며, 주로 var 선언과 함수 선언을 저장하는 정적 스냅샷
  • Lexical Environment: 현재 실행 중인 코드의 변수 상태를 동적으로 관리하며, let/const 선언과 블록 스코프를 처리
  • 참조 관계: Variable Environment는 생성 시점에 Lexical Environment를 복사하지만, 이후 독립적으로 동작
  • 스코프 체인: Lexical Environment만 외부 환경 참조를 통해 스코프 체인을 구성하며 변수 탐색에 사용됨
  • TDZ 관리: Temporal Dead Zone은 Lexical Environment에서만 관리되며, let/const의 초기화 전 접근을 방지

🎯 실무에서의 영향

이 두 환경의 차이를 이해하면 var와 let/const의 근본적인 동작 차이를 명확히 파악할 수 있습니다. 예를 들어, 클로저를 사용할 때 var 변수가 예상과 다르게 동작하는 이유, 블록 스코프 내에서 let/const가 호이스팅되지만 접근할 수 없는 이유를 정확히 설명할 수 있게 됩니다. 실무에서는 레거시 코드를 리팩토링할 때 var를 let/const로 안전하게 변환하거나, 복잡한 스코프 구조에서 변수 참조 오류를 디버깅할 때 이 지식이 필수적입니다. 또한 JavaScript 엔진의 최적화 방식을 이해하는 데도 도움이 되며, 메모리 누수나 성능 문제를 진단할 때 Variable Environment와 Lexical Environment의 생명주기를 고려할 수 있습니다. 특히 모듈 시스템이나 비동기 코드에서 스코프 관리가 복잡해질 때, 이 두 환경의 역할을 정확히 이해하면 더 안정적이고 예측 가능한 코드를 작성할 수 있습니다.


핵심 개념

Variable Environment의 역할

입문

Variable Environment는 함수가 시작될 때의 변수 상태를 ‘사진 찍듯이’ 저장하는 공간이에요. 마치 타임캡슐처럼 초기 상태를 보관한답니다!

📸 시간을 멈춘 사진 여러분이 친구들과 단체 사진을 찍는다고 상상해보세요. 사진을 찍는 순간의 모습이 그대로 사진에 남겠죠? Variable Environment도 마찬가지예요. 함수가 시작되는 순간의 변수 상태를 ‘찰칵’ 하고 사진처럼 저장해둬요.

🎒 타임캡슐 속의 물건들 초등학교 졸업할 때 타임캡슐을 묻어본 적 있나요? 그 안에 넣은 물건들은 시간이 지나도 그대로 보존되죠. Variable Environment는 var로 선언한 변수와 함수 선언을 이렇게 타임캡슐처럼 보관해요. 나중에 코드가 실행되면서 변수 값이 바뀌어도, 처음 시작했을 때의 상태는 그대로 기억하고 있어요.

🏠 변하지 않는 집 주소 이사를 가도 예전 집 주소는 바뀌지 않죠? Variable Environment도 마찬가지예요. 함수가 실행되면서 새로운 변수가 추가되거나 값이 바뀌어도, Variable Environment에 저장된 초기 상태는 그대로 유지돼요.

📦 var 변수의 보관함 Variable Environment는 특히 var로 선언한 변수들을 저장하는 걸 좋아해요. var는 함수 전체에서 사용할 수 있는 변수라서, 함수가 시작될 때 바로 Variable Environment에 자리를 잡아요.

중급

Variable Environment는 실행 컨텍스트의 구성 요소 중 하나로, 함수가 생성될 때의 초기 변수 바인딩(variable bindings)을 저장하는 환경 레코드입니다.

Variable Environment의 주요 특징

  • 함수 실행 시점에 Lexical Environment의 스냅샷(snapshot)을 생성하여 저장
  • 주로 var 선언과 함수 선언(Function Declaration)을 관리
  • 함수의 초기 상태를 보존하며, 이후 변경사항은 반영되지 않음
  • with 문이나 catch 블록처럼 스코프가 동적으로 변경될 때 원본 환경을 보존하는 역할
function outer() {
  var x = 10; // Variable Environment에 저장

  function inner() {
    console.log(x); // outer의 Variable Environment 참조
  }

  x = 20; // 값이 변경되지만 Variable Environment 구조는 유지
  inner(); // 20 출력 (현재 값 참조)
}

outer();

실행 컨텍스트 생성 과정

  1. 함수가 호출되면 새로운 실행 컨텍스트 생성
  2. Variable Environment가 먼저 생성되어 초기 바인딩 설정
  3. Lexical Environment가 Variable Environment를 참조하여 생성
  4. 이후 코드 실행 중에는 Lexical Environment만 업데이트
function example() {
  console.log(a); // undefined (호이스팅)
  var a = 1;

  if (true) {
    var b = 2; // 블록 무시, Variable Environment에 등록
  }

  console.log(b); // 2 (함수 스코프)
}

심화

Variable Environment는 ECMAScript 명세 9.1 Environment Records에 정의된 실행 컨텍스트의 정적 환경 컴포넌트로, 함수 인스턴스화(Function Instantiation) 시점의 불변 스냅샷을 유지합니다.

ECMAScript 명세 기반 Variable Environment 구조 ECMAScript 2023, Section 9.4 Execution Contexts에 따르면, Variable Environment는 VariableEnvironment 컴포넌트로 실행 컨텍스트에 포함됩니다. 이는 Environment Record 타입의 값으로, 다음과 같은 특성을 가집니다:

  • Immutable Reference: 생성 후 참조가 변경되지 않는 불변 환경 레코드
  • Snapshot Semantics: FunctionDeclarationInstantiation 완료 시점의 바인딩 상태를 고정
  • var Binding Target: var 선언은 항상 Variable Environment의 Environment Record에 바인딩
  • Function Declaration Hosting: FunctionDeclaration은 Variable Environment에 초기화

동적 스코프 보존 메커니즘 Variable Environment의 핵심 설계 목적은 with 문(Section 14.11)과 catch 절(Section 14.15.1)에서 Lexical Environment가 임시로 교체될 때 원본 환경을 보존하는 것입니다.

// Pseudo-code for with statement execution
1. oldEnv = LexicalEnvironment
2. newEnv = NewObjectEnvironment(withObject, oldEnv)
3. LexicalEnvironment = newEnv  // 임시 교체
4. Execute WithStatement
5. LexicalEnvironment = oldEnv  // 복원 (VariableEnvironment는 불변)

V8 엔진의 Variable Environment 구현 V8 엔진(TurboFan, Ignition 인터프리터)에서 Variable Environment는 Context 객체의 일부로 구현됩니다:

  • Context Chain: Variable Environment는 Context의 불변 슬롯(slot)으로 저장
  • Lazy Allocation: 실제로 with/catch가 없으면 Lexical Environment와 동일한 포인터 사용 (메모리 최적화)
  • Inline Caching: Variable Environment의 불변성을 활용하여 변수 접근을 인라인 캐시로 최적화
  • Scope Analysis: Bytecode 생성 시 Variable Environment 접근 패턴을 분석하여 직접 슬롯 접근으로 변환

메모리 및 성능 특성

  • Variable Environment는 함수 실행 컨텍스트와 생명주기를 공유하므로 클로저가 없으면 함수 종료 시 가비지 컬렉션
  • with 문 사용 시 Variable Environment 보존으로 인한 메모리 오버헤드 발생 (약 24-48 bytes per context, V8 기준)
  • 현대 JavaScript (ES6+)에서는 with 문이 strict mode에서 금지되어 Variable Environment의 실질적 역할이 감소

Lexical Environment의 역할

입문

Lexical Environment는 현재 실행 중인 코드의 변수들을 실시간으로 관리하는 ‘살아있는 노트’예요. 코드가 실행되면서 계속 업데이트돼요!

📝 계속 쓰이는 일기장 매일매일 일기를 쓰는 일기장을 생각해보세요. 오늘 일어난 일을 쓰고, 내일 또 새로운 내용을 추가하죠. Lexical Environment도 마찬가지예요. 코드가 실행되면서 새로운 변수가 생기면 바로바로 기록하고, 값이 바뀌면 그것도 즉시 업데이트해요.

🎯 지금 이 순간의 상태 Variable Environment가 ‘과거의 사진’이라면, Lexical Environment는 ‘지금 여기’를 보여주는 거울이에요. 코드가 실행되는 바로 그 순간의 변수 상태를 보여주는 거죠.

🔗 부모와 연결된 끈 여러분이 부모님과 손을 잡고 걷는다고 상상해보세요. Lexical Environment도 ‘부모 환경’과 손을 잡고 있어요. 이 끈을 통해서 바깥 범위의 변수들을 찾아갈 수 있답니다. 이게 바로 ‘스코프 체인’이에요!

🌱 let과 const의 집 let과 const로 선언한 변수들은 Lexical Environment에 살아요. 특히 블록 {} 안에서 선언하면, 그 블록만의 새로운 Lexical Environment가 만들어져요. 마치 방 안에 또 작은 방을 만드는 것처럼요!

⏰ TDZ(접근 금지 시간) Lexical Environment에는 특별한 규칙이 있어요. let이나 const로 변수를 선언하기 전에는 절대 그 변수를 사용할 수 없어요. 마치 “아직 준비 안 됐어요!” 하고 막는 것처럼요. 이걸 Temporal Dead Zone이라고 불러요.

중급

Lexical Environment는 현재 실행 중인 코드의 식별자(identifier) 바인딩을 동적으로 관리하는 환경 레코드이며, 스코프 체인(scope chain)을 통해 외부 환경과 연결됩니다.

Lexical Environment의 주요 특징

  • 코드 실행 중 계속 업데이트되는 동적 환경
  • let, const 선언을 관리하고 블록 스코프(block scope)를 지원
  • Outer Environment Reference를 통해 외부 렉시컬 환경과 연결 (스코프 체인)
  • Temporal Dead Zone(TDZ) 관리 - 선언 전 접근 방지
function example() {
  // 함수 Lexical Environment 생성
  let x = 1;

  if (true) {
    // 새로운 블록 Lexical Environment 생성
    let y = 2;
    console.log(x); // 1 (외부 환경 참조)
    console.log(y); // 2 (현재 환경)
  }

  // console.log(y); // ReferenceError (블록 환경 종료)
}

스코프 체인을 통한 변수 탐색 변수를 참조할 때 JavaScript 엔진은 다음 순서로 탐색합니다:

  1. 현재 Lexical Environment의 Environment Record에서 검색
  2. 없으면 Outer Environment Reference를 따라 부모 환경으로 이동
  3. Global Environment까지 탐색하며, 찾지 못하면 ReferenceError
const global = 'global';

function outer() {
  const outerVar = 'outer';

  function inner() {
    const innerVar = 'inner';
    console.log(innerVar);  // 1. 현재 환경
    console.log(outerVar);  // 2. 부모 환경
    console.log(global);    // 3. 전역 환경
  }

  inner();
}

TDZ(Temporal Dead Zone) 관리 let/const는 호이스팅되지만 초기화 전까지 TDZ 상태로 관리되어 접근이 불가능합니다.

function example() {
  // TDZ 시작 (x는 선언되었지만 초기화 안됨)
  console.log(x); // ReferenceError: Cannot access 'x' before initialization

  let x = 10; // TDZ 종료, 초기화 완료
  console.log(x); // 10
}

심화

Lexical Environment는 ECMAScript 명세 9.1 Environment Records의 핵심 구현으로, 정적 스코프(lexical scope) 결정과 동적 바인딩 관리를 담당하는 실행 시간 환경 구조입니다.

ECMAScript 명세 기반 Lexical Environment 구조 ECMAScript 2023, Section 9.1에 따르면, Lexical Environment는 다음 두 컴포넌트로 구성됩니다:

  • Environment Record: 식별자 바인딩을 저장하는 레코드 (Declarative, Object, Global, Function, Module 타입)
  • Outer Environment Reference: 외부 렉시컬 환경에 대한 null 가능 참조 (스코프 체인 구성)

Environment Record 타입별 특성

  • Declarative Environment Record: let/const/class/function 등의 선언적 바인딩 관리
  • Object Environment Record: with 문이나 전역 객체의 프로퍼티 기반 바인딩
  • Function Environment Record: this, super, new.target 등 함수 관련 바인딩 추가
  • Global Environment Record: Declarative + Object 하이브리드 구조 (let/const + var/function)
  • Module Environment Record: import 바인딩의 불변성 보장

BlockDeclarationInstantiation과 TDZ Section 14.2.4 Runtime Semantics: BlockDeclarationInstantiation에서 블록 진입 시 Lexical Environment 생성 과정을 정의합니다:

1. oldEnv = running execution context's LexicalEnvironment
2. blockEnv = NewDeclarativeEnvironment(oldEnv)  // 새 환경 생성
3. for each lexical declaration in block:
     3a. InitializeBoundName(name, undefined)  // 바인딩만 생성 (TDZ 시작)
4. Set running execution context's LexicalEnvironment to blockEnv
5. Execute block statements
6. Set running execution context's LexicalEnvironment to oldEnv  // 복원

초기화(initialization)는 실제 선언문 실행 시 발생하며, 그 전까지는 TDZ 상태로 접근 시 ReferenceError를 발생시킵니다.

V8 엔진의 Lexical Environment 구현 및 최적화 V8 엔진에서 Lexical Environment는 Context 객체와 ScopeInfo 메타데이터로 구현됩니다:

  • Context Object: 런타임 변수 값을 저장하는 힙 객체 (클로저가 캡처하면 힙에 유지)
  • ScopeInfo: 컴파일 타임에 생성된 변수 레이아웃 메타데이터 (불변, 코드 공유)
  • Stack Allocation: 클로저 없는 경우 스택에 직접 할당 (Context 생성 생략)
  • Context Chain: Outer Reference는 Context의 previous 포인터로 구현

Escape Analysis와 최적화 TurboFan 최적화 컴파일러는 Escape Analysis를 통해 Lexical Environment의 생명주기를 분석합니다:

  • 변수가 클로저에 캡처되지 않으면 스택 할당으로 최적화 (Context 생성 생략)
  • 블록 스코프 변수가 외부에서 참조되지 않으면 인라인화 (Environment 생성 생략)
  • 스코프 체인 탐색을 컴파일 타임 슬롯 인덱스 계산으로 대체 (O(1) 접근)

성능 및 메모리 특성

  • 블록 스코프마다 새 Lexical Environment 생성 비용은 클로저 없으면 무시 가능 (스택 포인터 조정)
  • 클로저 캡처 시 Context 객체 힙 할당: 약 32-64 bytes overhead (V8 기준)
  • TDZ 체크는 바이트코드에 명시적 체크 명령어 삽입 (약 2-4% 오버헤드, 벤치마크 기준)

Variable Environment와 Lexical Environment의 차이

입문

두 환경의 가장 큰 차이는 ‘과거의 사진’과 ‘현재의 거울’이라는 점이에요!

📸 사진 vs 🪞 거울 Variable Environment는 함수가 시작될 때 찍은 사진처럼 변하지 않아요. 반면에 Lexical Environment는 거울처럼 지금 이 순간을 보여줘요. 거울을 보면 지금 내 모습이 보이듯이, Lexical Environment는 지금 실행 중인 코드의 변수 상태를 보여줘요.

🏠 오래된 집 vs 🏗️ 새 건물 Variable Environment는 오래된 집처럼 한번 지으면 구조가 안 바뀌어요. 하지만 Lexical Environment는 공사 중인 건물처럼 계속 새로운 층(블록)을 추가할 수 있어요. if문 안에서 let을 쓰면 새로운 층이 생기는 거예요!

👥 var와 let/const의 각자 집 var로 만든 변수는 Variable Environment라는 큰 집에 살고, let과 const로 만든 변수는 Lexical Environment라는 새 집에 살아요. 그래서 var는 함수 전체에서 보이지만, let과 const는 블록 안에서만 보이는 거예요.

🔗 연결 고리의 차이 Lexical Environment는 부모 환경과 연결된 끈을 가지고 있어요. 이 끈을 따라가면 바깥 범위의 변수를 찾을 수 있죠. Variable Environment는 이런 끈이 없어서 그냥 자기만의 정보만 가지고 있어요.

⏰ 시간 규칙 Variable Environment의 var는 호이스팅으로 미리 준비되어서 언제든 쓸 수 있어요 (값은 undefined). 하지만 Lexical Environment의 let/const는 선언하기 전에는 절대 못 써요. 이게 바로 TDZ 규칙이에요!

중급

Variable Environment와 Lexical Environment는 실행 컨텍스트 내에서 서로 다른 역할과 생명주기를 가진 환경 컴포넌트입니다.

핵심 차이점

  1. 변경 가능성

    • Variable Environment: 생성 후 참조가 불변 (immutable reference)
    • Lexical Environment: 코드 실행 중 동적으로 업데이트
  2. 관리 대상

    • Variable Environment: var 선언, 함수 선언
    • Lexical Environment: let/const 선언, 블록 스코프
  3. 스코프 체인

    • Variable Environment: 스코프 체인에 직접 참여하지 않음
    • Lexical Environment: Outer Reference를 통해 스코프 체인 구성
  4. 생명주기

    • Variable Environment: 함수 실행 컨텍스트 생성 시 한 번 설정
    • Lexical Environment: 블록 진입/종료 시마다 생성/폐기
function compare() {
  // Variable Environment에 var a 등록 (호이스팅)
  console.log(a); // undefined (접근 가능)
  var a = 1;

  // Lexical Environment에 let b 등록 (TDZ)
  // console.log(b); // ReferenceError (접근 불가)
  let b = 2;

  if (true) {
    // 새로운 Lexical Environment 생성
    let c = 3;
    var d = 4; // Variable Environment에 등록 (블록 무시)
  }

  console.log(d); // 4 (Variable Environment)
  // console.log(c); // ReferenceError (Lexical Environment 종료)
}

초기화 과정 차이

함수 실행 컨텍스트 생성 시:

  1. Variable Environment 생성 및 var/함수 선언 바인딩
  2. Lexical Environment가 Variable Environment를 참조하여 생성
  3. 이후 let/const 선언 시 Lexical Environment만 업데이트
function example() {
  // 1. Variable Environment: { a: undefined, func: <function> }
  // 2. Lexical Environment: Variable Environment 복사

  console.log(a); // undefined
  var a = 1;

  function func() { return 'function'; }

  // 3. let 선언 시 Lexical Environment만 업데이트
  let b = 2;

  if (true) {
    // 4. 새 블록 Lexical Environment 생성
    let c = 3;
  }
  // 5. 블록 종료 시 블록 Lexical Environment 폐기
}

심화

Variable Environment와 Lexical Environment의 차이는 ECMAScript 명세의 실행 컨텍스트 모델과 JavaScript의 이중 스코프 시스템(함수 스코프 + 블록 스코프)을 반영한 설계입니다.

ECMAScript 명세 기반 차이점 분석 Section 9.4 Execution Contexts에서 두 환경의 관계를 명확히 정의합니다:

  • VariableEnvironment: GetIdentifierReference 연산에서 직접 사용되지 않음, with/catch 복원용
  • LexicalEnvironment: 모든 식별자 해석(identifier resolution)의 시작점

함수 실행 컨텍스트 생성 시 (Section 10.2 ECMAScript Code Execution Contexts):

1. VariableEnvironment = NewFunctionEnvironment(F, newTarget)
2. LexicalEnvironment = VariableEnvironment  // 초기에는 동일 참조
3. FunctionDeclarationInstantiation 실행 (var, 함수 선언 처리)
4. 이후 블록/let/const 선언 시 LexicalEnvironment만 교체

with 문과 catch 절에서의 역할 Variable Environment의 핵심 존재 이유는 동적 스코프 보존입니다:

function example() {
  var x = 1;
  let y = 2;

  with ({ x: 10 }) {
    // LexicalEnvironment: NewObjectEnvironment({ x: 10 }, outer)
    // VariableEnvironment: 원본 유지 (x=1 보존)
    console.log(x); // 10 (with 객체)
    console.log(y); // 2 (외부 Lexical Environment)
  }

  // LexicalEnvironment 복원 (VariableEnvironment 참조)
  console.log(x); // 1 (원본 복원)
}

var vs let/const의 환경 분리 이유 ECMAScript 위원회는 하위 호환성(var 동작 유지)과 블록 스코프 도입(let/const)을 동시에 달성하기 위해 두 환경을 분리했습니다:

  • var의 함수 스코프: Variable Environment의 Function Environment Record에 바인딩
  • let/const의 블록 스코프: Lexical Environment의 Declarative Environment Record에 바인딩
  • 호이스팅 차이: var는 FunctionDeclarationInstantiation에서 undefined 초기화, let/const는 바인딩만 생성

V8 엔진의 최적화 전략 V8은 대부분의 경우 두 환경을 동일한 Context 객체로 통합하여 메모리를 절약합니다:

  • Fast Path: with/catch 없으면 VariableEnvironment와 LexicalEnvironment가 동일 Context 포인터 공유
  • Slow Path: with/catch 진입 시에만 별도 Context 할당 및 복원 로직 실행
  • Scope Analysis: 컴파일 타임에 with/catch 사용 여부를 분석하여 Fast Path 선택

Inline Caching과 환경 접근 변수 접근 시 두 환경의 차이는 Inline Cache 최적화에 영향을 줍니다:

  • Variable Environment의 var: 항상 동일한 Context 슬롯 인덱스 → Monomorphic IC (단일 형태 캐시)
  • Lexical Environment의 let/const: 블록 깊이에 따라 다른 Context 깊이 → Polymorphic IC 가능

성능 벤치마크 V8 9.0+ 기준, 두 환경의 성능 차이는 with/catch 없는 일반 코드에서는 측정 불가능 수준(<0.1%)이며, with 문 사용 시에만 약 5-15%의 오버헤드가 발생합니다.

클로저와 두 환경의 관계

입문

클로저는 함수가 자기가 태어난 환경을 기억하는 마법이에요. 이때 두 환경이 함께 협력한답니다!

🎒 추억이 담긴 가방 여행을 떠날 때 집에서 가져온 물건들을 가방에 넣잖아요? 클로저도 마찬가지예요. 함수가 바깥 세상으로 나갈 때, 자기가 태어난 환경의 변수들을 가방에 담아서 가져가요. 나중에 어디서 함수를 실행하든, 그 가방을 열면 고향의 변수들을 쓸 수 있어요!

🏠 집 주소를 기억하기 여러분이 친구 집에 가도 우리 집 주소는 잊지 않죠? 클로저의 Lexical Environment도 마찬가지예요. 함수가 어디로 가든, 자기가 태어난 곳의 ‘주소’(Outer Reference)를 계속 기억하고 있어요.

📦 var와 let/const의 짐 싸기 클로저가 변수를 가져갈 때, var로 만든 변수는 Variable Environment에서, let/const로 만든 변수는 Lexical Environment에서 가져와요. 둘 다 가방에 담지만, 원래 있던 집이 다른 거예요!

🔗 끊어지지 않는 연결 클로저가 생기면 함수가 끝나도 환경이 사라지지 않아요. 마치 전화선처럼 연결이 계속 유지돼요. 그래서 나중에 함수를 다시 부르면, 그때 썼던 변수 값을 그대로 쓸 수 있는 거예요!

⚠️ 반복문에서 조심하기 for 반복문에서 var를 쓰면 같은 Variable Environment를 공유해서 모든 클로저가 같은 값을 봐요. 하지만 let을 쓰면 반복할 때마다 새로운 Lexical Environment가 만들어져서 각자 다른 값을 기억해요!

중급

클로저(closure)는 함수가 생성될 때의 Lexical Environment를 캡처하여 함수 종료 후에도 외부 변수에 접근할 수 있게 하는 메커니즘입니다. Variable Environment와 Lexical Environment 모두 클로저에 의해 유지될 수 있습니다.

클로저의 환경 캡처 원리

  • 함수 객체는 내부 슬롯 [[Environment]]에 생성 시점의 Lexical Environment 참조를 저장
  • 함수 실행 시 이 참조를 Outer Environment Reference로 사용
  • 클로저가 존재하면 해당 환경은 가비지 컬렉션되지 않고 메모리에 유지
function createCounter() {
  let count = 0; // Lexical Environment에 저장

  return function increment() {
    count++; // 외부 Lexical Environment 참조
    return count;
  };
}

const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2
// createCounter의 Lexical Environment가 계속 유지됨

var vs let/const의 클로저 동작 차이

var는 함수 스코프이므로 반복문에서 클로저를 생성하면 모든 클로저가 동일한 Variable Environment를 참조합니다.

function createFunctionsWithVar() {
  var arr = [];
  for (var i = 0; i < 3; i++) {
    arr.push(function() {
      return i; // 모두 같은 i 참조 (Variable Environment)
    });
  }
  return arr;
}

const funcs = createFunctionsWithVar();
console.log(funcs[0]()); // 3
console.log(funcs[1]()); // 3
console.log(funcs[2]()); // 3 (모두 마지막 값)
function createFunctionsWithLet() {
  const arr = [];
  for (let i = 0; i < 3; i++) { // 반복마다 새 Lexical Environment
    arr.push(function() {
      return i; // 각자 다른 i 참조
    });
  }
  return arr;
}

const funcs = createFunctionsWithLet();
console.log(funcs[0]()); // 0
console.log(funcs[1]()); // 1
console.log(funcs[2]()); // 2 (각자 고유 값)

메모리 관리 클로저는 외부 환경을 참조하므로 메모리 누수의 원인이 될 수 있습니다. 필요 없어진 클로저는 참조를 제거하여 가비지 컬렉션이 가능하도록 해야 합니다.

심화

클로저의 환경 캡처는 ECMAScript 명세의 함수 객체 생성과 실행 컨텍스트 연결 메커니즘에 의해 구현되며, Variable Environment와 Lexical Environment의 생명주기 관리에 직접적인 영향을 줍니다.

ECMAScript 명세 기반 클로저 메커니즘 Section 10.2 ECMAScript Function Objects의 [[Environment]] 내부 슬롯은 함수 생성 시 현재 실행 컨텍스트의 LexicalEnvironment를 캡처합니다:

OrdinaryFunctionCreate(functionPrototype, ParameterList, Body, thisMode, Scope):
  1. F = new Function object
  2. F.[[Environment]] = Scope  // 현재 Lexical Environment 캡처
  3. F.[[FormalParameters]] = ParameterList
  4. ...

함수 호출 시 (Section 10.2.1 [[Call]]):

F.[[Call]](thisArgument, argumentsList):
  1. callerContext = running execution context
  2. calleeContext = PrepareForOrdinaryCall(F, undefined)
  3. OrdinaryCallBindThis(F, calleeContext, thisArgument)
  4. result = OrdinaryCallEvaluateBody(F, argumentsList)
  5. ...

PrepareForOrdinaryCall에서 새 함수 환경을 F.[[Environment]]를 Outer Reference로 하여 생성하므로, 클로저는 생성 시점의 Lexical Environment에 접근 가능합니다.

for 루프의 let 바인딩과 Per-Iteration Environment Section 14.7.4.7 ForStatement Runtime Semantics에서 let/const를 사용한 for 루프는 반복마다 새로운 Lexical Environment를 생성하도록 명세화되어 있습니다:

// Per-iteration environment 생성 과정
for (let i = 0; i < 3; i++) {
  // 각 반복마다:
  // 1. perIterationEnv = NewDeclarativeEnvironment(outerEnv)
  // 2. i를 perIterationEnv에 바인딩하고 현재 값 복사
  // 3. LexicalEnvironment = perIterationEnv
  // 4. 루프 본문 실행 (클로저 생성 시 이 환경 캡처)
}

이는 var에는 적용되지 않으며, var는 함수의 Variable Environment에 단일 바인딩만 생성합니다.

V8 엔진의 클로저 최적화 V8은 클로저 캡처 시 다음과 같은 최적화를 수행합니다:

  • Escape Analysis: 함수가 외부로 반환되지 않으면 클로저 생성 생략 (스택 할당 유지)
  • Partial Context: 클로저가 참조하는 변수만 Context에 포함 (불필요한 변수 제외)
  • Context Snapshot: 필요 시 Context를 스냅샷으로 복사하여 원본 Context 조기 해제
  • Inline Caching: 클로저의 변수 접근을 Context 슬롯 직접 접근으로 최적화

for-let의 Per-Iteration Environment 구현 V8은 for-let 루프를 다음과 같이 바이트코드로 변환합니다:

// Conceptual bytecode
CreateFunctionContext [1]  // 외부 환경
Ldar r0                    // i 초기화
Loop:
  PushContext r1           // 반복마다 새 Context
  StaContextSlot r1, [0]   // i 값 복사
  // 루프 본문 실행 (이 Context 캡처 가능)
  PopContext               // Context 종료
  Inc                      // i++
  JumpIfTrue Loop

각 반복의 Context는 클로저가 캡처하면 힙에 유지되고, 그렇지 않으면 즉시 폐기됩니다.

메모리 프로파일링 클로저 메모리 사용량 (V8 기준):

  • Context 객체: 기본 32 bytes + (변수 수 × 8 bytes)
  • Function 객체: 약 56-80 bytes (코드 공유, 인라인 캐시 등)
  • for-let 루프 클로저: (반복 횟수 × Context 크기) - 10,000 반복 시 약 320KB

가비지 컬렉션 전략

  • 클로저 참조가 제거되면 Scavenger(Young Generation GC)가 Context를 수집
  • Context 체인이 끊기면 연결된 모든 Context가 수집 대상 (Mark-Sweep)
  • WeakMap/WeakSet을 사용하여 클로저 참조를 약한 참조로 관리 가능