본문 바로가기
  • GDG on campus Ewha Tech Blog
4-1기 스터디/Javascript-Typescript Deep Dive

[JS Deep Dive 스터디] 5주차 - 클로저

by 호프 2023. 1. 25.

일시: 2022.11.27

참석자:


금주 스터디는 오예린님께서 발표해주셨습니다.

클로저

클로저의 의미 및 원리 이해

  • 클로저란?
    • 어떤 함수 A에서 선언한 변수 a를 참조하는 내부함수 B를 외부로 전달할 경우 A의 실행 컨텍스트가 종료가 되더라도 변수 a가 사라지지 않는 현상

1) 외부 함수의 변수를 참조하는 내부 함수 - 내부 함수 자체를 return 하는 경우
    ```javascript
    var outer = function(){
        var a = 1;
        var inner = function() {
            return ++a;
        }
        return inner;
    };
    var outer2 = outer();
    console.log(outer2());
    console.log(outer2());
    ```
    - outer 함수에서 inner 함수 자체를 반환해버림
    - outer 함수의 실행 컨텍스트가 종료가 되어도 outer2 변수는 outer의 실행 결과인 inner 함수 참조
    - inner 함수가 참조된다면 outerEnvironmentReference는 inner 함수가 선언된 위치의 LexicalEnvironment가 참조복사됨, 즉, outer 함수의 LexicalEnvironment가 담김
    - 정리하자면, outer에서 선언한 a에 접근하고, 1을 더한 값을 반환하며, inner 함수의 실행 컨텍스트를 종료함 <br> <br>

    > <br>🎯 outer 함수의 실행 컨텍스트가 종료가 되었는데 outer 함수 내부 함수(inner)가 잘 실행되는 이유? <br><br> -> 가비지 컬렉터의 동작 방식 때문 <br> -> 가비지 컬렉터는 어떤 값을 참조하는 변수가 하나라도 있다면 수집을 하지 않는 특성을 가짐 <br> <br>


 2) return 없이도 클로저가 발생하는 기타 경우
    ```javascript
        (function () {
            var a = 0;
            var intervalId = null;
            var inner = function() {
                if (++a >= 10){
                    clearInterval(intervalId);
                }
                console.log(a);
            };
            intervalId = setInterval(inner, 1000)l
        })();
    ```
    - 별도 외부객체 window의 메서드(setTimeout or setInterval)에 전달한 콜백 함수 내부에서 지역변수 참조

클로저와 메모리 관리

  • 클로저 사용을 할 경우 현재 자바스크립트 엔진에서 우리는 메모리 소모 만 잘 관리하면 됨
  • 클로저를 사용을 한다 == 의도적으로 함수의 지역변수가 메모리를 소모하도록 유지함 == 불필요한 시점이 되면 메모리 소모를 하지 않도록 조치해주면 됨
    • 참조 카운트를 0으로 만들어 가비지 컬렉터의 수집 대상이 되도록 변경하기
    • 식별자에 참조형이 아닌 기본형 데이터 (대개 null이나 undefined)를 할당해주면 끝
    • var outer = function(){ var a = 1; var inner = function() { return ++a; } return inner; }; var outer2 = outer(); console.log(outer2()); console.oog(outer2()); outer = null; //outer 식별자에 inner 함수 참조를 끊어줌 == GC 수거대상이 되고, 메모리 소비를 줄일 수 있음

