TIL.클로저

chloe·2021년 1월 9일
0

TIL

목록 보기
35/81
post-thumbnail
post-custom-banner

🧐 클로저?

  • 자신을 내포하는 함수의 컨텍스트에 접근할 수 있는 함수
  • 함수가 특정 스코프에 접근할 수 있도록 의도적으로 그 스코프에 정의하는 것
  • 함수를 선언할 때 만들어지는 유효범위가 사라진 후에도 호출할 수 있는 함수
  • 이미 생명 주기상 끝난 외부 함수의 변수를 참조하는 함수
  • 자유변수가 있는 함수와 자유변수를 알 수 있는 환경의 결합

흠... 도대체 무슨 말이지? 매우 많은 책에서 클로저에 대해 위와 같이 정의하고 있다.
MDN에 따르면 클로저는 함수와 그 함수가 선언될 당시의 lexical environment의 상호관계에 따른 현상이다.=> 선언될 당시의 lexical environment: outerEnvironmentReference를 의미

👉 클로저란 어떤 함수에서 선언한 변수를 참조하는 내부함수에서만 발생하는 현상이다"
예제1.외부 함수의 변수를 참조하는 내부함수

var outer=function(){
  var a=1;
  var inner=function(){
    console.log(++a);
  };
  inner();
};
outer();

inner함수 내부에서는 a를 선언하지 않았다
=> 그래서 environmentRecord에서 값을 찾지 못한다
=>outerEnvironmentReference에 지정된 상위 컨텍스트인 outer의 LexicalEnvironment에 접근해서 다시 a를 찾는다.
=>outer함수의 실행컨텍스트가 종료되면 LexicalEnvironment에 저장된 식별자들 (a,inner)에 대한 참조를 지운다.
=> 각 주소에 저장돼 있던 값들은 자신을 참조하는 변수가 하나도 없으니 가비지 설렉터의 대상이된다.

예제2.외부 함수의 변수를 참조하는 내부함수

var outer=function(){
  var a=1;
  var inner=function(){
    return++a;
  };
  return inner();
};
var outer2=outer();
console.log(outer2);
  • inne함수 내부에서 외부변수인 a를 사용
  • 그런데 inner함수를 실행한 결과를 6번째 줄에서 리턴
    (결과적으로 outer함수의 실행 컨텍스트가 종료된 시점에는 a변수를 참조하는 대상이 없어진다)=> 이말은 즉 a,inner 변수의 값들은 언젠가 가비지 컬렉터에 의해 소멸될 것이다.

    💁🏻‍♀️ 가비지? 정리되지 않은 메모리, 유효하지 않은 메모리 주소
    가비지는 메모리가 부족할 때 이런 가비지들을 메모리에서 해제시켜 다른 용도로 사용할 수 있게 해주는 프로그램

예제3.외부 함수의 변수를 참조하는 내부함수

var outer =function(){
  var a=1;
  var inner=function(){
    return ++a;
  };
  return inner;
  //inner함수 자체를 반환 => 그러면 outer함수의 실행 컨텍스트가 종료될 때 
  //outer2변수는 outer의 실행 결과인 inner함수를 참조하게 될 것
};
var outer2=outer();
console.log(outer2());//앞서 반환된 함수인 inner가 실행될 것
console.log(outer2());
  • inner함수의 실행 컨텍스트의 environmentRecord에는 수집할 정보가 없다.
  • outer-EnvironmentReference에는 inner함수가 선언된 위치의 LexicalEnvironment가 참조복사된다.
  • inner함수는 outer함수 내부에서 선언됐으므로 outer함수의 LexicalEnvironment가 담길 것이다.
  • 스코프 체이닝에 따라 outer에서 선언한 변수 a에 접근해서 1만큼 증가시킨 후 2를 반환하고, inner함수의 실행 컨텍스트가 종료된다

    🎈 이상한점? inner함수의 실행 시점에는 outer함수는 이미 실행이 종료된 상태인데 outer함수의 LexicalEnvironment에 어떻게 접근할 수 있는걸까?
    =>가비지 컬렉터의 동작방식때문이다.(가비지 컬렉터는 어떤 값을 참조하는 변수가 하나라도 있다면 그 값은 수집 대상에 포함시키지 않는다)

