변수 보호와 은닉의 필요성
일반적으로 전역 변수는 어디서든 접근하고 수정할 수 있다는 위험이 있습니다.
그렇다면 변수를 전역이 아닌, 함수 내부에 선언하면 어떨까요?
function levelup() {
let level = 1; // 함수가 실행될 때마다 초기화
level += 1;
return level;
}
let level = levelup();
level = levelup();
console.log(level); // 여전히 2
level 변수가 함수가 호출될 때마다 새로 생성되기 때문에 상태를 유지하지 못합니다.
여기서 변수의 상태를 유지하면서 외부로부터 보호하는 방법이 바로 클로저입니다.
클로저가 필요한 이유
#데이터 은닉 - 외부에서 데이터에 직접 접근 불가
#상태 유지 - 함수 호출 간 변수를 계속 기억
클로저 현상
클로저 함수는 자신이 선언될 당시의 렉시컬 환경을 기억하고 있습니다...
어느 내부 함수가 자신의 상위 렉시컬 환경을 참조하고 있을 때,
그 상위 렉시컬 환경의 실행 컨텍스트가 콜스택에서 사라지더라도 내부함수(클로저)가 상위 렉시컬 환경을 참조하고 있기 때문에
상위 렉시컬 환경은 가비지 컬렉션이 수행되지 않고 메모리에 남아있게 됩니다.
이로 인하여 상위 렉시컬을 계속 참조할 수 있게 됩니다.
이 현상을 클로저 현상이라고 부릅니다!
가비지 컬렉션이 수행되지 않아 렉시컬 환경이 메모리에서 해제되지 않아서 계속해서 데이터를 사용 가능한 것이 핵심이죠
여기서 렉시컬 환경이란 함수가 생성될 때의 변수, 함수 등을 의미합니다.
클로저 예제
function levelup() {
let level = 0; // 이 변수가 클로저에 의해 기억됩니다
function inner() {
level++;
return level;
}
return inner; // 함수 자체를 반환합니다.
}
let warriorLevel = levelup();
let warriorLevelup = warriorLevel(); // level = 1
warriorLevelup = warriorLevel(); // level = 2
console.log(warriorLevelup); // 3
이 클로저에서 중요한 점은 return inner; 라는 구문으로 함수 자체를 반환하는것 입니다.
inner();는 함수의 호출입니다.
이 구문으로 인해 let warriorLevel = levelup();을 실행하게 되면 warriorLevel 변수에는 반환되는 inner함수가 들어가게 되겠죠
이를 warriorLevel();을 이용해서 상위 함수인 levelup가 아닌 하위 함수 inner를 호출할 수 있게 됩니다!
호출된 inner 함수(클로저)는 렉시컬 환경(level 변수)를 기억하고 있어,
여러번 호출되어도 같은 level 변수를 참조하고 있습니다.
따라서 값의 변화를 유지할 수 있는 것이죠!
const archerLevel = levelup(); // 새로운 실행 컨텍스트
let archer = archerLevel(); // level = 1
archer = archerLevel(); // level = 2
console.log(archer); // 3
// warriorLevel의 상태는 archerLevel의 영향을 받지 않습니다.
이 때 warriorLevel이 levelup의 상태를 유지하는 것처럼,
archerLevel이라는 새로운 클로저를 만들면 새로 만들어진 클로저는 새로운 levelup을 참조하게 되어 두 클로저들은 각각 독립적인 상태를 가지게 됩니다.
즉, 워리어와 아쳐가 서로 다른 level 변수를 참조하고 있다는 거죠!
클로저의 활용 패턴
데이터 은닉
function createUser(name) {
let _name = name; // _ (언더바)는 private 변수라는 관례적인 표기
return {
getName: () => _name,
setName: (newName) => (_name = newName),
};
}
const user = createUser("Kim");
console.log(user.getName()); // "Kim"
user.setName("Jiho");
console.log(user.getName()); // "Jiho"
// 외부에서는 _name 변수에 직접 접근하거나 변경할 수 없습니다.
// user._name = "Hacker"; // 이 코드는 작동하지 않습니다.
외부에서 접근할 수 없는 은닉화 된 변수를 만들 수 있고,
이 변수 이름 앞에 _(언더바)를 붙여 표시하는 것이 관례입니다!
함수 팩토리
function makeMultiple(multiple) {
return function (x) {
return x * multiple;
};
}
const double = makeMultiple(2); // multiple이 2로 고정된 함수를 생성
const triple = makeMultiple(3); // multiple이 3으로 고정된 함수를 생성
console.log(double(5)); // 10
console.log(triple(5)); // 15
함수 팩토리란 함수를 만들어내는 함수를 의미합니다.
- 특정 설정을 미리 고정시켜놓고, 그 설정을 기반으로 새로운 함수를 만들어 반환하는 형식입니다!
위 코드에서는 double과 triple을 각각 2와 3으로 mutiple함수를 고정해서 생성하였고,
이후 각 함수에 인자로 x값을 보내 원하는 값을 얻을 수 있습니다!
메모이제이션 Memoization
function fibonacci() {
const cache = {
0: 0,
1: 1,
};
function fibo(num) {
if (cache[num] >= 0) return cache[num];
const result = fibo(num - 2) + fibo(num - 1);
cache[num] = result;
return result;
}
return fibo;
}
let fibo = fibonacci();
console.log(fibo(10)); // 55
console.log(fibo(10)); // 55 (메모이제이션 사용)
fibo = null;
반복적인 계산을 캐시에 저장해 성능을 최적화하는 패턴입니다.
클로저를 이용해 캐시를 안전하게 유지할 수 있습니다.
위 코드에서는 피보나치 구현을 캐시를 이용하여 두 번째 호출부터는 더욱 빠른 속도로 값을 구할 수 있게 구현하였고,
핵심적인 특징은 캐시가 클로저의 상위 렉시컬 환경에 위치해 관리된다는 것 입니다.
메모리 해제
클로저를 무분별하게 사용하면 메모리 누수를 일으킬 수 있습니다.
클로저가 상위 렉시컬 환경을 계속 참조하고 있기 때문에,
클로저가 더이상 사용되지 않을 때는 명시적으로 상위 렉시컬 환경과의 참조를 끊어주어야 합니다.
상위 렉시컬을 편하게 해줍시다..
const archerLevel = levelup();
// ... archerLevel 코드 사용 완료
// 더 이상 archerLevel이 필요 없을 때
archerLevel = null; // 참조를 끊어 가비지 컬렉터가 상위 렉시컬 환경의 메모리를 해제할 수 있게 함
클로저에 null을 할당하는 이유?
- 이 변수는 더 이상 유효한 객체를 가리키지 않는다! 는 의도를 명확히 하기 위함입니다.
- const로 선언된 변수는 재할당이 불가능하므로, 메모리 해제를 진행하려면 let으로 할당해야 합니다!
또한, null 뿐만이 아니라 undefined나 3같은 리터럴을 넣어도 결과적으로 체인만 끊으면 되어서 모두 참조를 끊는 역할을 할 수 있지만 명시적으로 표현하기 위하여 null 을 사용하는 편이 좋습니다!
예제 코드 출처
'[웹 개발] JavaScript' 카테고리의 다른 글
| 이벤트 전파와 이를 이용한 이벤트 위임 (1) | 2025.09.10 |
|---|---|
| 생성자 함수와 프로토타입 그리고 this (1) | 2025.08.18 |
| 실행 컨텍스트와 콜스택 (1) | 2025.08.13 |
| 자바스크립트에서의 2차원 배열 (1) | 2025.06.20 |
| 단축 속성명 (0) | 2025.04.28 |