클로저
외부 함수의 변수를 내부 함수가 계속해서 참조할 수 있도록 하는 매커니즘
함수가 생성될 때 렉시컬 스코프(lexical scope)를 기억하며, 함수가 실행될 때도 그 스코프에 계속 접근할 수 있음
자바스크립트의 함수가 일급 객체라는 특성 + 렉시컬 스코프의 조합으로 만들어짐
function outerFunction(outerVariable) {
return function innerFunction(innerVariable) {
console.log('Outer Variable: ' + outerVariable);
console.log('Inner Variable: ' + innerVariable);
};
}
const newFunction = outerFunction('outside');
newFunction('inside');
1. outerFunction('outside')가 실행되면, outerVariable에는 'outside'가 저장됨
2. outerFunction 내부에서 innerFunction을 반환함
3. 반환된 innerFunction을 newFunction에 저장함
4. newFunction('inside')를 호출하면 innerFunction이 실행됨
5. innerFunction 내부에서 outerVariable에 접근함. 이미 outerFunction 실행이 끝났지만 여전히 접근할 수 있음.
내부 함수(innerFunction)은 자신이 선언된 환경(outerFunction의 실행환경)을 기억하고, 그 스코프에 접근할 수 있음
-> 클로저는 함수가 생성될 때 환경을 기억하고 활용하는 기능을 의미함
클로저의 활용
변수와 함수의 접근 범위를 제어하고 특정 데이터와 상태를 유지하기 위해 자주 활용됨
1. 데이터 은닉(Encapsulation):외부 접근을 막고 데이터의 무결성을 유지함
클로저는 외부에서 접근할 수 없는 비공개(private) 변수와 함수를 만들 수 있음
특정 함수 내부에서만 접근 가능한 변수를 생성하고, 이를 조작할 수 있는 함수만 외부로 노출해 관리
function counter() {
let count = 0; // 외부에서 직접 접근 불가능한 변수
return {
increment: function () {
count++;
console.log(`Count: ${count}`);
},
decrement: function () {
count--;
console.log(`Count: ${count}`);
},
getCount: function () {
return count;
}
};
}
const myCounter = counter();
myCounter.increment(); // Count: 1
myCounter.increment(); // Count: 2
myCounter.decrement(); // Count: 1
console.log(myCounter.getCount()); // 1
console.log(myCounter.count); // undefined (외부에서 접근 불가능)
count 변수는 외부에서 직접 변경할 수 없기 떄문에 잘못된 값이 할당되는 것을 방지할 수 있음
getCount, increment, decrement 메서드를 통해서만 count 값을 조작할 수 있음
2. 비동기 작업: 이전의 실행 컨택스트를 유지해야 할 때
비동기 작업을 수행할 때 클로저로 함수 실행 시점의 변수 상태를 유지할 수 있음
function createLogger(name) {
return function() {
console.log(`Logger: ${name}`);
};
}
const logger = createLogger('MyApp');
setTimeout(logger, 1000); // 1초 후에 'Logger: MyApp' 출력
1. createLogger('MyApp')을 호출하면, 내부 함수가 반환됨.
2. 내부 함수가 setTimeout(logger, 1000);에 전달되면서 1초 후에 실행됨
3. 실행될 때 name 변수의 값 'MyApp'을 유지하고 있기 때문에 'Logger: MyApp'이 정상적으로 출력됨
만약 name 변수를 setTimeout 내부에서 선언했다면, 1초 후 실행될 때 name을 찾을 수 없음
클로저는 외부 함수의 변수를 유지한 채 실행할 수 있음
3. 모듈 패턴 구현: 필요한 함수와 데이터만 외부로 노출해 모듈 패턴을 쉽게 구현
모듈 패턴: 하나의 객체를 생성하고, 내부 변수와 메서드를 캡슐화하면서 필요한 것만 외부로 노출하는 방식
(데이터 은닉과 다른 점이 무엇인지 개인적으로 의문..??)
const UserModule = (function () {
let users = []; // 외부에서 직접 접근 불가능
return {
addUser: function (name) {
users.push(name);
console.log(`${name} added`);
},
getUsers: function () {
return users.slice(); // 배열 복사본 반환 (직접 조작 방지)
}
};
})();
UserModule.addUser('Alice');
UserModule.addUser('Bob');
console.log(UserModule.getUsers()); // ['Alice', 'Bob']
console.log(UserModule.users); // undefined (직접 접근 불가능)
내부 변수(users)는 외부에서 직접 수정할 수 없고, 지정된 함수(addUser, getUsers)를 통해 조작 가능
유지보수가 쉬워지고, 코드 응집력이 높아짐
클로저 사용 시 주의할 점
1. 메모리 누수(Memory Leak)
클로저는 외부 변수에 대한 참조를 유지하므로, 불필요한 메모리를 차지할 수 있음
필요하지 않은 클로저는 null 할당을 통해 제거하는 것이 좋음
function outer() {
let data = new Array(1000000).fill('data'); // 큰 데이터
return function inner() {
console.log(data[0]); // 클로저로 인해 'data'가 유지됨
};
}
let myFunc = outer();
myFunc(); // 'data' 출력
myFunc = null; // 클로저 해제 (GC가 수거 가능)
2. 예기치 않은 값 유지
클로저는 변수를 참조(reference) 하므로, 반복문에서 예기치 않은 결과 초래할 수 있음
function createFunctions() {
let funcs = [];
for (var i = 0; i < 3; i++) {
funcs.push(function () {
console.log(i);
});
}
return funcs;
}
const funcs = createFunctions();
funcs[0](); // 3
funcs[1](); // 3
funcs[2](); // 3
var가 함수 스코프를 가짐. var i는 하나의 공유된 변수이므로, 반복문이 끝난 후 i의 최종 값이 모든 함수에서 사용됨.
아니 이거 솔직히 구라같은데;;이생각 하면서 실제로 테스트해봤는데
진짜 var로 하면 결과 다 3으로 뜸... 개신기하다...
2.1. 예기치 않은 값 해결 방법
1. let을 사용해 블록 스코프 유지
function createFunctions() {
let funcs = [];
for (let i = 0; i < 3; i++) {
funcs.push(function () {
console.log(i);
});
}
return funcs;
}
const funcs = createFunctions();
funcs[0](); // 0
funcs[1](); // 1
funcs[2](); // 2
for (let i=0; i<3; i++)
let은 블록 스코프를 가지므로, for 루프의 반복마다 새로운 i 변수가 생성됨
funcs[0]은 i=0인 상태에서 생성된 함수이고, funcs[1]은 i=1, funcs[2]은 i=2인 상태에서 생성된 함수
결과적으로 funcs[0](), funcs[1](), funcs[2]()가 각각 다른 i 값을 올바르게 출력
2. 즉시 실행 함수(IIFE, Immediately Invoked Function Expression)
각 반복에서 새로운 스코프 생성
function createFunctions() {
let funcs = [];
for (var i = 0; i < 3; i++) {
(function (index) {
funcs.push(function () {
console.log(index);
});
})(i);
}
return funcs;
}
const funcs = createFunctions();
funcs[0](); // 0
funcs[1](); // 1
funcs[2](); // 2
function(index) { ... }(i) <- 즉시 실행 함수(IIFE)
i 값을 즉시 실행 함수의 매개변수 index로 전달하여, 새로운 스코프에서 index를 고정시키고 클로저를 생성함
funcs[0], funcs[1], funcs[2]는 각각 다른 index 값을 기억하는 함수가 됨
3. bind()를 사용해 클로저 생성
bind()로 함수 내부에서 i 값을 고정
function createFunctions() {
let funcs = [];
for (var i = 0; i < 3; i++) {
funcs.push(
function () {
console.log(this);
}.bind(i)
);
}
return funcs;
}
const funcs = createFunctions();
funcs[0](); // 0
funcs[1](); // 1
funcs[2](); // 2
.bind(i)를 사용하면, 함수의 this가 i 값으로 고정됨
funcs[0], funcs[1], funcs[2]가 각각 올바른 i 값을 참조함
출처
[1] 매일메일. 241212. 클로저에 대해서 설명해주세요. 2번. https://maeil-mail.kr
[2] gpt에게 클로저에 대해 묻다.
'개발자 강화 > 프론트엔드' 카테고리의 다른 글
[매일메일] Error Boundary란? (FE.250205) (3) | 2025.02.06 |
---|---|
🌟[매일메일] 자바스크립트의 Promise란? (FE.250129/250203) (1) | 2025.02.03 |
[매일메일] 자바스크립트 함수의 특징 (FE.250101) (1) | 2025.01.31 |
[매일메일] 함수 선언식과 함수 표현식의 차이점? (FE.250131) (1) | 2025.01.31 |
[매일메일] 자바스크립트 ES6 버전이란? (FE.250130) (0) | 2025.01.30 |