안녕하세요, 자바스크립트를 공부하는 중급 개발자입니다.
클로저(Closure)라는 개념이 **"함수가 선언된 렉시컬 환경을 기억하는 함수"**라는 것은 이론적으로 이해했습니다. 내부 함수가 외부 함수의 변수에 접근할 수 있다는 기본적인 예제는 수없이 봤습니다.
function outer() {
const outerVar = '외부 변수';
function inner() {
console.log(outerVar); // 외부 함수의 변수에 접근
}
return inner;
}
const innerFunc = outer();
innerFunc(); // "외부 변수" 출력
하지만 이런 교과서적인 예제를 넘어서, 실제 프로젝트에서 클로저가 **"왜 필요하고, 어떻게 유용하게 사용되는지"**에 대한 감이 잘 오지 않습니다.
제가 궁금한 점은 다음과 같습니다.
데이터 은닉(Data Encapsulation): 클로저를 사용해 private 변수처럼 흉내 낼 수 있다고 들었습니다. class 문법이 있는데도 굳이 클로저를 사용해 데이터를 은닉하는 경우가 있나요? 있다면 어떤 장점이 있을까요?
이벤트 핸들러와 반복문: 반복문 안에서 이벤트 리스너를 추가할 때 클로저가 잘못 사용되면 발생하는 문제와 해결책은 익히 알고 있습니다. 이것 외에 이벤트 핸들링에서 클로저가 유용하게 쓰이는 다른 패턴이 있을까요?
성능과 메모리: 클로저를 사용하면 외부 함수의 스코프가 메모리에 계속 남아있게 되는데, 이로 인한 메모리 누수(memory leak)의 위험은 없나요? 실제 프로젝트에서 클로저를 사용할 때 메모리 관점에서 특별히 주의해야 할 점이 있다면 알려주세요.
실무에서 클로저를 유용하게 사용하셨던 경험이나 좋은 패턴이 있다면 코드 예제와 함께 공유해주시면 정말 감사하겠습니다!
안녕하세요! 클로저에 대해 깊이 있게 고민하고 계시는군요. 아주 좋은 질문입니다. 이론을 넘어 실용적인 관점에서 답변드리겠습니다.
말씀하신 대로 클로저는 모듈 패턴(Module Pattern)을 구현하여 private 상태를 만드는 데 핵심적인 역할을 합니다. class가 등장하기 전에는 가장 대중적인 방법이었습니다.
왜 지금도 유용한가?
function createCounter() {
let count = 0; // private 변수
return {
increment: function() {
count++;
console.log(count);
},
decrement: function() {
count--;
console.log(count);
}
};
}
const counter = createCounter();
counter.increment(); // 1
counter.increment(); // 2
// counter.count; // undefined. 외부에서 직접 접근 불가
위 예제에서 count 변수는 createCounter가 반환하는 객체의 메서드들만 접근할 수 있는 진정한 private 변수가 됩니다. 이는 의도치 않은 상태 변경을 막아 코드의 안정성을 높여줍니다.
클로저는 고차 함수와 함께 사용될 때 그 진가를 발휘합니다. 특히 함수의 재사용성을 높이는 커링 패턴에서 핵심적으로 사용됩니다.
// 특정 값에 더하는 함수를 생성하는 함수
function createAdder(x) {
return function(y) {
return x + y;
};
}
const add5 = createAdder(5); // x가 5인 클로저 생성
const add10 = createAdder(10); // x가 10인 클로저 생성
console.log(add5(2)); // 7
console.log(add10(2)); // 12
createAdder 함수는 실행된 후에도 자신의 인자 x를 "기억하는" 새로운 함수를 반환합니다. 이처럼 클로저를 이용하면 특정 설정을 미리 적용해 둔 함수를 동적으로 만들어낼 수 있어 코드의 유연성과 재사용성이 크게 향상됩니다.
클로저는 유용하지만, 잘못 사용하면 메모리 누수를 유발할 수 있습니다. 클로저가 참조하는 외부 스코프는 가비지 컬렉션(GC)의 대상이 되지 않기 때문입니다.
해결책:
null을 할당하여 가비지 컬렉터가 메모리를 회수할 수 있도록 해야 합니다.removeEventListener를 사용하여 명시적으로 이벤트 리스너를 제거하는 습관을 들이는 것이 매우 중요합니다.클로저는 자바스크립트의 강력한 기능이지만, 그 동작 원리와 잠재적인 단점을 정확히 이해하고 사용해야 합니다. 질문 주신 내용들은 클로저를 제대로 사용하기 위해 반드시 짚고 넘어가야 할 핵심적인 부분들입니다. 도움이 되셨기를 바랍니다.
좋은 질문입니다! 저는 클로저를 주로 한 번만 실행되어야 하는 초기화 코드를 작성할 때 유용하게 사용합니다.
예를 들어, 어떤 설정 값을 API로부터 딱 한 번만 받아와서 애플리케이션 전역에서 사용하고 싶을 때가 있습니다. 이 때 클로저를 활용하면 간단하게 구현할 수 있습니다.
const getAppConfig = (() => {
let config = null;
const initialize = async () => {
// API 호출을 흉내 냅니다.
console.log('설정 값을 초기화합니다...');
await new Promise(resolve => setTimeout(resolve, 1000));
return { theme: 'dark', version: '1.0.2' };
};
return async () => {
if (config) {
console.log('캐시된 설정 값을 반환합니다.');
return config;
}
config = await initialize();
return config;
};
})();
// 사용 예시
async function main() {
const config1 = await getAppConfig(); // "설정 값을 초기화합니다..." 출력 후 1초 뒤 객체 반환
console.log(config1);
const config2 = await getAppConfig(); // "캐시된 설정 값을 반환합니다." 출력 후 즉시 객체 반환
console.log(config2);
}
main();
위 코드에서 getAppConfig는 즉시 실행 함수 표현식(IIFE)을 통해 단 한 번만 생성됩니다. 이 함수는 내부적으로 config라는 변수를 클로저에 가두어 둡니다.
config는 null이므로 initialize 함수를 실행하여 설정 값을 받아온 후 config에 저장합니다.config에 이미 값이 있으므로 초기화 로직을 건너뛰고 저장된 값을 즉시 반환합니다.이처럼 클로저를 활용하면 상태를 안전하게 숨기면서, 특정 로직이 최초 한 번만 실행되도록 보장하는 코드를 깔끔하게 작성할 수 있습니다. 이런 패턴을 Lazy Initialization 이라고도 부릅니다.