다시 클로저의 정의를 살펴보면, 클로저는 어떤 함수 A에서 선언한 변수a를 참조하는 내부함수 B를 외부로 전달할 경우 A의 실행 컨텍스트가 종료된 이후에도 변수 a가 사라지지 않는 현상
👉 주의할 점: 외부로 전달이 곧 return만을 의미하는 것은 아니다.

return 없이도 클로저가 발생하는 경우

//(1)setInterval/setTimeout
(function(){
  var a=0;
  var intervalid=null;
  var inner=function(){
    if(++a>=10){
      clearInterval(intervalid);
    }
    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);
}();

(1)은 별도의 외부객체인 window메서드(setTimeout or setInterval)에 전달할 콜백 함수 내부에서 지역변수를 참조한다.
(2)는 별도의 외부 객체인 DOM의 메서드 (addEventListener)에 등록할 handler 함수 내부에서 지역변수를 참조한다.

💁🏻‍♀️ 두 상황 모두 지역변수를 참조하는 내부함수를 외부에 전달했기에 클로저!!


클로저와 메모리 관리

메모리 소모는 클로저의 본질적인 특성!
메모리 관리 방법은 간단하다. 클로저는 어떤 필요에 의해 의도적으로 함수의 지역변수를 메모리를 소모하도록 함으로써 발생한다.
참조 카운트를 0으로 만들면 언젠가 가비지컬렉터가 수거해 갈 것이고, 이때 소모됐던 메모리가 회수될 것이다. 그럼 참조 카운트를 0으로 만드는 방법은?
=> 식별자에 참조형이 아닌 기본형 데이터(보통 null,undefined)를 할당하면 된다.

//(1) return에 의한 클로저의 메모리 해제
var outer=(function(){
  var a =1;
  var inner=function(){
    return ++a;
  };
  return inner;
})();
console.log(outer());
console.log(outer());
outer=null; //outer식별자의 inner함수 참조를 끊음
//(2)setInterval에 의한 클로저의 메모리 해제
(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);
})();
//(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.removeEventListener('click',clickHandler);
      clickHandler=null;
     // clickHandler 식별자의 함수 참조를 끊음
    }
  };
  button.addEventListener('click',clickHandler);
  document.body.appendChild(button);
})();

클로저 활용 사례

1.콜백 함수 내부에서 외부 데이터를 사용하고자 할 때

✓ 콜백함수를 내부 함수로 선언해서 외부 변수를 직접 참조하는 방법

var fruits=['apple','banana','peach'];
var $ul=document.createElement('ul');//공통코드

fruits.forEach(function(fruit) {//(A)
  var $li=document.createElement('li');
  $li.innerText=fruit;
  $li.addEventListener('click',function(){//(B)
    alert('your choice is'+fruit);
  });
  $ul.appendChild($li);
});
document.body.appendChild($ul);

fruit변수를 순회하며 li를 생성하고, 각 li를 클릭하면 해당 리스너에 기억된 콜백 함수를 실행하게 된다.
forEach 메서드에 넘겨준 익명의 콜백함수 (A)는 그 내부에서 외부변수를 사용하지 않고 있으므로 클로저가 없지만, addEventListener에 넘겨준 콜백함수 (B)에는 fruit이라는 외부 변수를 참조하고 있으므로 클로저가 있다.

(A)는 fruits의 개수만큼 실행되며, 그때마다 새로운 실행 컨텍스트가 활성화될 것이다. A의 실행 종료여부와 무관하게 클릭 이벤트에 의해 각 컨텍스트의 (B)가 실행될 때는 (B)의 outerEnvironmentReference가 (A)의 LexicalEnvironment를 참조하게 될 것이다.
즉, 최소한 (B)함수가 참조할 예정인 변수 fruit에 대해서는 (A)가 종료된 후에도 GC대상에서 제외되어 계속 참조 가능할 것이다.

