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

+ Recent posts