클로저 활용 사례

  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.vbody.appendChild($ul);
    • fruits 변수를 순회하여 li 생성, li 클릭 시 리스너에 기억된 콜백 함수 실행
    • $li.addEventListener('click', function() ... 이 부분에서 넘겨준 콜백 함수(B)는 외부 변수 fruits를 참조하고 있어 클로저 발생
    • A 함수의 종료 여부와 관계 없이 B 함수가 실행될 때 B의 outerEnvironmentReference는 A의 변수 내용이 포함된 LexicalEnvironment 참조함 == B가 참조하는 변수 fruits는 A가 종료되어도 계속 참조할 것

    (2) bind 메서드로 직접 값 넘겨주기
    • B 함수를 사용할 때 반복되는 점을 무마하기 위해 외부 함수로 빼고, bind 메서드를 활용해 값을 직접 넘겨주는 식으로 코드 작성 가능
    • 이때, bind 메서드 활용 시 이벤트 객체가 인자로 넘어오는 순서가 바뀌고 함수 내부 this가 가리키는 대상이 달라짐
    • 이를 해결하기 위해서는 고차함수를 활용하는 방식이 존재함

    (3) 콜백 함수를 고차함수로 바꿔 클로저 활용하기
    • 고차함수란, 함수를 인자로 받거나 함수를 리턴하는 함수를 의미함
    • 해당 예씨에서는 alertFruit 대신 alertFruitBuilder 함수 사용 > alertFruitBuilder가 alertFruit와 같은 한 익명함수를 리턴함
    • 클릭 이벤트 발생 시 함수의 실행 컨텍스트가 열리면서 alertFruitBuilder의 인자로 넘어온 fruit를 outerEnvironmentReference에 의해 참조 가능! == alerFruitBuilder의 실행 결과로 반환된 함수에는 클로저 존재

  2. ... var fruits =['apple', 'banana', 'peach']; var $ul = document.createElement('ul'); 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)); //alertFruitBuilder 함수 실행 시 fruit 값을 인자로 전달. 실행 결과 다시 함수가 되고, 반환된 함수를 리스너에 콜백 함수로 전달. $ul.appendChild($li); }); ...
  3. ... var fruits =['apple', 'banana', 'peach']; var $ul = document.createElement('ul'); var alertFruit = function(fruit){ //B가 외부로 나왔다 alert('your choice is '+fruit); } fruits.forEach(function(fruit){ var $li = document.createElement('li'); $li.innerText = fruit; $li.addEventListener('click', alertFruit.bind(null, fruit)); $ul.appendChild($li); }); ...
  4. (1) 콜백 함수를 내부함수로 선언해 외부 변수를 직접 참조
  5. 접근 권한 제어할 때(정보 은닉 시)
    • 자바스크립트의 경우 다른 언어와 달리 public, private, protected와 같은 접근 권한을 직접 부여하지는 않아도 됨
    • BUT 클로저를 활용할 경우 public한 값과 private한 값을 구분짓는 것이 가능함
    • var outer = function(){ var a = 1; var inner = function() { return ++a; } return inner; //inner 함수 return함으로서 outer 함수의 지역변수 a를 외부에서 접근할 수 있게 함 //즉, 클로저를 잘 활용한다면 함수 내부의 변수들을 선택적으로 외부에 공개할 수 있고, 접근 권한을 부여할 수 있음 }; var outer2 = outer(); console.log(outer2()); console.oog(outer2());
  6. 부분 적용 함수
    • 부분 적용 함수란?
      • n개의 인자를 받는 함수에 미리 m개의 인자만 넘겨 기억시킨 후, 나중에 (n-m)개의 인자를 넘기면 함수의 실행 결과를 얻을 수 있게끔 하는 함수
      var partial = function(){
        var originalPartialArgs = arguments;
        var func = originalPartialArgs[0];
        if(typeof func !== 'function'){
            throw new Error('첫 번째 인자가 함수가 아닙니다');
        }
        return function(){
            var partialArgs = Array.prototype.slice.call(originalPartialArgs, 1);
            var restArgs = Array.prototype.slice.call(arguments);
            return func.apply(this,partialArgs.concat(restArgs));
        }
      }
      
      var add = function(){
        var result = 0;
        for (var i = 0; i<arguments.length; i++){
            result += arguments[i];
        }
        return result;
      }
      
      var addPartial = partial(add, 1,2,3,4,5); //원본 함수와 미리 적용할 인자들 전달
      console.log(addPartial(6,7,8,9,10)); //추가적으로 나머지 인자들을 받아 concat된 원본 함수 출력. 55를 출력
      // 부분 적용 함수의 예시
      
      var dog = {
        name: '강아지',
        greet: partial(function(prefix, suffix){
            return prefix + this.name + suffix;
        }, '왈왈, ')
      }
      
      dog.greet('입니다!'); //왈왈, 강아지입니다.
    • 미리 일부 인자를 넘겨주고 기억을 하게 한 다음, 추후 필요할 때 기억한 인자들까지 함께 실행한다는 개념 자체가 클로저의 정의에 부합

    • 디바운스
      • 짧은 시간 동안 반복적인 이벤트가 많이 발생할 경우 이를 전부 처리하지 않고 처음 or 마지막에 발생한 이벤트에 대해서 한 번만 처리하는 방식
      • 프론트엔드 성능 최적화에 큰 도움을 주는 기능 중 하나
      • scroll, wheel, mousemove, resize에 적용하기 좋음
      • var debounce = function(eventName, func, wait){ var timeoutId = null; return function(event){ var self = this; //이벤트가 발생한 DOM 요소 console.log(eventName, 'event 발생'); clearTimeout(timeoutId); //B timeoutId = setTimeout(func.bind(self, event), wait); //A } } var moveHandler = function(e){ console.log('move event'); } var wheelHandler = function(e){ console.log('wheel event'); } //이벤트가 발생하면 debounce 함수가 실행이 됨 document.body.addEventListener('mousemove', debounce('move', moveHandler, 500)); document.body.addEventListener('mousewheel', debounce('wheel', wheelHandler, 700));
      • 최초로 event가 발생한다면 timeout 대기열에 wait 시간 뒤에 func를 실행할 것이라는 내용 담김 (A)
      • wait 시간이 경과하기 전 동일한 event가 발생한다면 앞서 저장했던 대기열 초기화 (B) 다시 새로운 대기열 등록 (A)
      • 결론적으로 마지막으로 등록되어있는 대기열만 한 번 실행이 됨
  1. 커링 함수
    • 커링 함수란?
      • 여러 개의 인자를 받는 함수를 하나의 인자만 받는 함수로 나눠 순차적으로 호출이 되도록 체인 형태로 구성한 것을 의미
      • 한 번에 하나의 인자만 전달하며, 마지막 인자가 전달되기 전까지는 원본 함수가 실행 X
          var curry3 = function(func){
              return function (a){
                  return function (b){
                      return func(a, b);
                  };
              };
          };
        
          var getMaxWith10 = curry3(Math.max)(10);
          //getMaxWith10 == function(b) { return Math.max(10, b) } 
          console.log(getMaxWith10(8)); //10
          console.log(getMaxWith10(25)); //25
          //10과 숫자 인자 b를 한 번에 하나씩 전달을 해줬다는 점 & 마지막 인자 b가 전달이 되기 전까지는 우리가 원하는
          //리턴 값이 전달이 되지 않았다는 점에서 이는 커링 함수라고 할 수 있음
        
          var getMinWith10 = curry3(Math.min)(10);
          console.log(getMinWith10(8)); //8
          console.log(getMinWith10(25)); //10 
        
      •  


부분 적용 함수는 여러 개의 인자 전달 가능, 실행 결과 재실행 시 원본 함수가 실행이 된다는 점에서 차이 有)

    •  
    • 인자가 많아질수록 가독성이 떨어진다는 점을 방지하기 위해서 화살표 함수를 활용함
      • 함수에 어떤 값을 차례로 넘겨줄 것이고, 마지막에 호출될 함수(원본함수)가 무엇인지 한눈에 파악 가능
      • 각 단계에서 받은 인자들은 모두 마지막에 참조를 하므로 GC 대상이 아님 == 문제 없이 실행됨
    • 커링 함수가 유용한 경우
      • 지연 실행(lazy execution) 프로그래밍 (마지막 인자가 넘어갈 때까지 함수 실행 미루는 것)이 필요할 때
      • 프로젝트 내에서 자주 쓰이는 함수의 매개변수의 일부만 바뀔 때

댓글