위 코드를 fruit 을 인자로 받아 출력하는 형태로 바꿔보면 아래와 같다!

var alertFruit =function(fruit){
  alert('your choice is'+fruit);
};
fruits.forEach(function(fruit){
  var $li=document.createElement('li');
  $li.innerText=fruit;
  $li.addEventListener('click',alertFruit);
  $li.appendChild($li);
});
document.body.appendChild($ul);
alertFruit(fruits[1]);

공통 함수로 쓰고자 콜백 함수를 외부로 꺼내어 alertFruit라는 변수에 담았다.
콜백 함수의 인자에 대한 제어권을 addEventListener가 가진 상태이며, addEventListener는 콜백 함수를 호출할 때 첫번째 인자에 '이벤트 객체'를 주입한다. 이 문제는 bind메서드를 사용하면 손쉽게 활용할 수 있다.!!

✓bind메서드로 값을 직접 넘겨주면 클로저는 발생하지 않게 되지만 여러가지 제약이 따른다.

fruits.forEach(function(fruit){
  var $li=document.createElement('li');
  $li.innerText=fruit;
  $li.addEventListener('click',alertFruit.bind(null,fruit));
  $ul.appendChild($li);
});

그러나, 위처럼 코드를 작성하면 이벤트 객체가 인자로 넘어오는 순서가 바뀌는 점 및 함수 내부에서의 this가 원래의 그것과 달라지는 점은 감안해야 한다.
👉 bind메서드의 첫 번째 인자가 바로 새로 바인딩할 this인데, 이 값을 생략할 수 없기에 일반적으로 원래의 this를 유지하도록 할 수 없는 경우가 많다. 또한 예제에서는 두 번째 인자에 이벤트 객체가 넘어올 것이다.

✓ 콜백 함수를 고차함수로 바꿔서 클로저를 적극적으로 활용한 방안
고차함수? 함수를 인자로 받거나 함수를 리턴하는 함수

var alertFruitBuilder=function(fruit){
  return function(){
    alert('your choice is'+fruit);
  };
};
fruits.forEach(function(fruit){
  var $li=document.createElement('li');
  $li.innerText=fruit;
  $li.addEventListener('click',alertFruitBuilder(fruit));
  $ul.appendChild($li);
});

alertFruitBuilder라는 이름의 함수를 작성하고 이 함수 내부에서는 다시 익명함수를 반환한다. 마지막에서 2번째 줄에서는 alertFruitBuilder함수를 실행하면서 fruit값을 인자로 전달했다. 그러면 이 함수의 실행 결과가 다시 함수가 되며 이렇게 반환된 함수를 리스너에 콜백함수로써 전달할 것이다.
이후 언젠가 클릭 이벤트가 발생하면 비로소 이 함수의 실행 컨텍스트가 열리면서 alertfruitBuilder의 실행 결과로 반환된 함수에는 클로저가 존재한다.

2. 접근 권한제어( 정보은닉)

정보은닉은 어떤 모듈의 내부 로직에 대해 외부로의 노출을 최소화해서 모듈간의 결합도를 낮추고 유연성을 높이고자 하는 현대 프로그래밍 언어의 중요한 개념 중 하나.
흔히 접근권한에는 public,private,protected의 세 종류가 있다.

자바스크립트는 기본적으로 변수 자체에 이런 접근 권한을 직접 부여하도록 설계돼 있지 않다. 그렇다고 접근 권한 제어가 불가능한 것은 아니다. 클로저를 이용하면 함수 차원에서 public한 값과 private한 값을 구분하는 것이 가능하다.

Closure라는 영어단어는 사전적으로 닫혀있음, 폐쇄성, 완결성 정도의 의미를 가진다. 외부에 제공하고자 하는 정보들을 모아 return하고 내부에서만 사용할 정보들은 return하지 않는 것으로 접근 권한 제어가 가능하다. return한 변수들은 public member 공개멤버가 되고, 그렇지 않은 변수들은 private member 비공개 멤버가 된다.

