책을 읽은 지는 꽤 지났지만, 다시 한번 복습 겸 기록을 남겨보려고 합니다. 책을 읽고 요약해 정리한 내용의 전문은 제 개인 노션에 정리해 놓았습니다.
이전 글 : [FRONT-END/JavaScript] - 04. 콜백 함수 (코어 자바스크립트)
클로저(Closure)
클로저의 정의
클로저란 어떤 함수 A에서 선언한 변수 b를 참조하는 내부함수 C를 외부로 전달한 경우 A의 실행 컨텍스트가 종료된 이후에도 변수 b가 사라지지 않는 현상을 의미합니다.
함수와 그 함수가 선언될 당시의 lexical environment의 상호 관계의 따른 현상이라고 MDN에서는 얘기하는데요. 선언될 당시 lexical environment는 outerEnvironment에 해당합니다.
실행 컨텍스트 학습 내용을 다시 상기시켜 생각해 보면 외부 함수의 실행이 종료되면 외부함수의 실행 컨텍스트는 콜스택에서 제거되지만, 내부함수가 외부함수의 실행 컨텍스트를 참조하고 있기 때문에 변수가 메모리에서 유지되어 접근이 가능해지는 거죠. 리턴된 내부 함수를 새로운 변수에 저장할 때는 그 변수는 내부 함수의 실행 컨텍스트를 참조하기 때문에 내부함수의 실행 컨텍스트도 사라지지 않고 유지되는 겁니다.
var outer = function () {
var a = 1;
var inner = function () {
return ++a;
}
return inner;
}
var outer2 = outer()
console.log(outer2()) // 2
console.log(outer2()) // 3
위 코드로 다시 보면 outer 함수가 inner 함수를 return하는데, inner 함수 내 outer 함수의 a라는 변수를 참조하고 있죠. 그래서 outer 함수의 실행 컨텍스트가 종료되더라도 outer2를 호출할 때마다 계산된 값을 return 하는 것을 확인할 수 있습니다.
이는 자바스크립트의 가비지 컬렉터의 동작 방식 때문입니다. 가비지 컬렉터는 어떤 값을 참조하는 변수가 하나라도 있다면 그 값은 수집대상에 포함시키지 않습니다.
위 예시코드 말고 다른 예시도 보면서 익혀봅시다.
외부로 전달이 꼭 return을 의미하는 것이 아니라는 것을 이해할 수 있게 됩니다.
// (1) setInterval / setTimeout
(function () {
var a = 0
var intervalId = null
var inner = function() {
if (++a >= 10) {
clearInterval(interverId)
}
console.log(a)
}
intervalId = setInterval(inner, 1000)
})()
// (2) eventListener
(function() {
var count = 0
var button = document.createElement('button')
button.innerText = 'click'
button.addEventListener('click', function() {
console.log(++count, 'times clicked')
})
document.body.appendChild(button)
})()
위 코드를 보면 setInteval, setTimeout, eventListener에 의해 콜백함수로 전달되는 경우도 있다는 것을 확인할 수 있습니다!
클로저와 메모리 관리
그럼 클로저를 사용하면 무엇이 좋을까요? 언제 사용하면 좋을까요?
클로저를 활용하면 데이터 은닉과 캡슐화가 가능합니다. 즉, 접근 권한을 제어하고자 할 때 사용하면 좋습니다.
클로저를 이용하면 함수 차원에서 public한 값과 private 한 값을 구분하는 것이 가능합니다.
즉, 외부에 제공하고자 하는 정보들을 모아 return하고 내부에서만 사용할 정보들은 return 하지 않는 것으로 접근 권한 제어가 가능합니다.
함수 팩토리 생성이 가능합니다. 즉, 부분 적용 함수를 구현하고자 할 때 사용하면 좋습니다.
함수 팩토리는 다른 함수를 생성하고 반환하는 함수를 의미합니다.
반환된 함수는 팩토리 함수의 스코프에 접근할 수 있는데 이 부분이 클로저인 거죠!
실무에서 부분 함수를 사용하기 적합한 예로는 디바운스가 있습니다. (scroll, wheel, mouse move, resize 등)
디바운스는 짧은 시간 동안 동일한 이벤트가 많이 발생할 경우 이를 전부 처리하지 않고 처음 또는 마지막에 발생한 이벤트에 대해 한 번만 처리하는 것으로 성능 최적화에 도움을 주는 기능 중 하나입니다.
var debounce = function(eventName, func, wait) {
var timeoutId = null;
return function(event) {
var self = this;
console.log(eventName, 'event 발생');
clearTimeout(timeoutId);
timeoutId = setTimeout(func.bind(self, event), wait);
};
};
var moveHandler = function(e) {
console.log('move event 처리');
};
document.body.addEventListener('mousemove', debounce('move', moveHandler, 500));
비동기 처리에도 유용합니다. 클로저를 활용하면 비동기 작업의 상태를 안전하게 관리하고 추적할 수 있습니다. 즉, 비동기 작업의 완료 여부나 결과값을 외부에서 쉽게 확인하고 사용할 수 있게 됩니다.
function fetchData(url) {
return new Promise((resolve, reject) => {
// 실제 환경에서는 여기에 fetch나 XMLHttpRequest를 사용할 것입니다.
setTimeout(() => {
resolve("Data from " + url);
}, 1000);
});
}
function processData(urls) {
urls.forEach((url, index) => {
fetchData(url).then((data) => {
console.log(`Data ${index + 1}: ${data}`);
});
});
}
processData(["http://api1.com", "http://api2.com", "http://api3.com"]);
// 약 1초 후 출력:
// Data 1: Data from http://api1.com
// Data 2: Data from http://api2.com
// Data 3: Data from http://api3.com
forEach 루프 내의 콜백 함수는 클로저입니다. 각 콜백은 자신의 url과 index를 기억하고 있어 비동기 작업이 완료된 후에도 올바른 데이터를 출력할 수 있게 됩니다.
함수 팩토리, 부분 함수, 커링 함수 용어 정리
부분 함수, 함수 팩토리, 커링 함수는 비슷한 개념이지만 약간의 차이가 있습니다.
함수 팩토리 (Function Factory):
- 다른 함수를 반환하는 함수를 말합니다.
- 새로운 함수를 생성하고 반환하는 함수로, 동적으로 함수를 만들어낼 때 사용합니다.
부분 함수 (Partial Function):
- 함수의 일부 인자를 미리 고정시켜 새로운 함수를 만드는 기법입니다.
- 원래 함수의 일부 매개변수를 고정하고 나머지는 나중에 채울 수 있게 합니다.
커링 함수 (Currying):
- 여러 개의 인자를 받는 함수를 하나의 인자만 받는 함수들의 체인으로 바꾸는 기법입니다.
- 함수를 부분적으로 실행할 수 있게 해주며, 인자를 하나씩 채워나가는 방식으로 동작합니다.
- 함수 팩토리는 가장 넓은 개념으로, 다른 함수를 반환하는 모든 함수를 포함합니다.
- 부분 함수와 커링은 함수 팩토리의 특수한 형태로 볼 수 있습니다.
- 커링은 부분 함수 적용의 특별한 경우로 볼 수 있습니다. 커링은 항상 하나의 인자만 받는 함수 체인을 만들지만, 부분 함수는 여러 인자를 한 번에 고정할 수 있습니다.
클로저에서 주의해야 한다면 어떤 게 있을까요?
메모리 누수, 성능, 코드 복잡성 증가를 얘기할 수 있습니다.
참조되고 있어 가비지 컬렉터가 수집하지 않기 때문에 메모리를 계속 사용하게 됩니다. 그래서 메모리 누수가 일어날 수 있습니다.
하지만 포인트는 메모리 누수라는 표현은 개발자의 의도와 달리 어떤 값의 참조 카운트가 0이 되지 않아 가비지 컬렉터의 수거 대상이 되지 않는 경우에 맞는 표현입니다. 그러니 메모리 누수가 일어날 수 있어 주의해야 하는 포인트지 단점은 아니라고 할 수 있습니다.
클로저는 스코프 체인을 통해 변수를 찾아 과도한 사용은 성능에 영향을 줄 수 있습니다.
그리고 자칫 잘못 사용하면 코드의 복잡성을 증가시킬 수 있습니다.
그럼 어떻게 관리할 수 있을까요?
참조 카운트를 0으로 만들면 됩니다. 그 방법은 식별자에 기본형 데이터 (null, undefined)를 할당하면 됩니다.
// (1) return에 의한 클로저의 메모리 해제
var outer = (function () {
var a = 1;
var inner = function () {
return ++a;
}
return inner
})()
console.log(outer());
outer = null // outer 식별자의 inner 함수 참조를 끊음
// (2) setInterval에 의한 클로저의 메모리 해제
(function () {
var a = 0
var intervalId = null
var inner = function() {
if (++a >= 10) {
clearInterval(interverId)
inner = null // inner 식별자의 함수 참조를 끊음
}
console.log(a)
}
intervalId = setInterval(inner, 1000)
})()
// (3) eventListener에 의한 클로저의 메모리 해제
(function() {
var count = 0
var button = document.createElement('button')
button.innerText = 'click'
var clickHandler = function() {
console.log(++count, 'times clicked')
if (count >= 10) {
button.removeEventLister('click', clickHandler)
clickHandler = null // clickHandler 식별자의 함수 참조 끊기
}
})
document.body.appendChild(button)
})()
그 외로는 즉시 실행함수를 활용해 클로저의 생명주기를 제한하는 방법, WeakMap이나 WeakSet을 사용해 객체에 대한 약한 참조를 만들 수 있습니다.
다음 글 : [FRONT-END/JavaScript] - 06. 프로토타입 (코어 자바스크립트)
'FRONT-END > JavaScript' 카테고리의 다른 글
07. 클래스 (코어 자바스크립트) (1) | 2024.09.24 |
---|---|
06. 프로토타입 (코어 자바스크립트) (0) | 2024.09.23 |
04. 콜백 함수 (코어 자바스크립트) (1) | 2024.09.22 |
03. this (코어 자바스크립트) (1) | 2024.09.22 |
02. 실행 컨텍스트 (코어 자바스크립트) (0) | 2024.09.21 |
댓글