5 Closure
5.1 클로저의 원리 및 이해
- MDN 에서는 클로저는 "함수와 그 함수가 선언될 당시의 lexical environment의 상호관계(combination)에 따른 현상 이라고 표현하고있다.
- '선언될 당시의 lexical environment는 실행 컨텍스트중 하나인 outerEnvironmentReference에 해당한다.
- lexicalEnvironment의 environmentRecord와 outerEnvironmentReference에 의해 변수의 유효범위인 스코프가 결정되고 스코프 체인이 가능해진다고 이미 설명한 바 있다.
- 위에서 의미하는 combination이란, 내부함수에서 외부변수를 참조하는 경우에 한해서만 combination, 즉 '선언될 당시의 LexicalEnvironment와의 상호관계' 이다.
var outer = function () {
var a = 1;
var inner = function() {
return ++a;
}
return inner();
}
var outer2 = outer();
console.log(outer2); // 2
console.log(outer2); // 2
- 위의 예제를 살펴보면, outer 내부의 inner function이 outer의 a식별자를 참조하고있다. 그러나 특별한 현상은 안보인다.
- 그러나 outer 함수의 실행컨텍스트가 종료된 시점에서는 a변수를 참조하는 대상이 없어진다.
- a, (inner 변수의 값)들은 언젠가 가비지컬렉터에 의해 소멸될 것이다.
- 만약 outer의 실행 컨텍스트가 종료된 후에도 inner 함수를 호출할 수 있게 만들면 어떻게 될까??
var outer = function () {
var a = 1;
var inner = function() {
return ++a;
}
return inner; // ** 함수의 실행결과가 아닌 함수의 주소 자체를 리턴
}
var outer2 = outer();
console.log(outer2()); // 2
console.log(outer2()); // 3
- inner 함수의 실행 결과가 아니라 inner 함수 자체를 반환했다.
- 결과 또한, a가 2,3 이전 값을 기억 했다.
- inner 함수의 실행 시점에는 outer 함수는 이미 실행종료된 상태인데, outer 함수의 LexicalEnvironment에 어떻게 접근할 수 있을까?
- 잘 알다시피, 가비지 컬렉터는 어떤 값을 참조하는 변수가 하나라도 있다면 그값은 수집대사엥 포함시키지 않는다.
- inner 함수의 실행 컨텍스트가 활성화되면 outerEnvironmnetReference가 outer 함수의 LexicalEnvironment를 필요로 할 것이므로 수집대상에서 제외된다.
[120page 그림]
(설명: 원래는 LexicalEnvironment 전부를 gc하지 않았으나, 2019년 기준으로 v8엔진에서는 내부함수에서 사용하는 변수만 남겨두고 나머지는 gc)
- 즉 "어떤함수에서 선언한 변수를 참조하는 내부함수에서만 발생하는 현상" 이란 "외부함수의 LexicalEnvironment가 가비지컬렉팅 되지 않는 현상" 을말하는 것이다.
- 이를 정리하자면 클로저란 어떤 함수 A에서 선언한 변수 a를 참조하는 내부함수 B를 외부로 전달할 경우 A의 실행컨텍스트가 종료된 이후에도 변수 a가 사라지지 않는 현상이다.
다른책에서는 Clousre를 이렇게 표현하는데 그 중 가장 이해하기 쉬운걸 소개함
- 함수를 선언할 때 만들어지는 유효범위가 사라진 후에도 호출할 수 있는 함수 - 존레식, <자바스크립트 닌자 비급>
- 이미 생명 주기가 끝난 외부 함수의 변수를 참조하는 함수 - 송형주 고현준, <인사이드 자바스크립트>
- 자신이 생성될 때의 스코프에서 알 수 있었던 변수들 중 언젠가 자신이 실행 될 때 사용할 변수들만을 기억하여유지시키는 함수 - 유인동 <함수형 자바스크립트 프로그래밍>
- 주의할점은 '외부로 전달' 이 곧 retunr 만을 의미하는 것은 아니다.
- return 없이도 클로저가 발생하는 경우
(function () {
var a = 0;
var intervalId = null;
var inner = function() {
if(++a >= 10) {
clearInterval(intervalId);
}
console.log(a);
};
intervalId = setInterval(inner, 1000);
}) ();
(function () {
var count = 0;
var button = document.createElement('button');
button.innerText = 'click';
button.addEventListenr('click', function() {
console.log(++count, 'time clicked');
});
document.body.appendChild(bttuon);
}) ();
- 함수내부에서 지역변수를 참조하고 있다. 두상황 모두 지역변수를 참조하는 내부함수를 외부에 전달했기 때문에 클로저 이다.
5.2 클로저와 메모리 관리
- 클로저는 매우 중요한 개녀이나, 일부는 메모리 누수의 위험을 이유로 클로저 사용을 지양해야 한다고 한다.
- '메모리 누수' 라는 표현은 개발자의 의도와 달리 어떤값의 참조 카운트가 0이 되지않아 GC의 수거대상이 되지 않는 경우 이다.
- 적절한 사용과 적절한 클로저의 메모리 해제만 있으면 괜찮다.
- 참조카운트를 0으로 만들면 GC의 수거대상이된다. 식별자에 참조형이 아닌 기본형 데이터 (보통 null이나 undefined)를 할당 하면 된다.
var outer = function () {
var a = 1;
var inner = function() {
return ++a;
}
return inner; // ** 함수의 실행결과가 아닌 함수의 주소 자체를 리턴
}
var outer2 = outer();
console.log(outer2()); // 2
console.log(outer2()); // 3
outer2 = null; //outer 식별자의 inner 함수 참조를 끊음
(function () {
var a = 0;
var intervalId = null;
var inner = function() {
if(++a >= 10) {
clearInterval(intervalId);
inner = null; //inner 식별자에대하여 참조를 끊음
}
console.log(a);
};
intervalId = setInterval(inner, 1000);
}) ();
(function () {
var count = 0;
var button = document.createElement('button');
button.innerText = 'click';
button.addEventListenr('click', function() {
console.log(++count, 'time clicked');
if(count >= 10) {
button.removeEnventListner('click', clickHandler);
clickHandler = null; // clickHandler 식별자의 함수 참조를 끊음.
}
});
document.body.appendChild(bttuon);
}) ();
5.3 클로저 활용 사례
5.3.1 콜백함수 내부에서 외부 데이터를 얻고자 할때.
5.3.2 정보은닉
5.3.3 부분적용함수
- debounce
- 기본적인 예제는 인터넷에서도 많으니 실무에서 활용할 수 있는예제 디바운스만 정리해보았다.
- 디바운스는 짧은 시간동안 동일한 이벤트가 많이 발생할경우, 이를 전부처리 하지않고, 처음 또는 마지막 발생한 이벤트에 대해 한번만 처리하는 것이다. (Front-End performance 개선에 많은 도움이 된다)
- scroll, wheel, mousemove,resize 그리고 최근에 검색어 자동완성 등에서 사용한 경험이있다.
var debounce = function (eventName, func, wait) {
var timerId = null;
return function (event) {
var self = this;
console.log(eventName, ' event 발생';
clearTimeout(timerId);
timerId = setTimeout(func.bind(self, event), wait);
};
};
var moveHandler = function(e) {
console.log('move event 처리');
}
documnet.body.addEventListener('mousemove', debounce('move',moveHandler, 500));
5.3.4 커링 함수
- 커링 함수 (currying function)란 여러개의 인자를 받는 함수를 하나의 인자만 받는 함수로 나눠서 순차적으로 호출될 수 있게 체인형태로 구성한 것을 말한다 (흡사 build pattern과 비슷하지만, function 으로 사용되니 좀다르긴 한듯)
- 커링은 하나의 한개의 인자만 전달하는 것을 원칙으로 한다.
var curry3 = function (func) {
return function (a) {
return function (b) {
return func(a,b);
}
}
}
var getMaxWith10 = curry3(Math.max)(10);
consoel.log(getMaxWith10(3)); // 3
console.log(getMaxWith10(20)); // 20
- 위처럼 일부 매개변수는 고정되고, 가변적인 parameter를 넘겨줄때 사용된다.
- 프로젝트 내부에서 자주 쓰이는 함수의 매개변수가 항상 비슷하고, 일부만 바뀌는 경우에 대해 이를 적용하면 유용하다.
var getHttpRequest = function (baseUrl) {
return function (path) {
return function (id) {
return fetch(baseUrl + path + '/' + id);
}
}
}
//es6로 만들면 가독성이 훨씬 높아진다.
var getHttpRequest = baseUrl => path => id => fetch(baseUrl + path + '/' + id);
- getHttpRequest라는 공통함수를 만들었다. 이를 기반으로 아래와 같이 코드 중복을 제거할 수있다.
var userInfoUrl = 'http://localhost:1234/';
var fileUrl = 'http://localhost:8574/';
// 유저 직군별 request 준비
var getUserInfo = getHttpRequest(userInfoUrl); // http://localhost:1234/
var getDeveloperInfo = getUserInfo('developer'); // http://localhost:1234/developer
var getTaInfo = getUserInfo('ta'); // http://localhost:1234/ta
// file 종류별 request 준비
var getFileInfo = getHttpRequest(fileUrl); // http://localhost:1234/
var getZipInfo = getUserInfo('zip'); // http://localhost:1234/zip
var getImageInfo = getUserInfo('image'); // http://localhost:1234/image
// 실제 get request
var zipfile = getZipInfo(123); // http://localhost:1234/zip/123
var zipfile2 = getZipInfo(456); // http://localhost:1234/zip/456
var image = getImageInfo('navi'); // http://localhost:1234/zip/navi
var image2 = getImageInfo('dog'); // http://localhost:1234/zip/dog
// ..... ommitted
- 특히 이런이유로 커링이 광범위 하게 사용되는데, Redux middleware에서도 사용된다.
// Redux middleware 'Logger'
const logger = store => next => action => {
console.log('dispatching', action);
console.log('next state', store.getState());
return next(action);
}
// Redux Middleware 'thunk'
const thunk = store => next => action => {
return typeof action === 'function'
? action(dispatch, store.getState)
: next(action);
}
- 두 미들웨어는 공통적으로 store, next, action 순서로 인자를 받는다.
- store는 한번만 생성되고 바뀌지 않고, dispatch의 의미를 가지는 next도 마찬가지지만, action은 매 요청마다 달라진다.
- 그러므로 store, next 값이 결정되면 redux 내부에서 logger 또는 thunk에 store,next를 미리 넘겨서 반환된 함수를 저장시켜놓고, 이후에는 action만 받아서 처리할 수 있게끔 한 것이다.
5.4 정리
- 클로저란 어떤 함수에서 선언한 변수를 참조하는 내부함수를 외부로 전달할 경우, 함수의 실행 컨텍스트가 종료된 후에도 해당 변수가 사라지지 않는 현상이다.
- 내부함수를 외부로 전달하는 방법에는 함수를 return하는 경우뿐 아니라 콜백으로 전달하는 경우도 포함된다.
- 클로저는 그 본질이 메모리를 계속 차지하는 개념ㄴ이므로 더는 사용하지 않게 된 클로저에 대해서는 메모리를 차지하지 도록 관리해줄 필요가 있다.
'To be Developer > JavaScript' 카테고리의 다른 글
Core Javascript 3 - this (0) | 2020.05.14 |
---|---|
Core Javascript - 2 Execution Context (0) | 2020.05.13 |
Core Javascript 1 - Data Type (0) | 2020.05.12 |
[javascript] Closure와 필요성. (0) | 2019.11.08 |
[javascript] let 과 var 그리고 hoisting (0) | 2019.11.07 |