JavaScript 클로저와 스코프 간단 설명
다른 함수를 반환하는 JavaScript 함수를 작성하고 있다고 가정해 봅시다. 내부 함수는 외부 함수의 변수를 사용하지만, 나중에 호출할 때 외부 함수는 이미 실행을 마쳤습니다. 그런데도 변수는 여전히 존재합니다. 어떻게 가능할까요? 바로 클로저와 스코프의 마법입니다. 많은 개발자가 이 개념에 어려움을 겪으며, 예상치 못한 변수 값이나 메모리 누수 같은 버그를 만듭니다. 이 글에서는 JavaScript 클로저와 스코프를 실제 예제와 함께 쉽게 설명하겠습니다.
JavaScript에서 스코프란?
스코프는 코드에서 변수에 접근할 수 있는 범위를 결정합니다. JavaScript에는 세 가지 주요 스코프 유형이 있습니다:
- 전역 스코프: 어떤 함수나 블록 외부에서 선언된 변수는 전역적으로 접근할 수 있습니다.
- 함수 스코프: 함수 내부에서 선언된 변수는 해당 함수 내에서만 접근할 수 있습니다.
- 블록 스코프: 블록
{}내부에서let과const로 선언된 변수는 해당 블록 내에서만 접근할 수 있습니다.
간단한 예를 들어보겠습니다:
let globalVar = 'I am global';
function myFunc() {
let functionVar = 'I am in function scope';
if (true) {
let blockVar = 'I am in block scope';
console.log(globalVar); // accessible
console.log(functionVar); // accessible
console.log(blockVar); // accessible
}
console.log(blockVar); // ReferenceError: blockVar is not defined
}
myFunc();
console.log(functionVar); // ReferenceError: functionVar is not defined
스코프를 이해하는 것은 변수 충돌을 방지하고 메모리를 관리하는 데 도움이 되므로 매우 중요합니다.
렉시컬 스코핑: 클로저의 기초
JavaScript는 렉시컬 스코핑을 사용합니다. 즉, 변수의 스코프는 소스 코드에서의 위치에 따라 결정됩니다. 내부 함수는 외부(둘러싸는) 스코프의 변수에 접근할 수 있습니다. 이를 정적 스코핑이라고도 합니다.
다음을 살펴보세요:
function outer() {
let outerVar = 'outer';
function inner() {
console.log(outerVar); // 'outer'
}
inner();
}
outer();
inner 함수는 outer 내부에 렉시컬하게 위치하므로 outerVar에 접근할 수 있습니다. 이는 동적이 아니며, 함수가 어떻게 호출되는지에 의존하지 않습니다.
클로저란?
클로저는 함수가 해당 스코프 외부에서 실행될 때도 자신의 렉시컬 스코프를 기억하는 함수입니다. 다시 말해, 클로저는 내부 함수에서 외부 함수의 스코프에 접근할 수 있게 해줍니다. 클로저는 함수가 생성될 때마다, 함수 생성 시점에 만들어집니다.
예:
function makeCounter() {
let count = 0;
return function() {
count++;
return count;
};
}
const counter = makeCounter();
console.log(counter()); // 1
console.log(counter()); // 2
여기서 makeCounter는 count를 증가시키는 내부 함수를 반환합니다. makeCounter의 실행이 끝난 후에도 반환된 함수는 여전히 count에 접근할 수 있습니다. 이것이 클로저입니다.
클로저의 내부 작동 방식
함수가 생성될 때, 함수는 자신의 렉시컬 환경(스코프 체인)에 대한 참조를 저장합니다. 함수가 호출되면, 그 환경을 사용하여 변수를 해석합니다. 환경은 클로저가 존재하는 한 계속 유지됩니다.
클로저의 실용적인 사용
클로저는 이론적인 것만이 아니라 JavaScript 전반에서 사용됩니다.
1. 데이터 프라이버시 (모듈 패턴)
ES6 모듈 이전에는 클로저를 사용하여 프라이빗 변수를 만들었습니다:
const counterModule = (function() {
let count = 0;
return {
increment: function() { count++; },
getCount: function() { return count; }
};
})();
counterModule.increment();
console.log(counterModule.getCount()); // 1
console.log(counterModule.count); // undefined
여기서 count는 프라이빗이며 반환된 메서드를 통해서만 접근할 수 있습니다.
2. 이벤트 핸들러와 콜백
클로저는 이벤트 핸들러에서 외부 스코프의 변수에 접근하기 위해 흔히 사용됩니다:
function setupButton() {
let clickCount = 0;
document.getElementById('myButton').addEventListener('click', function() {
clickCount++;
console.log(`Button clicked ${clickCount} times`);
});
}
이벤트 핸들러 클로저는 clickCount를 기억합니다.
3. 함수 팩토리
미리 설정된 인수를 가진 함수를 만들 수 있습니다:
function multiplyBy(factor) {
return function(number) {
return number * factor;
};
}
const double = multiplyBy(2);
console.log(double(5)); // 10
흔한 함정과 피하는 방법
클로저를 이해하지 못하면 예상치 못한 동작을 초래할 수 있습니다.
루프 변수 캡처
루프에서 var를 사용하면 단일 공유 바인딩이 생성됩니다:
for (var i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i); // prints 3, 3, 3
}, 100);
}
모든 콜백이 동일한 i를 공유합니다. 해결하려면 let(블록 스코프) 또는 IIFE를 사용하세요:
for (let i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i); // prints 0, 1, 2
}, 100);
}
메모리 누수
클로저는 변수를 계속 살려둡니다. 클로저가 장수명 객체(예: DOM 요소)에 연결되어 있으면 가비지 컬렉션을 방해할 수 있습니다. 더 이상 필요하지 않으면 항상 이벤트 리스너를 제거하세요.
스코프 체인과 변수 해석
변수에 접근할 때 JavaScript는 스코프 체인을 따라 조회합니다: 먼저 현재 스코프, 그 다음 외부 스코프, 전역 스코프까지. 찾지 못하면 ReferenceError가 발생합니다.
예:
let a = 'global';
function outer() {
let b = 'outer';
function inner() {
let c = 'inner';
console.log(a, b, c); // all accessible
}
inner();
}
outer();
inner의 스코프 체인은 inner -> outer -> global입니다.
비교: var, let, const
| 키워드 | 스코프 | 호이스팅 | 재할당 |
|---|---|---|---|
| var | 함수 | 예 (undefined로 초기화됨) | 예 |
| let | 블록 | 예 (초기화되지 않음, TDZ) | 예 |
| const | 블록 | 예 (초기화되지 않음, TDZ) | 아니오 |
이러한 차이를 이해하는 것이 스코프 관련 버그를 피하는 핵심입니다.
FAQ
JavaScript에서 클로저란?
클로저는 해당 스코프 외부에서 실행될 때도 자신의 렉시컬 스코프에 대한 접근을 유지하는 함수입니다. 함수가 정의된 곳의 변수를 "기억"할 수 있게 해줍니다.
클로저가 왜 중요한가요?
클로저는 데이터 프라이버시, 함수 팩토리, 상태를 유지하는 이벤트 핸들러를 가능하게 합니다. 많은 JavaScript 패턴의 기초입니다.
클로저 관련 버그를 어떻게 피할 수 있나요?
블록 스코핑을 위해 let과 const를 사용하고, 루프를 주의하며, 메모리 누수를 방지하기 위해 이벤트 리스너를 정리하세요.
이해도를 테스트할 준비가 되셨나요? JSON Formatter를 사용해 코드 스니펫을 실험하고 클로저가 실시간으로 어떻게 작동하는지 확인해 보세요.