우선 클로저와 연관된 주요 용어를 정리해보자.
- 중첩 함수: 다른 함수 안에 정의된 함수로, 내부 함수가 외부 함수의 변수에 액세스할 수 있다.
- 클로저: 함수와 함수가 생성된 환경의 조합으로, 외부 함수가 실행을 완료한 후에도 함수가 외부(둘러싸는) 범위의 변수에 액세스할 수 있도록 한다.
- 비공개(private) 변수 및 메서드: 객체 또는 함수 외부에서 직접 액세스할 수 없지만 객체 또는 함수 내에서 정의된 함수가 액세스할 수 있는 변수 및 메서드를 만드는 기법이다.
- 클로저 제거: 경우에 따라 메모리를 확보하거나 원치 않는 부작용을 방지하기 위해 클로저를 제거해야 할 수도 있다. 클로저 변수를 null 또는 정의되지 않은 것으로 설정하거나, 클로저를 참조하는 모든 이벤트 리스너를 제거하거나, 클로저가 포함된 프로퍼티를 삭제하거나, 위크맵(WeakMap)을 사용하여 클로저를 저장하는 등의 방법으로 클로저를 제거할 수 있습니다.
- 객체 지향 프로그래밍(OOP) 에뮬레이션: OOP에 대한 기본 지원이 없는 언어에서 객체 지향 개념을 구현하는 기술이다.
이러한 개념을 염두에 두고 각 개념을 좀 더 자세히 살펴보자.
중첩 함수(Nested Function)
중첩 함수는 부모 범위의 변수에 액세스할 수 있는 함수를 만들 수 있는 자바스크립트의 강력한 기능이다다. 특정 컨텍스트에 한정된 헬퍼 함수를 만들 때 주로 사용되며 전역 스코프에 노출될 필요가 없다. 예를 들어 다음 코드를 살펴보자.
function outerFunction() {
const message = 'Hello, world!';
function innerFunction() {
console.log(message);
}
innerFunction();
}
outerFunction(); // Output: "Hello, world!"
이 예제에서 innerFunction은 outerFunction 내에 정의되어 있으며, 이는 외부 함수에 정의된 메시지 변수에 액세스할 수 있다는 것을 의미한다. outerFunction이 호출되면 내부 함수가 차례로 호출되어 메시지 값을 콘솔에 기록한다.
클로저(Closure)
클로저는 중첩된 함수와 밀접한 관련이 있지만, 한 단계 더 나아가 부모 함수가 실행을 완료한 후에도 함수가 부모 범위의 변수를 “기억”할 수 있도록 한다. 이 작업은 함수와 부모 범위를 모두 포함하는 새 환경을 만든 다음 클로저를 만드는 데 사용하는 방식으로 수행된다. 예를 들어,
function outerFunction() {
const message = 'Hello, world!';
function innerFunction() {
console.log(message);
}
return innerFunction;
}
const myFunction = outerFunction();
myFunction(); // Output: "Hello, world!"
이 예제에서 outerFunction는 innerFunction를 반환하고, 이 함수는 myFunction에 할당된다. myFunction이 호출되면 outerFunction가 실행을 완료했더라도 message의 값을 콘솔에 기록한다. 이는 innerFunction이 부모 스코프에서 message 변수를 포함하는 클로저를 형성하기 때문이다.
비공개 변수와 메서드는 객체 또는 함수 내에서 기능을 캡슐화하고 해당 컨텍스트 외부에서 직접 액세스하지 못하도록 하는 데 유용한 기술이다. 이는 종종 클로저를 사용하여 객체 또는 함수 내에서만 액세스할 수 있는 비공개 변수를 생성하여 수행된다. 예를 들어,
function createCounter() {
let count = 0;
function increment() {
count++;
console.log(count);
}
return {
increment: increment
};
}
const counter = createCounter();
counter.increment(); // Output: 1
counter.increment(); // Output: 2
이 예제에서 createCounter는 클로저 내에서만 액세스할 수 있는 비공개 count 변수를 증가시키는 데 사용할 수 있는 단일 메서드인 increment를 가진 객체를 반환한다.
클로저 제거
특정 상황에서는 메모리 누수나 기타 원치 않는 부작용을 방지하기 위해 클로저를 제거해야 할 수 있다. 클로저 변수를 null
또는 undefined
으로 설정하거나, 클로저를 참조하는 모든 이벤트 리스너를 제거하거나, 클로저가 포함된 프로퍼티를 삭제하거나, 클로저를 저장하기 위해 WeakMap을 사용하는 등의 방법으로 이 작업을 수행할 수 있다.
우선, 클로저 변수를 null
또는 undefined
으로 설정해서 제거하는 방법의 예를 살펴 보자.
function createCounter() {
let count = 0;
return function increment() {
count++;
console.log(count);
};
}
const incrementCounter = createCounter();
// Later, when you want to remove the closure variable:
incrementCounter = null;
이 예제에서 createCounter는 비공개 카운트 변수와 이를 증가시키는 함수를 포함하는 클로저를 반환한다. 이 클로저를 incrementCounter 변수에 할당하면 카운터를 증가시키는 데 사용할 수 있다.
클로저 변수를 제거하려면 incrementCounter를 null로 설정하기만 하면 된다. 이렇게 하면 createCounter에 의해 생성된 클로저에 대한 참조가 효과적으로 제거되어 클로저가 사용하는 메모리를 가비지 콜렉터가 해제할 수 있다.
두번째는 , 이벤트 리스너를 제거하는 경우이다.
function createEventListener() {
const message = 'Hello, world!';
function handleClick() {
console.log(message);
}
const button = document.getElementById('myButton');
button.addEventListener('click', handleClick);
return function removeEventListener() {
button.removeEventListener('click', handleClick);
};
}
const removeListener = createEventListener();
// Later, when you want to remove the listener:
removeListener();
이 예제에서 createEventListener는 메시지 변수와 버튼 요소의 클릭 이벤트 리스너를 포함하는 클로저를 생성한다. 이 함수는 호출 시 이벤트 리스너를 제거하는 데 사용할 수 있는 함수를 반환한다.
클로저를 제거하려면 createEventListener가 반환하는 함수를 호출하여 버튼 요소에서 이벤트 리스너를 제거한다. 이렇게 하면 createEventListener에 의해 생성된 클로저가 효과적으로 제거되어 클로저가 사용하는 메모리를 가비지 콜렉터가 해제할 수 있다.
다음으로는 클로저가 포함된 프로퍼티를 삭제하여 클로저를 제거하는 경우이다.
const obj = {
message: 'Hello, world!',
};
obj.handleClick = function () {
console.log(this.message);
};
// Later, when you want to remove the closure:
delete obj.handleClick;
이 예제에서는 속성 메시지와 클로저 handleClick으로 객체 객체를 정의한다. 클로저는 obj.handleClick에 새 함수를 할당할 때 생성됩니다. 이를 통해 객체의 message 프로퍼티에 대한 참조가 포함된다.
클로저를 제거하려면 delete obj.handleClick을 호출하여 객체에서 handleClick 프로퍼티를 삭제하면 된다. 이렇게 하면 핸들클릭에 할당된 함수에 의해 생성된 클로저에 대한 참조가 효과적으로 제거되어 클로저가 사용하는 메모리를 가비지 콜렉터가 해제할 수 있다.
마지막으로 WeakMap을 이용해서 클로저를 제거할 수 있는 방법이다.
const closureMap = new WeakMap();
function createClosure() {
const message = 'Hello, world!';
function logMessage() {
console.log(message);
}
closureMap.set(logMessage, true);
return logMessage;
}
const myClosure = createClosure();
// Later, when you want to remove the closure:
closureMap.delete(myClosure);
이 예제에서는 closureMap이라는 WeakMap과 클로저를 반환하는 createClosure 함수를 생성한다. 클로저를 키로 하고 값을 true로 설정한 closureMap.set(logMessage, true)를 사용하여 클로저를 클로저맵에 저장한다.
클로저를 제거하려면 클로저를 인수로 하여 closureMap.delete(myClosure)를 호출한다. 이렇게 하면 클로저에 대한 참조가 WeakMap에서 효과적으로 제거되어 클로저가 사용하던 메모리를 가비지 컬렉터가 해제할 수 있다.
WeakMap을 사용하여 클로저를 저장할 때의 한 가지 장점은 WeakMap의 키가 약하게 유지되기 때문에 더 이상 필요하지 않을 때 WeakMap이 클로저에 대한 참조를 자동으로 제거한다는 것이다. 이렇게 하면 클로저를 수동으로 제거할 수 없는 경우 메모리 누수를 방지하는 데 도움이 될 수 있다.
OOP 에뮬레이션
OOP 에뮬레이션은 OOP를 기본적으로 지원하지 않는 언어에서 객체 지향 개념을 구현하는 기술이다. 이는 종종 클로저와 팩토리 함수를 사용하여 개인 변수와 메서드가 있는 객체를 생성하는 방식으로 이루어진다. 예를 들어,
function createPerson(name) {
let age = 0;
function getAge() {
return age;
}
function setAge(newAge) {
age = newAge;
}
return {
name: name,
getAge: getAge,
setAge: setAge
};
}
const person = createPerson('Alice');
person.setAge(30);
console.log(person.name); // Output: "Alice"
console.log(person.getAge()); // Output: 30
이 예제에서 createPerson은 name 속성과 클로저 내의 비공개 age 변수에 액세스하는 getAge 및 setAge 메서드가 있는 객체를 반환한다.