클로저란?
클로저(Closure)는 함수와 그 함수가 선언된 렉시컬 환경의 조합입니다. 쉽게 말해, 함수가 자신이 만들어진 환경의 변수를 기억하고 접근할 수 있는 특성입니다. 함수가 외부 스코프의 변수를 참조하면, 해당 스코프가 종료된 이후에도 변수에 접근할 수 있습니다.
클로저는 JavaScript의 가장 핵심적인 개념 중 하나이며, 데이터 캡슐화, 팩토리 패턴, 이벤트 핸들러, React의 hooks 등 곳곳에서 활용됩니다.
스코프 체인과 렉시컬 환경
JavaScript 엔진은 코드를 실행할 때 **실행 컨텍스트(Execution Context)**를 생성합니다. 각 실행 컨텍스트에는 변수를 저장하는 **렉시컬 환경(Lexical Environment)**이 포함되며, 외부 환경에 대한 참조를 통해 스코프 체인을 형성합니다.
const globalVar = "전역";
function outer() {
const outerVar = "외부";
function inner() {
const innerVar = "내부";
// 스코프 체인: inner → outer → global
console.log(innerVar); // "내부" (자신의 스코프)
console.log(outerVar); // "외부" (외부 함수 스코프)
console.log(globalVar); // "전역" (전역 스코프)
}
inner();
}
outer();
// 내부
// 외부
// 전역
변수를 찾을 때 현재 스코프에서 시작하여 상위 스코프로 올라가며 탐색합니다. 이 체인이 스코프 체인입니다.
| 스코프 종류 | 생성 시점 | 예시 |
|---|---|---|
| 전역 스코프 | 프로그램 시작 | const globalVar |
| 함수 스코프 | 함수 호출 | var, function 선언 |
| 블록 스코프 | 블록 {} 진입 | let, const 선언 |
클로저의 동작 원리
클로저는 함수가 정의된 위치의 스코프를 기억합니다. 호출된 위치가 아닙니다.
function createCounter(initial = 0) {
let count = initial; // 이 변수가 클로저에 의해 유지됨
return {
increment() {
count += 1;
return count;
},
decrement() {
count -= 1;
return count;
},
getCount() {
return count;
},
};
}
const counter = createCounter(10);
console.log(counter.increment()); // 11
console.log(counter.increment()); // 12
console.log(counter.decrement()); // 11
console.log(counter.getCount()); // 11
// count 변수에 직접 접근 불가 — 캡슐화
// console.log(count); // ReferenceError
createCounter()가 실행을 마쳐도 count 변수는 반환된 메서드들이 참조하고 있으므로 가비지 컬렉션되지 않습니다. 이것이 클로저의 핵심입니다.
실전 패턴 1 — 데이터 캡슐화
클로저는 프라이빗 변수를 구현하는 데 사용됩니다. 외부에서 직접 접근할 수 없는 상태를 만들 수 있습니다.
function createWallet(ownerName, initialBalance = 0) {
// 프라이빗 변수 — 외부에서 직접 접근 불가
let balance = initialBalance;
const transactions = [];
function recordTransaction(type, amount) {
transactions.push({
type,
amount,
balance,
timestamp: new Date().toISOString(),
});
}
return {
deposit(amount) {
if (amount <= 0) throw new Error("양수만 입금 가능합니다");
balance += amount;
recordTransaction("입금", amount);
return `${amount}원 입금 완료. 잔액: ${balance}원`;
},
withdraw(amount) {
if (amount <= 0) throw new Error("양수만 출금 가능합니다");
if (amount > balance) throw new Error("잔액이 부족합니다");
balance -= amount;
recordTransaction("출금", amount);
return `${amount}원 출금 완료. 잔액: ${balance}원`;
},
getBalance() {
return balance;
},
getHistory() {
return [...transactions]; // 복사본 반환
},
getOwner() {
return ownerName;
},
};
}
const wallet = createWallet("홍길동", 10000);
console.log(wallet.deposit(5000));
// 5000원 입금 완료. 잔액: 15000원
console.log(wallet.withdraw(3000));
// 3000원 출금 완료. 잔액: 12000원
console.log(wallet.getBalance());
// 12000
// balance, transactions에 직접 접근 불가
// console.log(wallet.balance); // undefined
실전 패턴 2 — 함수 팩토리
클로저를 사용하면 설정값을 기억하는 특화된 함수를 생성할 수 있습니다.
// 멱등성 체크 함수 팩토리
function createRateLimiter(maxCalls, windowMs) {
const calls = [];
return function rateLimiter() {
const now = Date.now();
// 윈도우 시간이 지난 호출 제거
while (calls.length > 0 && calls[0] <= now - windowMs) {
calls.shift();
}
if (calls.length >= maxCalls) {
return { allowed: false, retryAfter: calls[0] + windowMs - now };
}
calls.push(now);
return { allowed: true, remaining: maxCalls - calls.length };
};
}
// 1초에 3번까지 허용하는 제한기
const limiter = createRateLimiter(3, 1000);
console.log(limiter()); // { allowed: true, remaining: 2 }
console.log(limiter()); // { allowed: true, remaining: 1 }
console.log(limiter()); // { allowed: true, remaining: 0 }
console.log(limiter()); // { allowed: false, retryAfter: ... }
흔한 실수 — 루프에서의 클로저
var와 루프를 함께 사용하면 예상치 못한 동작이 발생합니다. 이는 클로저와 관련된 가장 유명한 함정입니다.
// 잘못된 예: var는 함수 스코프
for (var i = 0; i < 3; i++) {
setTimeout(() => {
console.log(i); // 모두 3 출력 (루프 종료 후의 i 값)
}, 100);
}
// 3, 3, 3
// 올바른 해결 1: let은 블록 스코프 (각 반복마다 새 바인딩)
for (let i = 0; i < 3; i++) {
setTimeout(() => {
console.log(i); // 0, 1, 2 정상 출력
}, 100);
}
// 올바른 해결 2: IIFE로 클로저 생성 (ES6 이전 방법)
for (var i = 0; i < 3; i++) {
(function (captured) {
setTimeout(() => {
console.log(captured); // 0, 1, 2
}, 100);
})(i);
}
var는 함수 스코프이므로 루프 전체에서 하나의 i를 공유합니다. let은 블록 스코프이므로 각 반복마다 독립적인 i가 생성됩니다.
메모리 고려사항
클로저가 참조하는 변수는 가비지 컬렉션되지 않으므로 메모리 누수에 주의해야 합니다.
function createHeavyClosure() {
// 큰 데이터를 클로저가 참조
const hugeArray = new Array(1000000).fill("data");
return function process() {
// hugeArray 전체가 메모리에 유지됨
return hugeArray.length;
};
}
// 개선: 필요한 데이터만 클로저에 포함
function createLightClosure() {
const hugeArray = new Array(1000000).fill("data");
const length = hugeArray.length; // 필요한 값만 추출
// hugeArray는 더 이상 참조되지 않아 GC 대상
return function process() {
return length;
};
}
정리
클로저는 JavaScript의 핵심 메커니즘이며 올바르게 이해하면 강력한 패턴을 구현할 수 있습니다.
- 정의: 함수와 그 함수가 선언된 렉시컬 환경의 조합
- 스코프 체인: 현재 스코프에서 상위 스코프로 변수를 탐색하는 체인
- 캡슐화: 프라이빗 변수를 구현하여 외부 접근을 제한
- 팩토리: 설정값을 기억하는 특화된 함수 생성
- 주의점:
var+ 루프 함정, 메모리 누수에 주의 - 현대 활용: React hooks, 이벤트 핸들러, 커링, 메모이제이션 등에 폭넓게 활용