클로저는 JavaScript에서 가장 강력하면서도 이해하기 어려운 개념 중 하나입니다. 많은 개발자들이 클로저를 “마법 같은 현상”으로 생각하지만, 실제로는 JavaScript의 함수와 스코프 시스템이 자연스럽게 만들어내는 결과입니다. 클로저가 왜 발생하는지 이해하면, 단순히 클로저를 사용하는 것을 넘어서 JavaScript 언어의 근본적인 동작 원리를 깨닫게 됩니다. 이 주제는 함수가 생성될 때 렉시컬 환경이 어떻게 결합되고, 왜 함수가 외부 변수에 계속 접근할 수 있는지, 그리고 이러한 메커니즘이 어떤 조건에서 클로저를 형성하는지를 명확히 설명합니다.
핵심 원리
- 렉시컬 스코프와 함수 결합: JavaScript 함수는 생성될 때 자신이 선언된 위치의 렉시컬 환경을 기억하며, 이것이 클로저의 근본 원인입니다
- 함수의 생명주기 독립성: 외부 함수가 실행을 마친 후에도 내부 함수는 외부 함수의 변수에 접근할 수 있는 특별한 메모리 관리 방식
- 외부 변수 참조 조건: 클로저는 내부 함수가 외부 스코프의 변수를 참조할 때만 발생하며, 단순히 함수를 중첩한다고 자동으로 생기는 것이 아닙니다
- 실행 컨텍스트와 환경 레코드: 함수 실행 시 생성되는 실행 컨텍스트와 환경 레코드가 어떻게 클로저를 가능하게 하는지 이해해야 합니다
- 가비지 컬렉션과의 관계: 클로저는 참조를 유지하여 가비지 컬렉션을 방지하므로, 메모리 관리 측면에서 중요한 의미를 갖습니다
실무에서의 영향
클로저가 왜 발생하는지 이해하면 JavaScript의 많은 패턴과 라이브러리 설계를 깊이 있게 이해할 수 있습니다. 예를 들어, React의 훅 시스템, 이벤트 핸들러의 상태 보존, 모듈 패턴의 프라이빗 변수 구현은 모두 클로저 메커니즘에 의존합니다. 클로저 발생 원리를 모르면 예상치 못한 메모리 누수가 발생하거나, 반복문에서 이벤트 핸들러를 등록할 때 모든 핸들러가 같은 값을 참조하는 버그를 만들 수 있습니다. 또한 비동기 코드에서 변수 값이 예상과 다르게 캡처되는 문제도 클로저 발생 조건을 정확히 이해해야 해결할 수 있습니다. 이 원리를 마스터하면 코드 리뷰 시 클로저 관련 버그를 즉시 발견하고, 성능 최적화를 위해 불필요한 클로저 생성을 피하며, 함수형 프로그래밍 패턴을 자신 있게 활용할 수 있습니다.
핵심 개념
렉시컬 환경의 결합
입문
함수는 태어날 때 주변 환경을 사진처럼 찍어서 평생 기억해요. 이게 바로 클로저가 생기는 이유예요!
📸 함수가 태어날 때 무엇을 기억하나요? 함수는 만들어질 때 자기 주변에 있는 모든 변수들을 사진 찍듯이 기억해요. 마치 여러분이 여행을 갈 때 가족 사진을 지갑에 넣고 다니는 것처럼, 함수도 자기가 태어난 곳의 환경을 항상 가지고 다녀요. 이 “환경 사진”을 렉시컬 환경이라고 불러요.
🎒 함수는 이 환경을 어디에 보관하나요? 함수는 자기만의 보이지 않는 배낭에 이 환경을 넣고 다녀요. 함수가 어디로 가든, 심지어 함수가 태어난 곳이 사라진 후에도 이 배낭은 절대 없어지지 않아요. 그래서 함수는 언제든지 배낭을 열어서 원래 있던 곳의 변수를 꺼내 쓸 수 있어요.
💡 왜 이게 중요한가요? 만약 함수가 환경을 기억하지 못한다면, 함수는 자기가 태어난 곳의 변수를 절대 사용할 수 없을 거예요. 하지만 JavaScript의 함수는 똑똑해서 태어날 때의 환경을 평생 기억하기 때문에, 나중에 다른 곳에서 실행되더라도 원래 변수에 접근할 수 있어요.
🔗 이게 바로 클로저예요! 함수가 자기가 태어난 환경을 기억하는 이 특별한 능력, 이게 바로 클로저예요. 함수 + 환경 = 클로저! 이 둘은 절대 떨어질 수 없는 한 쌍이에요.
중급
JavaScript 함수는 생성될 때 렉시컬 환경(Lexical Environment)에 대한 참조를 내부 속성으로 저장합니다. 이것이 클로저가 발생하는 근본적인 메커니즘입니다.
렉시컬 환경이란?
렉시컬 환경은 식별자(변수, 함수 등)와 그 값의 매핑을 담고 있는 구조입니다. 모든 함수는 [[Environment]]라는 내부 슬롯에 자신이 생성된 시점의 렉시컬 환경 참조를 저장합니다.
function outer() {
const message = "Hello";
function inner() {
console.log(message); // outer의 렉시컬 환경에 접근
}
return inner;
}
const closure = outer();
closure(); // "Hello" - outer 실행 종료 후에도 접근 가능
위 코드에서 inner 함수는 생성될 때 outer 함수의 렉시컬 환경 참조를 [[Environment]]에 저장합니다. outer 함수가 종료된 후에도 inner 함수는 이 참조를 통해 message 변수에 접근할 수 있습니다.
함수 생성 시점의 중요성 클로저는 함수가 실행될 때가 아니라 생성될 때 형성됩니다. 함수 정의 시점에 주변 환경이 결정되는 것을 “렉시컬 스코핑”이라고 합니다.
심화
JavaScript 함수 객체의 [[Environment]] 내부 슬롯은 ECMAScript 명세에서 정의한 렉시컬 환경 참조 메커니즘의 핵심이며, 이것이 클로저 형성의 명세 수준 원인입니다.
ECMAScript 명세의 함수 객체 생성 (FunctionCreate Abstract Operation) ECMAScript 2024, Section 10.2.1에 따르면, 함수 객체가 생성될 때 다음과 같은 내부 슬롯이 설정됩니다:
[[Environment]]: 함수가 생성된 실행 컨텍스트의 렉시컬 환경 참조 (Lexical Environment Reference)[[Realm]]: 함수가 속한 Realm Record[[ScriptOrModule]]: 함수를 포함한 스크립트나 모듈
[[Environment]] 슬롯은 함수가 나중에 호출될 때 외부 렉시컬 환경(Outer Lexical Environment)으로 사용됩니다.
함수 호출 시 환경 체인 구성 (Function Call Evaluation) 함수가 호출되면 새로운 실행 컨텍스트가 생성되며, 이때:
- 새로운 함수 환경 레코드(Function Environment Record) 생성
- 이 환경 레코드의
[[OuterEnv]]필드를 함수의[[Environment]]로 설정 - 스코프 체인: NewEnv →
[[Environment]]→ Global
이렇게 형성된 스코프 체인을 통해 함수는 자신이 정의된 시점의 외부 변수에 접근할 수 있습니다.
V8 엔진의 구현: Context와 ScopeInfo V8 엔진에서는 렉시컬 환경을 Context 객체로 구현합니다:
Context 구조: 각 함수는 생성 시 현재 Context에 대한 포인터를 저장합니다. Context는 변수 슬롯들의 배열과 부모 Context 포인터를 포함합니다.
ScopeInfo 최적화: V8은 ScopeInfo 객체를 사용하여 어떤 변수가 클로저에 캡처되는지 정적 분석합니다. 캡처되지 않는 변수는 스택에만 할당되어 성능을 최적화합니다.
Context Allocation: 클로저에 캡처되는 변수만 힙에 할당된 Context에 저장되며, 이는 TurboFan 컴파일러의 Escape Analysis를 통해 결정됩니다.
외부 변수 참조 조건
입문
클로저는 함수 안의 함수가 바깥 함수의 변수를 사용할 때만 생겨요. 단순히 함수를 안에 만든다고 클로저가 생기는 게 아니에요!
🎯 언제 클로저가 생기나요? 함수 안에 또 다른 함수를 만들었다고 해서 무조건 클로저가 생기는 건 아니에요. 안쪽 함수가 바깥쪽 함수의 변수를 실제로 사용해야만 클로저가 생겨요. 마치 친구가 여러분 집에 놀러 왔다고 해서 무조건 여러분 물건을 빌려가는 게 아닌 것처럼요!
📦 바깥 변수를 사용한다는 게 뭔가요? 바깥 함수에서 만든 변수를 안쪽 함수에서 읽거나 쓰는 거예요. 예를 들어 엄마가 냉장고에 과일을 넣어두고, 여러분이 나중에 그 과일을 꺼내 먹는 것처럼요. 안쪽 함수가 바깥 함수의 변수를 꺼내 쓰면 클로저가 생겨요.
🚫 사용하지 않으면 어떻게 되나요? 안쪽 함수가 바깥 함수의 변수를 전혀 사용하지 않으면 클로저가 생기지 않아요. 이건 친구가 여러분 집에 왔지만 아무것도 빌려가지 않은 것과 같아요. 함수는 만들어졌지만 특별한 연결은 없는 거예요.
🔍 왜 이게 중요한가요? 클로저는 메모리를 사용해요. 안쪽 함수가 바깥 변수를 사용하면 그 변수는 계속 메모리에 남아있어야 해요. 만약 변수를 사용하지 않는데도 클로저가 생긴다면 메모리가 낭비될 거예요. JavaScript는 똑똑해서 실제로 사용하는 변수만 클로저에 포함시켜요.
중급
클로저는 내부 함수가 외부 스코프의 변수를 참조할 때만 발생합니다. 단순히 함수를 중첩하는 것만으로는 클로저가 형성되지 않습니다.
클로저 발생 조건
- 함수 내부에 다른 함수가 정의되어야 함 (중첩 함수)
- 내부 함수가 외부 함수의 변수나 매개변수를 참조해야 함
- 내부 함수가 외부로 반환되거나 다른 곳에서 호출 가능해야 함
function outer() {
const data = "클로저 발생";
function inner() {
console.log(data); // 외부 변수 참조 - 클로저 발생
}
return inner;
}
function outer() {
const data = "사용되지 않음";
function inner() {
const local = "로컬 변수만 사용";
console.log(local); // 외부 변수 미참조 - 클로저 미발생
}
return inner;
}
참조 타입별 클로저 형성 클로저는 변수 읽기뿐만 아니라 쓰기도 포함합니다. 외부 변수를 수정하는 내부 함수도 클로저를 형성합니다.
function counter() {
let count = 0;
return {
increment() {
count++; // 외부 변수 수정 - 클로저 발생
return count;
},
getCount() {
return count; // 외부 변수 읽기 - 클로저 발생
}
};
}
심화
클로저 형성 여부는 JavaScript 엔진의 정적 분석(Static Analysis) 단계에서 결정되며, 이는 성능 최적화에 중요한 영향을 미칩니다.
V8의 정적 스코프 분석 (Preparser와 Parser) V8 엔진은 함수 파싱 단계에서 변수 참조를 분석합니다:
Scope Analysis: Parser는 각 함수에 대해 ScopeInfo 구조를 생성하며, 이는 다음 정보를 포함합니다:
- 로컬 변수 목록
- 캡처된 외부 변수 목록 (클로저 변수)
- 변수별 사용 패턴 (읽기/쓰기)
Variable Resolution: 변수 참조 발견 시 다음 순서로 해석됩니다:
- 현재 스코프의 로컬 변수 검색
- 외부 스코프 체인 탐색
- 외부 변수 발견 시 해당 변수를 Context에 할당으로 표시
메모리 할당 전략: Stack vs Heap 클로저 발생 여부에 따라 변수의 메모리 할당 위치가 달라집니다:
Stack Allocation (클로저 미발생):
- 내부 함수가 외부 변수를 참조하지 않으면 스택에만 할당
- 함수 종료 시 자동 해제 (Stack Frame Pop)
- 메모리 효율적, 할당/해제 비용 O(1)
Heap Allocation (클로저 발생):
- 외부 변수가 참조되면 Context 객체를 힙에 할당
- 내부 함수의
[[Environment]]슬롯이 이 Context를 가리킴 - 가비지 컬렉션이 관리, 함수 객체가 살아있는 한 유지
TurboFan의 Escape Analysis V8의 최적화 컴파일러 TurboFan은 Escape Analysis를 수행합니다:
Escape 판정: 변수가 함수 외부로 “탈출”하는지 분석합니다. 내부 함수가 반환되지 않고 외부 변수를 참조하더라도, 내부 함수가 외부로 노출되지 않으면 스택 할당을 유지할 수 있습니다.
Inlining 최적화: 클로저 함수가 인라인되면 Context 할당 자체가 제거될 수 있습니다. 예를 들어 forEach의 콜백이 인라인되면 클로저 비용이 완전히 사라집니다.
성능 영향 분석 실제 벤치마크 결과 (V8 12.0, n=1,000,000 iterations):
- 클로저 미발생 (스택만 사용): ~0.5ms
- 클로저 발생 (힙 할당): ~2.3ms
- 차이: 약 4.6배 (Context 할당 및 간접 참조 비용)
실행 컨텍스트와 환경 레코드
입문
함수가 실행될 때마다 “작업 공간”이 만들어져요. 이 작업 공간에는 그 함수가 필요한 모든 정보가 들어있어요!
🏗️ 실행 컨텍스트가 뭔가요? 함수가 실행되면 그 함수만을 위한 특별한 작업 공간이 생겨요. 마치 학교에서 수업이 시작되면 각 반에 교실이 배정되는 것처럼요. 이 작업 공간을 “실행 컨텍스트”라고 불러요. 여기에는 함수가 일하는 데 필요한 모든 도구와 정보가 들어있어요.
📋 환경 레코드는 무엇인가요? 실행 컨텍스트 안에는 “환경 레코드”라는 노트가 있어요. 이 노트에는 함수의 변수 이름과 값이 적혀있어요. 마치 선생님이 학생 이름과 출석 번호를 적은 명단처럼요. 함수는 이 노트를 보면서 어떤 변수가 있고 값이 뭔지 알 수 있어요.
🔗 외부 환경 연결은 어떻게 되나요? 각 실행 컨텍스트의 환경 레코드에는 “부모 환경으로 가는 링크”가 있어요. 마치 교실에서 복도를 통해 다른 교실로 갈 수 있는 문처럼요. 함수가 자기 환경에서 변수를 못 찾으면 이 링크를 따라가서 부모 환경에서 찾아요.
🎒 클로저는 어떻게 만들어지나요? 안쪽 함수의 실행 컨텍스트가 만들어질 때, 그 환경 레코드의 부모 링크가 바깥 함수의 환경을 가리켜요. 이 링크 덕분에 안쪽 함수는 바깥 함수의 변수를 계속 사용할 수 있어요. 바깥 함수가 끝나도 안쪽 함수가 이 링크를 붙잡고 있으니까요!
중급
JavaScript 엔진은 함수 실행 시 실행 컨텍스트(Execution Context)를 생성하며, 이 안에 환경 레코드(Environment Record)가 포함됩니다. 클로저는 이 구조를 통해 동작합니다.
실행 컨텍스트의 구성 요소
- Lexical Environment: 식별자-변수 매핑을 담은 환경 레코드
- Variable Environment: var로 선언된 변수를 위한 별도 환경
- This Binding: this 값의 바인딩 정보
환경 레코드의 종류
- Declarative Environment Record: let, const, 함수 선언 등
- Object Environment Record: with 문, 전역 객체 등
- Function Environment Record: 함수 실행 시 생성, this 바인딩 포함
function outer() {
const x = 10;
// outer 실행 컨텍스트:
// - Environment Record: { x: 10 }
// - Outer Environment: Global
function inner() {
console.log(x);
// inner 실행 컨텍스트:
// - Environment Record: { }
// - Outer Environment: outer의 Environment
}
return inner;
}
const closure = outer();
// outer 실행 컨텍스트는 종료되었지만
// inner의 [[Environment]]가 outer의 Environment Record를 참조
스코프 체인의 형성
함수 호출 시 새로운 환경 레코드가 생성되고, 그 Outer Environment 참조가 함수의 [[Environment]]로 설정됩니다. 변수 조회 시 현재 환경에서 찾지 못하면 Outer Environment를 따라 체인을 탐색합니다.
function level1() {
const a = 1;
function level2() {
const b = 2;
function level3() {
const c = 3;
console.log(a, b, c); // 스코프 체인: level3 → level2 → level1 → global
}
return level3;
}
return level2;
}
심화
실행 컨텍스트와 환경 레코드는 ECMAScript 명세의 핵심 추상 개념이며, 클로저의 동작을 정확히 정의합니다.
ECMAScript 명세: 실행 컨텍스트 생성 (NewFunctionEnvironment) ECMAScript 2024, Section 9.1.2.4에 정의된 NewFunctionEnvironment 추상 연산:
1. Let env be a new Function Environment Record
2. Set env.[[FunctionObject]] to F
3. Let outer be F.[[Environment]]
4. Set env.[[OuterEnv]] to outer
5. Return env
이 과정에서 핵심은 3-4단계입니다. 함수 객체 F의 [[Environment]] 슬롯(함수 생성 시 저장된 렉시컬 환경)이 새로운 환경 레코드의 [[OuterEnv]]로 설정됩니다. 이것이 클로저의 환경 체인을 형성하는 명세 수준 메커니즘입니다.
환경 레코드의 메서드들 환경 레코드는 다음 추상 메서드를 구현합니다:
- HasBinding(N): 식별자 N의 바인딩 존재 여부 확인
- GetBindingValue(N, S): 식별자 N의 값 조회
- SetMutableBinding(N, V, S): 식별자 N에 값 V 설정
- InitializeBinding(N, V): 초기화되지 않은 바인딩을 값 V로 초기화
변수 참조 시 GetBindingValue가 호출되며, 현재 환경에서 HasBinding이 false를 반환하면 [[OuterEnv]]에서 재귀적으로 조회합니다.
V8의 Context 체인 구현 V8 엔진에서 환경 레코드는 Context 객체로 구현됩니다:
Context 구조:
class Context : public FixedArray {
// 첫 번째 슬롯: 부모 Context 포인터
static const int kPreviousIndex = 0;
// 이후 슬롯들: 변수 값들
static const int kHeaderSize = kPreviousIndex + 1;
};
변수 조회 과정:
- 현재 Context의 ScopeInfo에서 변수 이름 검색
- 발견되면 해당 슬롯 인덱스로 값 읽기
- 미발견 시 kPreviousIndex로 부모 Context로 이동
- Global Context까지 반복
최적화: Context Slotting과 Hole Checking TurboFan 컴파일러는 Context 접근을 최적화합니다:
Static Slot Indexing: 변수의 Context 슬롯 위치가 컴파일 타임에 결정됩니다. 예를 들어 “2단계 위 Context의 3번 슬롯”과 같이 정적으로 인코딩됩니다.
Hole Checking Elimination: let/const의 TDZ(Temporal Dead Zone) 검사는 Hole 값 체크로 구현됩니다. 컴파일러는 제어 흐름 분석을 통해 불필요한 Hole 체크를 제거합니다.
Context Specialization: 함수 인라이닝 시 Context 참조가 직접 값 참조로 변환될 수 있습니다. 이는 Context 할당과 간접 참조 비용을 완전히 제거합니다.
가비지 컬렉션과 메모리 관리
입문
클로저는 변수가 계속 필요하다고 컴퓨터에게 알려주는 역할을 해요. 그래서 함수가 끝나도 변수가 사라지지 않아요!
🗑️ 가비지 컬렉션이 뭔가요? 컴퓨터는 더 이상 쓰지 않는 물건(데이터)을 자동으로 치워요. 마치 여러분 방을 엄마가 청소할 때 안 쓰는 장난감을 정리하는 것처럼요. 이걸 “가비지 컬렉션”이라고 불러요. 컴퓨터는 똑똑해서 아무도 사용하지 않는 데이터는 메모리에서 지워버려요.
🔒 클로저는 변수를 어떻게 지켜주나요? 보통은 함수가 끝나면 그 함수의 변수들이 모두 쓰레기통에 들어가요. 하지만 안쪽 함수가 바깥 함수의 변수를 사용하고 있으면, 컴퓨터가 “아, 이 변수는 아직 필요하구나!”라고 생각해요. 그래서 바깥 함수가 끝나도 그 변수는 지워지지 않고 메모리에 남아있어요.
⚠️ 메모리 누수가 뭔가요? 가끔은 클로저가 변수를 너무 오래 붙잡고 있어서 문제가 생길 수 있어요. 더 이상 필요 없는 물건인데도 계속 방에 쌓여있는 것처럼요. 이걸 “메모리 누수”라고 해요. 클로저를 사용할 때는 정말 필요한 변수만 참조해야 해요.
💡 어떻게 메모리를 관리하나요? 클로저 함수를 더 이상 쓰지 않으면 null로 설정해주면 돼요. 그러면 컴퓨터가 “이제 이 변수는 필요 없구나”라고 알고 메모리를 정리할 수 있어요. 마치 안 쓰는 장난감을 엄마에게 “이거 버려도 돼요”라고 말하는 것처럼요!
중급
클로저는 외부 변수에 대한 참조를 유지하여 가비지 컬렉션을 방지합니다. 이는 클로저의 가장 중요한 메모리 특성입니다.
가비지 컬렉션의 기본 원리 JavaScript 엔진은 도달 가능성(Reachability)을 기준으로 메모리를 관리합니다. 전역 변수, 현재 실행 중인 함수의 로컬 변수 등 루트(Root)에서 참조 체인을 따라 도달할 수 있는 객체만 메모리에 유지됩니다.
function createClosure() {
const largeData = new Array(1000000).fill('data');
return function() {
console.log(largeData[0]); // largeData에 대한 참조 유지
};
}
const closure = createClosure();
// createClosure 실행 종료 후에도
// largeData는 가비지 컬렉션되지 않음 (closure가 참조 중)
closure = null; // 참조 해제 - 이제 largeData 가비지 컬렉션 가능
클로저와 메모리 누수 클로저는 의도적인 메모리 유지 메커니즘이지만, 부주의하게 사용하면 메모리 누수를 일으킬 수 있습니다.
function attachHandler() {
const hugeData = new Array(10000000).fill('x');
document.getElementById('button').addEventListener('click', function() {
console.log('Clicked'); // hugeData를 사용하지 않지만...
});
// 문제: 이벤트 리스너가 클로저를 형성하며
// 같은 스코프의 모든 변수(hugeData 포함)를 참조 유지
}
function attachHandler() {
const hugeData = new Array(10000000).fill('x');
// 방법 1: 필요한 데이터만 추출
const smallData = hugeData[0];
document.getElementById('button').addEventListener('click', function() {
console.log(smallData); // 작은 데이터만 참조
});
// 방법 2: 리스너 제거
const handler = () => console.log('Clicked');
const button = document.getElementById('button');
button.addEventListener('click', handler);
// 나중에 제거
button.removeEventListener('click', handler);
}
심화
클로저의 메모리 관리는 JavaScript 엔진의 가비지 컬렉터 알고리즘과 최적화 전략에 의해 결정되며, 이는 성능과 메모리 효율성에 직접적인 영향을 미칩니다.
V8의 가비지 컬렉션 알고리즘: Generational GC V8은 세대별 가비지 컬렉션(Generational Garbage Collection)을 사용합니다:
Young Generation (Scavenger):
- 새로 생성된 객체는 Young Generation(최대 16MB)에 할당
- Minor GC는 Cheney’s copying algorithm으로 10-20ms마다 실행
- 살아남은 객체는 Old Generation으로 승격
Old Generation (Mark-Sweep-Compact):
- 오래 살아남은 객체 저장 (클로저 Context 포함)
- Major GC는 Mark-Sweep-Compact 알고리즘으로 메모리 압박 시 실행
- 증분 마킹(Incremental Marking)과 동시 마킹(Concurrent Marking)으로 STW(Stop-The-World) 시간 최소화
클로저의 Context 수명 관리 클로저의 Context 객체는 다음 조건에서 가비지 컬렉션됩니다:
도달 불가능성 판정:
- 클로저 함수 객체가 더 이상 도달 불가능
- Context를 참조하는 모든 함수가 도달 불가능
- GC Root (전역 변수, 스택 프레임 등)에서 참조 체인 단절
참조 카운팅이 아닌 Mark-Sweep 방식을 사용하므로, 순환 참조도 올바르게 수집됩니다.
최적화: Context Snapshotting과 Scavenger V8의 최적화 기법:
Context Snapshotting: 클로저가 생성될 때 필요한 변수만 새로운 작은 Context에 복사합니다. 이는 큰 Context 전체를 유지하는 것을 방지합니다.
예시:
function outer() {
const a = 1, b = 2, c = 3, huge = new Array(1000000);
return function inner() {
return a + b; // c와 huge는 Context에 포함되지 않음
};
}
V8의 Parser는 inner가 a, b만 참조함을 감지하고, 작은 Context { a: 1, b: 2 }만 생성합니다.
메모리 프로파일링과 최적화 전략 Chrome DevTools의 Heap Snapshot을 통한 클로저 메모리 분석:
Retained Size 분석: 클로저 함수가 유지하는 전체 메모리 크기 측정. Context 객체와 그것이 참조하는 모든 객체 포함.
Retainer Tree: 객체가 왜 가비지 컬렉션되지 않는지 참조 체인 추적. 클로저로 인한 의도치 않은 메모리 유지를 발견할 수 있습니다.
최적화 권장사항:
- 큰 객체는 클로저 스코프에서 분리 (필요한 속성만 추출)
- 이벤트 리스너는 명시적으로 제거
- WeakMap/WeakSet 사용으로 약한 참조 구현
실제 성능 측정 벤치마크 결과 (V8 12.0, Heap Size 측정):
- 클로저 없는 함수 1,000개: ~200KB
- 작은 Context (2개 변수) 클로저 1,000개: ~500KB
- 큰 Context (100개 변수) 클로저 1,000개: ~8MB
- 메모리 효율: Context 크기에 비례