안녕하세요! JavaScript의 강력하고도 때로는 이해하기 까다로운 개념 중 하나인 클로저(Closure)에 대해 심도 있게 다뤄보겠습니다. 클로저는 JavaScript 개발자라면 반드시 마스터해야 할 핵심 개념이며, 이를 통해 더 유연하고 효율적인 코드를 작성할 수 있습니다. 함께 클로저의 세계로 떠나볼까요?
1. 클로저란 무엇인가?
MDN Web Docs에서는 클로저를 다음과 같이 정의합니다:
"클로저는 함수와 함수가 선언된 어휘적(lexical) 환경의 조합입니다."
쉽게 말해, 클로저는 **외부 함수의 실행이 종료된 후에도 내부 함수가 외부 함수의 스코프(Lexical Environment)에 접근할 수 있는 현상**을 의미합니다. 내부 함수는 자신이 선언될 때의 환경을 기억하고 있다가, 외부 함수가 사라진 뒤에도 그 환경 속의 변수들에 접근하여 사용할 수 있습니다.
클로저의 기본적인 형태
function outerFunction() {
const outerVariable = '저는 외부 함수의 변수입니다.';
function innerFunction() { // innerFunction이 클로저가 될 수 있습니다.
console.log(outerVariable); // 내부 함수가 외부 함수의 변수에 접근
}
return innerFunction; // 내부 함수를 반환
}
const myClosure = outerFunction(); // outerFunction 실행 후 반환된 innerFunction이 myClosure에 할당
myClosure(); // "저는 외부 함수의 변수입니다." 출력
위 코드에서 outerFunction
이 실행을 마친 후에도, innerFunction
은 여전히 outerVariable
에 접근할 수 있습니다. 이는 innerFunction
이 outerVariable
이 선언된 '어휘적 환경'을 기억하고 있기 때문입니다.
2. 클로저의 핵심 원리: 어휘적 환경 (Lexical Environment)
클로저를 이해하려면 '어휘적 환경'에 대한 이해가 필수적입니다. 어휘적 환경이란 코드가 작성된 물리적인 위치(소스코드 내 위치)에 따라 스코프가 결정되는 것을 의미합니다. 함수가 정의되는 순간, 그 함수는 자신이 정의된 스코프의 환경을 '기억'합니다.
let globalVar = '글로벌 변수';
function outer() {
let outerVar = '외부 함수 변수';
console.log(globalVar); // outer의 어휘적 환경: 전역 스코프 포함
function inner() {
let innerVar = '내부 함수 변수';
console.log(outerVar); // inner의 어휘적 환경: outer의 스코프 포함
console.log(globalVar); // inner의 어휘적 환경: 전역 스코프 포함
}
return inner;
}
const myInner = outer();
myInner(); // outerVar, globalVar에 접근 가능
inner
함수는 outer
함수 내부에 정의되었으므로, outer
함수의 스코프를 자신의 어휘적 환경으로 포함합니다. 그리고 outer
함수는 전역 스코프를 포함합니다. 이처럼 함수가 중첩될 때 각 함수는 외부 스코프에 대한 참조를 유지하며, 이것이 클로저의 기반이 됩니다.
3. 클로저의 다양한 활용 사례
3.1. private 변수 구현 (정보 은닉)
클로저를 이용하면 특정 변수를 외부에서 직접 접근하지 못하게 하면서, 특정 함수를 통해서만 접근하도록 만들 수 있습니다. 이는 객체 지향 프로그래밍의 캡슐화(Encapsulation)와 유사한 패턴입니다.
function createCounter() {
let count = 0; // 이 변수는 외부에서 직접 접근 불가 (private)
return {
increment: function() {
count++;
return count;
},
decrement: function() {
count--;
return count;
},
getCount: function() {
return count;
}
};
}
const counter = createCounter();
console.log(counter.increment()); // 1
console.log(counter.increment()); // 2
console.log(counter.getCount()); // 2
// console.log(counter.count); // undefined (직접 접근 불가)
count
변수는 createCounter
함수 내부에서만 선언되었기 때문에 외부에서는 직접 접근할 수 없습니다. 오직 반환된 객체 안의 메서드를 통해서만 조작이 가능하여 정보 은닉이 이루어집니다.
3.2. 함수 팩토리 (Function Factory)
특정 설정을 기억하는 함수를 생성하는 팩토리 함수로 클로저를 활용할 수 있습니다.
function multiplyBy(factor) {
return function(number) { // factor를 기억하는 클로저
return number * factor;
};
}
const multiplyBy2 = multiplyBy(2); // 2를 기억하는 함수 생성
const multiplyBy10 = multiplyBy(10); // 10을 기억하는 함수 생성
console.log(multiplyBy2(5)); // 10
console.log(multiplyBy10(5)); // 50
multiplyBy
함수는 factor
값을 기억하는 새로운 함수를 반환하며, 이 함수들은 각각 고유한 factor
값을 가지고 동작합니다.
3.3. 모듈 패턴 (Module Pattern) 구현
JavaScript에서 모듈을 구현할 때 클로저를 활용하여 전역 스코프 오염을 방지하고 코드의 응집도를 높일 수 있습니다.
const calculator = (function() { // IIFE로 클로저 생성
let privateResult = 0; // private 변수
function add(num) {
privateResult += num;
return privateResult;
}
function subtract(num) {
privateResult -= num;
return privateResult;
}
return { // 공개 API
add: add,
subtract: subtract,
getCurrentResult: function() {
return privateResult;
}
};
})();
console.log(calculator.add(10)); // 10
console.log(calculator.subtract(3)); // 7
console.log(calculator.getCurrentResult()); // 7
// console.log(calculator.privateResult); // undefined
privateResult
는 외부에서 직접 접근할 수 없으며, add
, subtract
, getCurrentResult
메서드를 통해서만 조작 및 조회가 가능합니다. 이는 강력한 모듈화 기능을 제공합니다.
4. 클로저 사용 시 주의할 점
클로저는 강력하지만, 오용 시 메모리 누수를 유발하거나 예상치 못한 동작을 할 수 있습니다.
- 메모리 관리: 클로저는 외부 스코프의 변수를 계속 참조하기 때문에, 클로저가 더 이상 필요 없을 때 가비지 컬렉션이 되지 않아 메모리 누수가 발생할 수 있습니다. 특히, DOM 엘리먼트를 참조하는 클로저를 사용할 때는 주의가 필요합니다.
- 변수의 캡처 방식 이해: 클로저는 변수의 '값'이 아니라 '참조'를 기억합니다. 따라서 클로저가 실행될 시점에 외부 변수의 값이 변경되어 있다면, 클로저는 변경된 값을 참조하게 됩니다. (
let
과var
의 차이에서 두드러짐)
for (var i = 1; i <= 3; i++) {
setTimeout(function() {
console.log(i); // 4, 4, 4 (i의 최종 값)
}, i * 1000);
}
for (let j = 1; j <= 3; j++) {
setTimeout(function() {
console.log(j); // 1, 2, 3 (각 반복마다 j가 새로운 스코프를 가짐)
}, j * 1000);
}
var
는 함수 스코프를 가지므로 클로저가 i
의 최종 값(4)을 기억하지만, let
은 블록 스코프를 가지므로 각 반복마다 새로운 j
변수가 생성되어 올바른 값을 기억합니다.
결론: 클로저는 JavaScript 개발의 필수 요소
클로저는 JavaScript의 스코프 체인과 메모리 관리를 이해하는 데 있어 가장 중요한 개념 중 하나입니다. 정보 은닉, 함수 팩토리, 모듈 패턴 구현 등 다양한 고급 패턴에 활용되며, 코드의 재사용성과 유지보수성을 높여줍니다. 클로저를 정확히 이해하고 올바르게 활용한다면, 더욱 견고하고 효율적인 JavaScript 애플리케이션을 개발할 수 있을 것입니다.
이 포스팅이 클로저에 대한 이해를 돕는 데 유용했기를 바랍니다. 클로저에 대해 궁금한 점이나 추가하고 싶은 내용이 있다면 언제든지 댓글로 남겨주세요!
댓글 없음:
댓글 쓰기