3.부분 적용 함수

부분 적용 함수란 n개의 인자를 받는 함수에 미리 m개의 인자만 넘겨 기억시켰다가 나중에 (n-m)개의 인자를 넘기면 비로소 원래 함수의 실행 결과를 얻을 수 있게끔 하는 함수이다. this를 바인딩해야하는 점을 제외하면 앞서 살펴본 bind메서드의 실행결과가 바로 부분 적용 함수.

var add= function(){
  var result=0;
  for(var i=0,i<arguments.length;i++){
    result +=arguments[i];
  }
  return result;
};
var addPartial=add.bind(null,1,2,3,4,5);
console.log(addPartial(6,7,8,9,10));//55

addPartial함수는 인자 5개를 미리적용하고 추후 추가적으로 인자들을 전달하면 모든 인자를 모아 원래의 함수가 실행되는 부분 적용함수다.

부분 적용함수 -디바운스
디바운스는 짧은 시간동안 동일한 이벤트가 많이 발생할 경우 이를 전부처리하지 않고 처음 또논 마지막에 발생한 이벤트에 대해 한 번만 처리하는 것으로, 프론트엔드 성능 최적화에 큰 도움을 주는 기능 중 하나이다.

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처리');
};
var wheelHandler=function(e){
  console.log('wheel event처리');
};
document.body.addEventListener('mousemove',debounce('move',moveHandler,500));
document.body.addEventListener('mousewheel',debounce('wheel',moveHandler,700));

위 디바운스 함수는 출력 용도로 지정한 eventname,실행할 함수(func),마지막으로 발생한 이벤트인지 여부를 판단하기 위한 대기시간 (wait)을 받는다.
내부에서는 timeoutId변수를 생성하고, 클로저로 EventListener에 의해 호출될 함수를 반환한다.

4. 커링 함수

커링 함수란 여러 개의 인자를 받는 함수를 하나의 인자만 받는 함수로 나눠서 순차적으로 호출될 수 있게 체인 형태로 구성한 것을 말한다. 부분 적용함수와 기본적인 맥락은 일치하지만 다른 점이 있다. 커링은 한번에 하나의 인자만 전달하는 것이 원칙이다. 또한 중간 과정상의 함수를 실행한 결과는 그 다음인자를 받기 위해 대기만하고 마지막 인자가 전달되기 전까지는 원본 함수가 실행되지 않는다
(부분적용함수는 여러개의 인자를 전달할 수 있고, 실행결과를 재실행할 때 원본함수가 무조건 실행됨)

var curry3=function(func){
  return function(a){
    return function(b){
      return func(a,b);
    };
  };
};

var getMaxWith10=curry3(Math.max)(10);
console.log(getMaxWith10(8));//10
console.log(getMaxWith10(25));//25

var getMinWith10=curry3(Math.min)(10);
console.log(getMinWith10(8));//8
console.log(getMinwith10(25))//10
                                

부분 적용함수와 달리 커링함수는 필요한 상황에 직접 만들어 쓰기 쉽다. 필요한 인자 개수만큼 함수를 만들어 계속 리턴해주다가 마지막에 짠하고 조합해서 리턴해주면 되기때문이다.

⭐️ 총정리
클로저란 어떤 함수에서 선언한 변수를 참조하는 내부함수를 외부로 전달할 경우 함수의 실행 컨텍스트가 종료된 후에도 해당 변수가 사라지지 않는 현상이다.
내부 함수를 외부로 전달하는 방법에는 함수를 return하는 경우뿐만 아니라 콜백으로 전달하는 경우도 포함된다.
클로저는 그 본질이 메모리를 계속 차지하는 개념이므로 더는 사용하지 않게 된 클로저에 대해서는 메모리를 차지하지 않도록 관리해줄 필요가 있다.

profile
Front-end Developer 👩🏻‍💻
post-custom-banner

0개의 댓글