우선, MDN
에 있는 정의를 볼까요?
(실제로 저는 클로저를 묻는다면 MDN을 기준으로 설명하긴 합니다. 그만큼 깔끔하다는 거죠!)
"클로저는 함수와 그 함수가 선언된 렉시컬 환경의 조합이다"
사실 정말 이게 다입니다.
렉시컬 스코프라는 것을 잘 이해하고 있다면, 그저 이러한 참조에서 발생하는 현상이라고 이해해도 무방할 정도로 말이죠.
결국 렉시컬 환경을 생각해 보면, 상위 렉시컬 환경을 계속해서 참조하죠?
그리고, 원하는 식별자 값이 나올 때까지 계속해서 탐색을 하게 되는 거에요.
그런데 이것이 만약 함수 호출과 만나면 어떻게 될까요?
예컨대, 다음과 같이 말이죠.
const x = 1;
function foo() {
const x = 10;
function bar() {
console.log(x);
}
return bar;
}
console.log(foo()()) // 10
겉보기에는, 이상이 없어 보이지만, 실제로 해석하면 실행 컨텍스트 상에서 의아한 부분이 발생합니다!
우선 한 번 실행 컨텍스트 상에서 해석을 해볼까요?
this
바인딩foo
함수 실행 컨텍스트 생성this
바인딩, 외부 렉시컬 환경 참조 결정)inner
함수 객체 반환하며 foo
함수 실행 컨텍스트 종료bar
함수 실행 컨텍스트 생성this
바인딩, 외부 렉시컬 환경 참조 결정)bar
함수 실행 컨텍스트 종료우리가 살펴볼 부분은, 바로 9번이에요.
💡 이미 실행 컨텍스트가 존재하는 경우,
foo
의x
값이 어떻게 참조될 수 있는 거죠?!
우리가 이렇게 코드에게 묻자, 코드는 안도의 한 숨을 내쉬며 이렇게 말합니다.
🙆🏻 우리는 외부 렉시컬 환경을 기억하고 있어요. 우리의 상위 렉시컬 환경은
foo
여서, 다행히 찾을 수 있었네요!
네. 결국 상위 스코프를 기억하고 있기 때문에 실행 컨텍스트가 종료 되었음에도 중첩 함수가 이를 기억하는 현상, 이것이 바로 클로저에요!
우리가 흔히 착각을 하는 게 있는데요!
실행 컨텍스트와 가비지 컬렉션의 주기는 일치하지 않아요.
만약 가비지 컬렉션이 상위 스코프에 해당되는 값을 지워버렸다면? 클로저가 발생하지 않겠죠.
즉, 가비지 컬렉션은 누군가가 참조하고 있다면 함부로 메모리를 해제하지 못해요.
즉, 중첩함수가 계속 살아남아 있다면, 상위 렉시컬 환경인 foo
함수의 실행 컨텍스트는, 소멸하지 않게 되는 겁니다. 왜냐구요? bar
함수 렉시컬 환경의 [[Environment]]
가 참조하고 있거든요. 😉
항상 직접적으로 어떤 변수를 핸들링할 수 있다는 것은, 언제든지 위험에 노출될 수 있다는 점을 의미합니다.
따라서, 클로저는 이러한 상태 값들을 은닉화하고 캡슐화(관련 프로퍼티, 메서드를 한데 모아 정보를 감춘다는 의미에서)시킴으로써, 결과적으로 코드의 안정성을 높여주는 역할을 한답니다 🥰
자바스크립트에서 함수는 일급 객체이며, 따라서 함수는 매개변수를 함수로 받는 고차 함수가 될 수 있습니다.
이때 함수를 인자로 받았을 때, 인자인 함수가 외부를 기억한다는 것은, 곧 다양하게 해당 함수를 재사용할 수 있다는 이점이 생깁니다!
따라서, 이러한 고차 함수를 응용하여, 불변성을 유지한채로 다양한 순수함수를 조합하여 결과 값을 도출해내는 함수형 프로그래밍이 가능해진다는 것이 또다른 이점이에요. 🥰
Debounce, Throttle
디바운스와 쓰로틀은 클로저의 전형적인 예라고 저는 자부합니다.
이 친구들은 이벤트들의 호출을 최적화하는 유틸 함수에요!
const debounce = (cb: Function, delay = 300) => {
let timerFunc: null | NodeJS.Timeout = null;
return (e: Event) => {
if (timerFunc) clearTimeout(timerFunc);
timerFunc = setTimeout(cb, delay, e) as unknown as NodeJS.Timeout;
};
};
const throttle = (cb: Function, delay = 300) => {
let timerFunc: null | NodeJS.Timeout = null;
return (...args: any) => {
if (timerFunc) return;
timerFunc = setTimeout(() => {
cb(...args);
timerFunc = null;
}, delay)
}
}
여기서 주목할 것은 timerFunc
죠.
저 친구가 만약 전역에서 살아 있었다면, 저 값을 실수로 접근할 수도 있게될 뿐더러, 전역 네임스페이스가 오염됩니다.
그러나, 저렇게 함수 내부에 존재함으로써, 이제 안정성이 높아지게 되는 것이죠!
또한, cb
를 인자로 받는 고차 함수로 존재함으로써, 이후 다양한 콜백 함수를 전달해도 무방할 수 있으므로 재사용성이 높습니다!
사실 이 책에는 좀 더 많은 예시가 있는데, 제 생각에는 더 많은 이야기를 하는 것이 오히려 혼동을 줄 수 있을 거 같아, 핵심만 정리하고 끝내려 합니다!
결국 클로저는 실행 컨텍스트의 종료에도, 중첩 함수가 상위 렉시컬 환경을 기억하는 현상이란 것만 잘 이해하면 되는 것 같아요.
다만, 나중에 좀 더 레벨이 올라가면, 이러한 클로저를 바탕으로 은닉화와, 재사용성 높은 프로그래밍을 할 수 있기에, 아무리 공부해도 지나치지 않습니다! 👏🏻
그럼, 다들 재밌는 공부 하시길 바라며. 이상!