변수의 유효범위와 렉시컬 환경
자바스크립트에서 변수의 유효 범위(Scope)는 해당 변수가 어디서부터 어디까지 유효하게 사용될 수 있는지를 결정합니다. 특히, let과 const 키워드를 사용하면 블록 스코프(block scope)를 갖는 변수를 선언할 수 있습니다. 즉, 함수나 조건문, 반복문 등 { ... } 안에서 선언한 변수는 해당 블록 외부에서는 접근할 수 없습니다.
{
let message = "안녕하세요!";
console.log(message); // 출력: 안녕하세요!
}
console.log(typeof message); // 출력: undefined (message 변수는 블록 밖에서 접근 불가)
자바스크립트의 렉시컬 환경(Lexical Environment)은 코드가 작성된 구조를 기반으로 식별자와 변수의 유효 범위를 결정합니다. 각 실행 컨텍스트(execution context)는 자신만의 렉시컬 환경을 가지며, 이를 통해 변수의 접근과 생명 주기를 관리합니다.
클로저의 개념
클로저(Closure)는 함수가 선언되었을 때의 렉시컬 환경을 기억하여, 함수가 외부Scope 밖에서 호출되더라도 그 환경에 접근할 수 있는 기능을 말합니다. 즉, 함수와 그 함수가 선언된 상태에서의 주변 변수들로 구성된 패키지라고 할 수 있습니다.
function makeCounter() {
let count = 0; // 클로저가 기억할 변수
function counter() {
count += 1;
return count;
}
return counter;
}
const counter1 = makeCounter();
console.log(counter1()); // 출력: 1
console.log(counter1()); // 출력: 2
const counter2 = makeCounter();
console.log(counter2()); // 출력: 1 (counter2는 독립적인 클로저)
makeCounter 함수는 내부에 count 변수를 선언하고, 이 변수를 접근하는 counter 함수를 반환합니다. 이 counter 함수는 count에 대한 클로저를 형성하여, 외부에서 count에 직접 접근하지 않고도 값을 변경하고 유지할 수 있습니다.
클로저의 활용: 데이터 은닉과 상태 관리
클로저는 외부에 노출하고 싶지 않은 데이터를 은닉(encapsulation)하고, 상태(state)를 유지하는 데에 유용합니다. 이를 통해 모듈 패턴이나 정보 은닉을 구현할 수 있습니다.
은행 계좌 객체 생성 예제
function createBankAccount(initialBalance) {
let balance = initialBalance; // 비공개 변수
return {
deposit(amount) {
balance += amount;
console.log(`입금: ${amount}원, 잔액: ${balance}원`);
},
withdraw(amount) {
if (amount <= balance) {
balance -= amount;
console.log(`출금: ${amount}원, 잔액: ${balance}원`);
} else {
console.log('잔액이 부족합니다.');
}
},
getBalance() {
return balance;
}
};
}
const myAccount = createBankAccount(1000);
myAccount.deposit(500); // 출력: 입금: 500원, 잔액: 1500원
myAccount.withdraw(200); // 출력: 출금: 200원, 잔액: 1300원
console.log(myAccount.getBalance()); // 출력: 1300
위 코드에서 balance 변수는 외부에서 직접 접근할 수 없으며, 반환된 객체의 메서드를 통해서만 조작할 수 있습니다. 이를 통해 데이터의 무결성을 유지하고, 예기치 않은 변경을 방지할 수 있습니다.
반복문에서의 클로저 주의점
클로저를 반복문 내에서 사용할 때 변수의 스코프에 유의해야 합니다. 특히 var 키워드는 함수 스코프를 가지므로, 반복문에서 예상치 못한 동작을 일으킬 수 있습니다.
반면 let을 사용하면 각 반복마다 새로운 변수가 생성되어 올바른 값을 캡처합니다.
// 문제 발생: var 사용 시
function createCallbacks() {
var callbacks = [];
for (var i = 0; i < 3; i++) {
callbacks.push(function() {
console.log(`i의 값: ${i}`);
});
}
return callbacks;
}
var callbacksVar = createCallbacks();
callbacksVar.forEach(function(cb) {
cb(); // 모두 "i의 값: 3" 출력
});
// 해결 방법: let 사용하기
function createFixedCallbacks() {
var callbacks = [];
for (let i = 0; i < 3; i++) {
callbacks.push(function() {
console.log(`i의 값: ${i}`);
});
}
return callbacks;
}
var callbacksLet = createFixedCallbacks();
callbacksLet.forEach(function(cb) {
cb(); // "i의 값: 0", "i의 값: 1", "i의 값: 2" 출력
});
var로 선언된 i는 함수 스코프를 가지므로, 루프가 끝난 후의 값인 3이 클로저에 저장됩니다. 반면, let으로 선언하면 블록 스코프를 가지므로, 각 반복마다 새로운 i 변수가 생성되어 클로저에 저장됩니다. 따라서 의도한 대로 각기 다른 i 값을 출력할 수 있습니다.
클로저와 메모리 관리
클로저는 함수가 필요로 하는 변수와 함수를 메모리에 유지하기 때문에, 불필요한 클로저가 많아지면 메모리 누수가 발생할 수 있습니다.
function createHeavyCounter() {
let count = 0;
// 내부 함수를 여러 곳에 저장하면, count는 계속 유지됩니다.
return function() {
return count++;
};
}
let heavyCounter = createHeavyCounter();
console.log(heavyCounter()); // 0
// 이후, heavyCounter에 대한 참조를 제거하면 그 클로저가 가비지 컬렉션 대상이 됩니다.
heavyCounter = null;
'JavaScript' 카테고리의 다른 글
[JavaScript] 전역 객체 및 전역 변수 관리 (0) | 2025.02.13 |
---|---|
[JavaScript] var의 특징 (0) | 2025.02.13 |
[JavaScript] 나머지 매개변수와 스프레드 구문 (0) | 2025.02.13 |
[JavaScript] 재귀와 스택의 동작 원리 (0) | 2025.02.13 |
[JavaScript] JSON의 직렬화와 역직렬화 (0) | 2025.02.13 |