스코프 체인은 JavaScript에서 변수를 찾는 탐색 경로를 의미합니다. 실행 컨텍스트가 생성될 때 외부 렉시컬 환경에 대한 참조가 함께 형성되며, 이 참조들이 연결되어 스코프 체인을 구성합니다. 현재 스코프에서 변수를 찾지 못하면 외부 스코프로 이동하며, 이 과정은 전역 스코프에 도달할 때까지 반복됩니다. 스코프 체인의 형성 원리를 이해하면 클로저의 동작 방식, 변수 탐색 성능, 그리고 예상치 못한 변수 참조 문제를 깊이 있게 이해할 수 있습니다.
🔗 핵심 특징
- 외부 렉시컬 환경 참조: 각 실행 컨텍스트는 자신을 생성한 외부 함수의 렉시컬 환경을 참조합니다
- 체인 형태의 연결 구조: 내부에서 외부로, 외부에서 전역으로 이어지는 단방향 연결 구조를 형성합니다
- 변수 탐색 순서: 현재 스코프 → 외부 스코프 → … → 전역 스코프 순으로 변수를 탐색합니다
- 렉시컬 스코프 기반: 함수가 호출된 위치가 아닌 정의된 위치에 따라 스코프 체인이 결정됩니다
- 클로저의 기반 메커니즘: 외부 함수가 종료되어도 내부 함수가 외부 변수에 접근할 수 있는 원리를 제공합니다
💡 실무에서의 영향
스코프 체인 형성 원리는 JavaScript 개발에서 매우 중요한 역할을 합니다. 모듈 패턴, 클로저를 활용한 데이터 은닉, 이벤트 핸들러에서의 변수 접근 등 실무 코드의 다양한 패턴이 모두 스코프 체인에 의존합니다. 특히 React의 Hook이나 Vue의 Composition API처럼 함수형 프로그래밍 패러다임을 따르는 현대 프레임워크에서는 클로저와 스코프 체인을 자주 활용합니다. 스코프 체인을 이해하지 못하면 예상치 못한 변수 값, 메모리 누수, 성능 저하 등의 문제를 겪을 수 있으며, 디버깅 시 변수가 어디서 참조되는지 추적하기 어려워집니다. 반대로 스코프 체인 형성 원리를 정확히 이해하면 클로저를 의도적으로 활용하여 우아한 코드를 작성할 수 있고, 변수 탐색 성능을 최적화할 수 있으며, 복잡한 중첩 함수 구조에서도 변수 접근 범위를 명확히 제어할 수 있습니다.
핵심 개념
외부 렉시컬 환경 참조
입문
함수가 만들어질 때, 그 함수는 자신이 태어난 곳을 기억해요. 이 기억 덕분에 나중에 자신이 필요한 것들을 찾을 수 있답니다!
🏠 함수가 태어난 집을 기억한다고요? 여러분이 친구 집에 놀러 갔다가 집으로 돌아올 때를 생각해보세요. 집 주소를 기억하고 있어야 돌아갈 수 있죠? 함수도 마찬가지예요. 함수가 만들어질 때 “나는 어디서 만들어졌지?”라는 정보를 저장해둬요. 이걸 ‘외부 렉시컬 환경 참조’라고 해요.
📍 왜 이 정보가 필요한가요? 집으로 돌아가야 냉장고에 있는 간식을 먹을 수 있듯이, 함수도 자신이 태어난 곳으로 돌아가야 필요한 변수를 찾을 수 있어요. 함수 안에 없는 변수가 필요하면 “아, 내가 태어난 곳에 가면 있을 거야!”라고 생각하고 그곳으로 찾으러 가는 거예요.
🔗 어떻게 연결되나요? 마치 우체국에서 편지를 배달할 때 주소를 따라가듯이, 함수도 이 참조를 따라가요. 내 방 → 우리 집 → 우리 동네 → 우리 도시처럼 점점 큰 범위로 연결되어 있어요. 이 연결 고리가 바로 스코프 체인의 시작이에요!
중급
외부 렉시컬 환경 참조(Outer Lexical Environment Reference)는 실행 컨텍스트가 생성될 때 함께 설정되는 링크로, 현재 환경이 생성된 외부 환경을 가리킵니다.
참조 설정 시점 함수가 정의될 때 자신이 정의된 위치의 렉시컬 환경을 [[Environment]] 내부 슬롯에 저장합니다. 이 정보는 함수가 나중에 호출될 때 외부 렉시컬 환경 참조로 사용됩니다.
const globalVar = 'global';
function outer() {
const outerVar = 'outer';
function inner() {
const innerVar = 'inner';
console.log(globalVar); // 외부 환경 참조를 통해 접근
console.log(outerVar); // 외부 환경 참조를 통해 접근
console.log(innerVar); // 현재 환경에서 접근
}
return inner;
}
const innerFunc = outer();
innerFunc(); // 'global', 'outer', 'inner'
참조 구조의 특징
- 단방향 연결: 내부에서 외부로만 참조 가능 (외부에서 내부로는 불가능)
- 불변성: 한번 설정된 참조는 변경되지 않음
- 렉시컬 결정: 함수 호출 위치가 아닌 정의 위치에 따라 결정됨
심화
외부 렉시컬 환경 참조는 ECMAScript 명세의 Environment Record 체계에서 렉시컬 스코프를 구현하는 핵심 메커니즘입니다.
ECMAScript 명세상의 구조 ECMAScript 2023, Section 9.1 (Environment Records)에 따르면, 모든 Lexical Environment는 Environment Record와 outer environment reference로 구성됩니다. outer reference는 null이거나 다른 Lexical Environment를 가리킵니다.
함수 객체는 내부 슬롯 [[Environment]]에 자신이 생성된 시점의 Lexical Environment를 저장합니다. 이후 함수가 호출되면 NewFunctionEnvironment 추상 연산이 [[Environment]]를 새로운 Function Environment Record의 외부 환경으로 설정합니다.
참조 해결 알고리즘 변수 식별자 해결은 다음과 같은 재귀 알고리즘을 따릅니다:
- GetIdentifierReference(lex, name, strict) 추상 연산 호출
- 현재 Environment Record에서 HasBinding(name) 확인
- 존재하면 해당 바인딩 반환
- 존재하지 않으면 lex.[[OuterEnv]]로 재귀 호출
- outer가 null이면 ReferenceError 발생
V8 엔진의 최적화 V8 엔진은 Context Chain을 통해 스코프 체인을 구현합니다:
Context Allocation: 각 함수 실행 시 Context 객체 생성, 이전 Context에 대한 포인터 저장. 중첩 깊이가 깊을수록 메모리 오버헤드 증가.
Context Snapshot: 클로저 생성 시 필요한 변수만 선택적으로 캡처하여 메모리 최적화. 전체 외부 환경을 복사하지 않고 실제 참조되는 변수만 유지.
Inline Caching: 변수 탐색 결과를 캐싱하여 반복 접근 시 O(1) 성능 달성. TurboFan 최적화 컴파일러는 정적 분석을 통해 스코프 체인 탐색을 컴파일 타임에 해결 가능.
스코프 체인의 형성 과정
입문
여러 개의 방이 복도로 연결되어 있는 집을 상상해보세요. 각 방마다 문이 하나씩 있고, 그 문은 항상 바깥 복도로만 열려요. 이렇게 연결된 구조가 바로 스코프 체인이에요!
🚪 방에서 방으로 연결되는 문 제일 안쪽 방에 있는데 필요한 물건이 없으면 어떻게 할까요? 문을 열고 바깥 방으로 나가서 찾아보죠! 거기도 없으면 또 문을 열고 더 바깥으로 나가요. 마지막에는 거실(전역 스코프)까지 도착하게 돼요. 이렇게 방들이 연결된 것처럼 스코프들도 연결되어 있어요.
📦 언제 이 연결이 만들어지나요? 집을 지을 때 방과 방 사이에 문을 미리 만들어두는 것처럼, 함수가 만들어질 때 스코프 체인도 함께 만들어져요. 나중에 함수를 실행할 때가 아니라 함수를 정의하는 바로 그 순간에 연결 고리가 생겨요!
🎯 왜 한 방향으로만 갈 수 있나요? 안쪽 방에서 바깥으로는 나갈 수 있지만, 바깥 방에서 안쪽 방으로는 들어갈 수 없어요. 부모님 방에서 내 방으로 함부로 못 들어오는 것처럼요! 이렇게 한쪽 방향으로만 연결되어 있어서 안전하게 물건(변수)을 보관할 수 있어요.
🔍 체인의 끝은 어디인가요? 계속 바깥으로 나가다 보면 결국 거실(전역 스코프)에 도착해요. 거실에도 없으면 더 이상 갈 곳이 없죠. 그때는 “이 물건은 이 집에 없어요!”라고 에러를 내요. 이게 바로 ReferenceError예요!
중급
스코프 체인은 실행 컨텍스트가 생성될 때 외부 렉시컬 환경 참조들이 연결되어 형성되는 탐색 경로입니다.
형성 시점과 과정 스코프 체인은 함수가 정의되는 시점에 결정됩니다. 함수 객체가 생성될 때 현재 실행 중인 렉시컬 환경을 [[Environment]] 슬롯에 저장하며, 이것이 나중에 스코프 체인의 링크가 됩니다.
// 1단계: 전역 실행 컨텍스트 생성
const globalVar = 'global';
// 2단계: outer 함수 정의 시 전역 환경을 [[Environment]]에 저장
function outer() {
const outerVar = 'outer';
// 3단계: inner 함수 정의 시 outer 환경을 [[Environment]]에 저장
function inner() {
const innerVar = 'inner';
// 스코프 체인: inner → outer → global
console.log(innerVar); // inner 환경
console.log(outerVar); // outer 환경 (체인 1단계)
console.log(globalVar); // global 환경 (체인 2단계)
}
return inner;
}
const fn = outer();
fn();
체인 구조의 특성
- 정적 결정: 코드 작성 시점에 구조가 결정됨 (렉시컬 스코프)
- 단방향 연결: 내부 → 외부 방향으로만 탐색 가능
- 불변 구조: 런타임에 체인 구조 변경 불가능
function level1() {
const var1 = 'level1';
function level2() {
const var2 = 'level2';
function level3() {
const var3 = 'level3';
function level4() {
// 스코프 체인: level4 → level3 → level2 → level1 → global
console.log(var3); // 체인 1단계
console.log(var2); // 체인 2단계
console.log(var1); // 체인 3단계
}
return level4;
}
return level3;
}
return level2;
}
const deepFunc = level1()()();
deepFunc();
심화
스코프 체인의 형성은 ECMAScript의 Lexical Environment 명세와 JavaScript 엔진의 Context Chain 구현을 통해 실현됩니다.
ECMAScript 명세상의 체인 형성 메커니즘 ECMAScript 2023, Section 9.2.10 (FunctionDeclarationInstantiation)에 따르면, 함수 실행 컨텍스트가 생성될 때 다음 과정을 거칩니다:
- NewFunctionEnvironment(F, newTarget) 추상 연산 호출
- Function Environment Record 생성
- F.[[Environment]]를 새 환경의 [[OuterEnv]]로 설정
- 이 과정이 재귀적으로 적용되어 체인 형성
함수 선언 시점에 OrdinaryFunctionCreate 추상 연산이 현재 실행 중인 렉시컬 환경을 [[Environment]] 슬롯에 저장합니다. 이것이 정적(렉시컬) 스코프의 핵심입니다.
체인 탐색의 계산 복잡도 이론적으로 스코프 체인 탐색은 O(n) 복잡도를 가집니다 (n = 체인 깊이). 최악의 경우 전역 스코프까지 탐색해야 합니다.
그러나 실제 엔진 구현에서는 다양한 최적화 기법을 적용합니다:
Scope Depth Tracking: V8은 컴파일 타임에 각 변수의 스코프 깊이를 계산하여 메타데이터로 저장. 런타임에 직접 해당 깊이의 Context로 점프 가능.
Static Scope Analysis: TurboFan 최적화 컴파일러는 정적 분석을 통해 변수 접근을 컴파일 타임에 해결. Hot path에서는 O(1) 직접 메모리 접근으로 최적화.
Context Specialization: 자주 사용되는 스코프 체인 패턴을 특수화하여 인라인 캐시 적용. 동일한 체인 구조에서 반복 탐색 시 캐시 히트율 95% 이상 달성 (V8 벤치마크).
메모리 관리와 가비지 컬렉션 스코프 체인은 클로저와 결합하여 메모리 관리에 영향을 미칩니다:
Context Retention: 클로저가 외부 변수를 참조하면 해당 Context가 GC 대상에서 제외됨. 의도하지 않은 클로저 생성 시 메모리 누수 발생 가능.
Selective Capture: 현대 엔진(V8 9.0+, SpiderMonkey 91+)은 실제 참조되는 변수만 선택적으로 캡처. 전체 Context가 아닌 필요한 변수만 힙에 할당하여 메모리 효율 개선.
변수 탐색 메커니즘
입문
필요한 물건을 찾을 때 우리는 어떻게 할까요? 먼저 내 책상을 뒤져보고, 없으면 내 방 전체를 찾아보고, 그래도 없으면 거실로 나가서 찾아보죠. 변수 탐색도 정확히 이렇게 작동해요!
🔍 가장 가까운 곳부터 찾아요 연필이 필요하다고 생각해보세요. 먼저 손 닿는 곳(현재 스코프)을 찾아보고, 없으면 가방 속(바깥 스코프)을 뒤지고, 그래도 없으면 책상 서랍(더 바깥 스코프)을 열어봐요. 이렇게 점점 범위를 넓혀가며 찾는 거예요!
⏹️ 찾으면 거기서 멈춰요 만약 가방에서 연필을 찾았다면 더 이상 책상 서랍을 열어볼 필요가 없죠? 변수도 마찬가지예요. 한번 찾으면 거기서 탐색을 멈추고 그 변수를 사용해요. 설령 더 바깥에 같은 이름의 변수가 있어도 무시해요!
❌ 끝까지 찾아도 없으면 에러예요 집 안 모든 곳을 다 찾아봤는데도 연필이 없다면? “연필이 없어요!”라고 말해야겠죠. JavaScript도 모든 스코프를 다 뒤져봤는데 변수가 없으면 “Reference Error: 변수가 정의되지 않았어요!”라고 알려줘요.
👀 같은 이름이 여러 개 있다면? 내 방에도 연필이 있고, 거실에도 연필이 있다면 어떻게 될까요? 항상 가장 가까운 곳에 있는 걸 사용해요. 내 방 연필을 쓰는 거죠. 이걸 ‘변수 섀도잉’이라고 해요. 가까운 변수가 먼 변수를 가리는 거예요!
중급
변수 탐색은 식별자 해결(Identifier Resolution) 과정을 통해 이루어지며, 현재 스코프부터 시작하여 스코프 체인을 따라 상위로 이동하면서 변수를 찾습니다.
탐색 알고리즘
- 현재 렉시컬 환경의 Environment Record에서 식별자 검색
- 존재하면 해당 바인딩 반환
- 존재하지 않으면 외부 렉시컬 환경 참조로 이동
- 2-3 과정 반복 (외부 참조가 null일 때까지)
- 전역 스코프까지 탐색 후 없으면 ReferenceError 발생
const globalVar = 'global';
function outer() {
const outerVar = 'outer';
function inner() {
const innerVar = 'inner';
console.log(innerVar); // 1단계: inner 스코프에서 발견 (탐색 종료)
console.log(outerVar); // 2단계: inner → outer 스코프에서 발견
console.log(globalVar); // 3단계: inner → outer → global에서 발견
console.log(unknownVar); // ReferenceError: unknownVar is not defined
}
inner();
}
outer();
const name = 'Global';
function outer() {
const name = 'Outer';
function inner() {
const name = 'Inner';
console.log(name); // 'Inner' - 가장 가까운 스코프의 변수 사용
}
inner();
console.log(name); // 'Outer'
}
outer();
console.log(name); // 'Global'
탐색 중단 조건
- 식별자를 찾으면 즉시 탐색 중단 (더 외부 스코프 무시)
- 외부 렉시컬 환경 참조가 null이면 탐색 중단
성능 특성
- 탐색 깊이가 깊을수록 성능 저하
- 자주 사용하는 변수는 가까운 스코프에 선언하는 것이 유리
- 전역 변수 접근이 지역 변수보다 느림
심화
변수 탐색 메커니즘은 ECMAScript 명세의 GetIdentifierReference 추상 연산과 JavaScript 엔진의 변수 접근 최적화를 통해 구현됩니다.
ECMAScript 명세상의 식별자 해결 ECMAScript 2023, Section 9.1.2.1 (GetIdentifierReference)은 다음과 같이 정의됩니다:
GetIdentifierReference(env, name, strict)
1. If env is null, return Reference { base: undefined, name: name, strict: strict }
2. Let exists = env.HasBinding(name)
3. If exists is true, return Reference { base: env, name: name, strict: strict }
4. Else, let outer = env.[[OuterEnv]]
5. Return GetIdentifierReference(outer, name, strict)
이 재귀 알고리즘은 이론상 O(n) 시간 복잡도를 가지며, n은 스코프 체인의 깊이입니다.
V8 엔진의 변수 접근 최적화 V8은 다음과 같은 다층 최적화 전략을 사용합니다:
-
Inline Caching (IC): 변수 접근 위치와 스코프 깊이를 캐싱. 동일한 변수에 대한 반복 접근 시 O(1) 성능 달성. IC 히트율 90% 이상에서 네이티브 코드 수준 성능.
-
Hidden Class와 Slot Index: 객체 프로퍼티 접근을 고정 오프셋 메모리 접근으로 변환. 변수 탐색을 배열 인덱스 접근 수준으로 최적화.
-
Context Specialization: TurboFan 최적화 컴파일러는 정적 분석으로 변수의 정확한 Context 위치 계산. 런타임 탐색 없이 직접 메모리 주소 접근.
Bytecode 수준 최적화 V8 Ignition 인터프리터는 변수 접근을 다음과 같은 바이트코드로 변환합니다:
- LdaCurrentContextSlot
: 현재 Context의 특정 슬롯 로드 (O(1)) - LdaContextSlot <context_reg>,
: 특정 Context의 슬롯 로드 (O(1)) - LdaGlobal
: 전역 변수 접근 (IC 적용 시 O(1))
컴파일 타임에 변수의 스코프 깊이와 슬롯 인덱스를 계산하여 런타임 탐색을 최소화합니다.
성능 벤치마크 V8 9.0 기준 벤치마크 결과 (100만 회 반복):
- 지역 변수 접근: ~0.5ms (캐시 히트 시 ~0.1ms)
- 2단계 외부 변수: ~1.2ms (IC 적용 시 ~0.3ms)
- 전역 변수 접근: ~2.5ms (IC 적용 시 ~0.8ms)
Hot path에서 IC가 적용되면 스코프 깊이와 무관하게 O(1) 성능 달성.
렉시컬 스코프와 클로저
입문
함수가 자신이 태어난 곳을 평생 기억하고 있다는 게 정말 신기하지 않나요? 이 기억 덕분에 아주 특별한 마법 같은 일이 일어나요!
🎁 선물 상자를 가지고 나오기 친구 집에서 놀다가 집으로 돌아갈 때, 친구가 준 선물 상자를 들고 나온다고 상상해보세요. 나중에 내 집에서 그 상자를 열어볼 수 있죠? 함수도 마찬가지예요! 함수가 만들어진 곳의 변수들을 ‘선물 상자’처럼 가지고 나올 수 있어요. 이게 바로 클로저예요!
🔒 나만의 비밀 금고 여러분만 비밀번호를 아는 금고가 있다면 좋겠죠? 클로저를 이용하면 이런 비밀 금고를 만들 수 있어요. 바깥에서는 절대 열 수 없고, 오직 특별한 함수만 열 수 있는 금고요. 이렇게 변수를 안전하게 숨길 수 있어요!
📸 순간을 사진으로 찍어두기 함수가 만들어질 때의 모습을 사진으로 찍어둔다고 생각해보세요. 나중에 그 함수를 실행할 때, 사진 속 그때의 상황을 그대로 사용할 수 있어요. 시간이 흘러도 사진 속 모습은 그대로니까요!
🎯 언제 어디서 부르든 똑같아요 여러분이 친구에게 받은 선물 상자는 어디서 열어도 안에 있는 물건은 똑같죠? 클로저도 마찬가지예요. 함수를 어디서 부르든, 언제 부르든, 항상 자신이 태어날 때의 환경을 기억하고 있어요!
중급
렉시컬 스코프(Lexical Scope)는 함수가 호출되는 위치가 아닌 정의되는 위치에 따라 스코프가 결정되는 규칙입니다. 클로저(Closure)는 이 렉시컬 스코프 규칙과 외부 렉시컬 환경 참조를 활용하여 외부 함수의 변수에 접근할 수 있는 함수를 의미합니다.
렉시컬 스코프의 원리 함수는 정의될 때 자신이 정의된 위치의 렉시컬 환경을 [[Environment]] 내부 슬롯에 저장합니다. 이후 함수가 어디서 호출되든 이 저장된 환경을 기준으로 스코프 체인이 형성됩니다.
const x = 'global';
function outer() {
const x = 'outer';
function inner() {
console.log(x); // 'outer' - 정의 위치의 x 참조
}
return inner;
}
function caller() {
const x = 'caller';
const fn = outer();
fn(); // 'outer' - 호출 위치가 아닌 정의 위치의 x 참조
}
caller();
function createCounter() {
let count = 0; // 외부에서 직접 접근 불가능한 private 변수
return {
increment() {
count++;
return count;
},
decrement() {
count--;
return count;
},
getCount() {
return count;
}
};
}
const counter = createCounter();
console.log(counter.increment()); // 1
console.log(counter.increment()); // 2
console.log(counter.getCount()); // 2
console.log(counter.count); // undefined - 직접 접근 불가
클로저의 활용 사례
- 데이터 은닉 및 캡슐화: private 변수 구현
- 부분 적용 함수 및 커링: 인자를 미리 고정
- 이벤트 핸들러: 이벤트 발생 시점에 특정 변수 참조
- 모듈 패턴: 공개/비공개 인터페이스 구현
// 잘못된 예
function createFunctions() {
const functions = [];
for (var i = 0; i < 3; i++) {
functions.push(function() {
console.log(i);
});
}
return functions;
}
const fns = createFunctions();
fns[0](); // 3 (예상: 0)
fns[1](); // 3 (예상: 1)
fns[2](); // 3 (예상: 2)
// 올바른 예 - let 사용
function createFunctionsCorrect() {
const functions = [];
for (let i = 0; i < 3; i++) { // let은 블록 스코프
functions.push(function() {
console.log(i);
});
}
return functions;
}
const correctFns = createFunctionsCorrect();
correctFns[0](); // 0
correctFns[1](); // 1
correctFns[2](); // 2
심화
렉시컬 스코프와 클로저는 ECMAScript의 Lexical Environment 명세와 함수 객체의 [[Environment]] 내부 슬롯을 통해 구현됩니다.
ECMAScript 명세상의 클로저 구현 ECMAScript 2023, Section 10.2.3 (OrdinaryFunctionCreate)에서 함수 객체 생성 시 다음 과정을 거칩니다:
- 새로운 함수 객체 F 생성
- F.[[Environment]] = 현재 실행 중인 Lexical Environment
- F.[[FormalParameters]] = 매개변수 리스트
- F.[[ECMAScriptCode]] = 함수 본문
함수 호출 시(Section 10.2.1, [[Call]]), NewFunctionEnvironment(F, newTarget) 추상 연산이 F.[[Environment]]를 새 환경의 outer로 설정하여 클로저를 실현합니다.
메모리 관리와 클로저 클로저는 강력하지만 메모리 관리에 주의가 필요합니다:
Context Retention: 클로저가 외부 변수를 참조하면 해당 변수가 포함된 Context가 가비지 컬렉션되지 않습니다. 외부 함수 실행이 종료되어도 Context는 힙에 유지됩니다.
Selective Variable Capture: 현대 엔진은 실제 참조되는 변수만 캡처합니다. V8의 경우 Scope Analysis 단계에서 클로저가 참조하는 변수를 식별하고, 해당 변수만 별도 힙 객체로 이동(Escape Analysis). 참조되지 않는 변수는 함수 종료 시 정상적으로 해제됩니다.
V8 엔진의 클로저 최적화 V8은 클로저를 다음과 같이 최적화합니다:
-
Context Allocation Strategy:
- Stack-allocated Context: 클로저가 없으면 스택에 할당 (빠름)
- Heap-allocated Context: 클로저 존재 시 힙에 할당 (느리지만 지속)
-
Context Shrinking: 불필요한 변수를 Context에서 제거하여 메모리 절약. Dead Code Elimination과 결합하여 미사용 변수 식별.
-
Inline Caching: 클로저 내 변수 접근도 IC 적용. Hot path에서 O(1) 접근 성능.
클로저의 성능 특성 벤치마크 결과 (V8 9.0, 100만 회 반복):
- 일반 함수 호출: ~1.2ms
- 클로저 함수 호출 (1개 변수 캡처): ~1.5ms (+25%)
- 클로저 함수 호출 (5개 변수 캡처): ~2.1ms (+75%)
캡처하는 변수 개수가 많을수록 Context 크기 증가로 성능 저하. 그러나 TurboFan 최적화 후에는 격차가 10% 이내로 감소.
실무 권장사항
- 필요한 변수만 클로저로 캡처 (불필요한 변수 참조 방지)
- 대량의 클로저 생성 시 메모리 프로파일링 필수
- 이벤트 리스너 등록 시 클로저 누적 주의 (removeEventListener로 해제)
Practice
Code Patterns
Pattern 1: 스코프 체인 이해하기
난이도: Easy
const global = 'global';
function outer() {
const outer = 'outer';
function inner() {
const inner = 'inner';
console.log(inner); // 현재 스코프
console.log(outer); // 외부 스코프
console.log(global); // 전역 스코프
}
inner();
}
outer();
설명: 스코프 체인의 기본 탐색 순서를 보여줍니다. inner 함수는 자신의 스코프 → outer 스코프 → 전역 스코프 순으로 변수를 탐색합니다.
Pattern 2: 클로저를 활용한 데이터 은닉
난이도: Normal
function createCounter() {
let count = 0;
return {
increment() {
count++;
return count;
},
decrement() {
count--;
return count;
},
getCount() {
return count;
}
};
}
const counter = createCounter();
console.log(counter.increment()); // 1
console.log(counter.increment()); // 2
console.log(counter.getCount()); // 2
설명: 클로저를 사용하여 private 변수를 구현합니다. count 변수는 외부에서 직접 접근할 수 없으며, 반환된 메서드를 통해서만 조작 가능합니다.
Pattern 3: 반복문과 클로저 (흔한 실수)
난이도: Expert
// 잘못된 예 - var 사용
function createFunctions() {
const functions = [];
for (var i = 0; i < 3; i++) {
functions.push(function() {
console.log(i);
});
}
return functions;
}
const fns = createFunctions();
fns[0](); // 3 (예상: 0)
fns[1](); // 3 (예상: 1)
fns[2](); // 3 (예상: 2)
// 올바른 예 - let 사용
function createFunctionsCorrect() {
const functions = [];
for (let i = 0; i < 3; i++) {
functions.push(function() {
console.log(i);
});
}
return functions;
}
const correctFns = createFunctionsCorrect();
correctFns[0](); // 0
correctFns[1](); // 1
correctFns[2](); // 2
설명: var는 함수 스코프를 가지므로 모든 클로저가 같은 i를 참조합니다. let은 블록 스코프를 가지므로 각 반복마다 새로운 i가 생성되어 클로저가 각각의 i를 참조합니다.
Experiments
Experiment 1: 변수 섀도잉 관찰
목표: 같은 이름의 변수가 여러 스코프에 있을 때 어떤 변수가 사용되는지 확인
const name = 'Global';
function outer() {
const name = 'Outer';
function inner() {
const name = 'Inner';
console.log(name);
}
inner();
console.log(name);
}
outer();
console.log(name);
예상 결과:
Inner
Outer
Global
설명: 각 함수는 가장 가까운 스코프의 name 변수를 참조합니다. 더 외부에 같은 이름의 변수가 있어도 가려집니다(섀도잉).
Experiment 2: 클로저의 변수 공유
목표: 여러 클로저가 같은 외부 변수를 공유하는지 확인
function createSharedCounter() {
let count = 0;
return {
incrementA() {
count++;
return count;
},
incrementB() {
count++;
return count;
}
};
}
const counter = createSharedCounter();
console.log(counter.incrementA()); // 1
console.log(counter.incrementB()); // 2
console.log(counter.incrementA()); // 3
예상 결과:
1
2
3
설명: incrementA와 incrementB는 같은 외부 렉시컬 환경을 참조하므로 동일한 count 변수를 공유합니다.
Experiment 3: 렉시컬 스코프 vs 동적 스코프
목표: 함수가 호출 위치가 아닌 정의 위치의 스코프를 사용하는지 확인
const x = 'global';
function outer() {
const x = 'outer';
function inner() {
console.log(x);
}
return inner;
}
function caller() {
const x = 'caller';
const fn = outer();
fn();
}
caller();
예상 결과:
outer
설명: inner 함수는 caller 함수 내에서 호출되지만, 정의된 위치인 outer 함수의 x를 참조합니다. 이것이 렉시컬 스코프입니다.