JavaScript 클로저: 렉시컬 환경 메커니즘과 실전 활용법
JavaScript에서 클로저는 함수와 그 함수의 렉시컬 환경의 조합으로, 내부 함수가 외부 함수의 실행이 완료된 후에도 외부 스코프의 변수에 접근할 수 있게 합니다. 이를 통해 비동기 컨텍스트에서 상태 캡슐화와 예측 가능한 동작을 구현할 수 있습니다.
이 개념은 Alonzo Church의 람다 계산법에서 비롯되었습니다. 고차 함수는 다른 함수를 반환하거나 받아들이며, 자유 변수(함수 내부에서 사용되지만 외부에서 선언된 변수)를 캡처합니다.
기본적인 카운터 예제로 원리를 살펴보겠습니다:
function counter() {
let count = 0
return function(step) {
count = count + step
return count
}
}
const myCounter = counter()
console.log(myCounter(1)) // 1
console.log(myCounter(1)) // 2
console.log(myCounter(1)) // 3
내부 함수는 count를 클로저로 감싸 가비지 컬렉션을 방지합니다. 이는 실시간으로 변경 사항이 반영되는 라이브 참조입니다.
클로저를 객체지향 프로그래밍에 비유하기
클로저는 객체의 private 필드와 public API를 모방합니다. 메서드를 가진 카운터 예제:
function Counter(initialValue = 0) {
let count = initialValue
return {
increment: function(step = 1) {
return count += step
},
decrement: function(step = 1) {
return count -= step
},
getValue: function() {
return count
},
reset: function() {
return count = initialValue
}
}
}
const myCounter = Counter(10)
console.log(myCounter.getValue()) // 10
console.log(myCounter.increment(5)) // 15
private 필드(#count)를 사용한 클래스로 동일한 로직을 구현할 수 있습니다. Pinia/Vuex 스토어에서는 클로저가 반응형 상태를 캡슐화합니다:
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
const initValue = 0
function increment(step) {
count.value += step
}
return { count, increment }
})
반복문과 이터레이터에서의 미묘한 차이
반복문에서 클로저의 동작은 호이스팅과 블록 스코프에 따라 달라집니다.
var 사용 시 — 모든 반복에서 공유되는 변수:
for (var i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i) // 3, 3, 3
}, 100)
}
let 사용 시 — 반복마다 별도의 렉시컬 환경 생성:
for (let i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i) // 0, 1, 2
}, 100)
}
forEach, map, filter 같은 이터레이터는 자동으로 클로저를 생성합니다:
[0, 1, 2].forEach(function(i) {
setTimeout(function() {
console.log(i) // 0, 1, 2
}, 100)
})
실전: 콜백과 이벤트
비동기 코드에서 클로저는 컨텍스트 문제를 해결합니다.
고유 ID를 사용한 Ajax 요청:
function ajaxRequest(url) {
let requestId = Math.random().toString(36).substring(2)
fetch(url).then(data => {
console.log(`Request ${requestId} completed:`, data)
}).catch(error => {
console.log(`Request ${requestId} failed:`, error.message)
})
}
이벤트 핸들러:
function clickSetup(elemId, message) {
const elem = document.getElementById(elemId)
elem.addEventListener('click', function() {
console.log(message)
})
}
화살표 함수도 일반 함수와 동일하게 작동합니다.
최적화와 디버깅
JS 엔진(V8)은 메모리를 절약하기 위해 클로저에서 사용되지 않는 변수를 제외합니다. DevTools(Scope → Closure)에서는 관련 변수만 표시됩니다.
핵심 실천법:
- 블록 스코프를 위해 반복문에서
let/const를 사용하세요. - 클래스 없이 private 상태를 구현할 때 클로저를 선호하세요.
- 비동기 콜백에서 컨텍스트를 조기에 고정하세요.
- 장기간 유지되는 클로저에서 메모리를 모니터링하세요.
핵심 요약
- 클로저 = 함수 + 변수에 대한 라이브 참조를 가진 렉시컬 환경.
- 반복문에서
let은 반복마다 클로저를 생성하지만,var는 그렇지 않습니다. - 객체지향 프로그래밍의 private 필드를 모방하며, Vuex/Pinia 스토어에서 사용됩니다.
- 엔진에 의해 최적화: 사용되지 않는 변수는 제외됩니다.
- 비동기 콜백과 이벤트에 필수적입니다.
— Editorial Team
아직 댓글이 없습니다.