개발일지

코어 자바스크립트 3주차 발표 본문

코어 자바스크립트 스터디

코어 자바스크립트 3주차 발표

박수미/ 2024. 7. 19. 00:14

5강 클로저

클로저의 의미 및 원리 이해

  • 클로저는 여러 함수형 프로그래밍 언어에서 등장하는 보편적인 특성이다.
  • 클로저란, 어떤 함수 A에서 선언한 변수를 참조하는 내부함수에서만 발생하는 현상
"A closure is the combination of a function and the lexical environment within which that
function was declared" - MDN
//  클로저는 함수와 그 함수가 선언될 당시의 lexical environment'의 상호관계에 따른 현상

즉, 어떤 컨텍스트 A에서 선언한 내부 함수 B의 실행 컨텍스트가 활성화된 시점에는 B의 outerEnvironmentReference가 참조하는 대상인 A의 LexicalEnvironment에도 접근 가능합니다.

  • A에서는 B에서 선언한 변수에 접근 불가
  • 반대로 B에서는 A에서 선언한 변수 접근 가능

단, 내부 함수 B에서 외부 변수 A를 참조하는 경우에만 해당됩니다. 외부 변수를 참조하지 않으면 B가 A의 LexicalEnvironment를 사용할 수 없습니다.

1. 외부 함수에 변수를 참조하는 내부 함수 (1)

var outer = function () {
  var a = 1;
  var inner = function () {
    console.log(++a);   // 2
  };
  inner();
};
outer();
  • 일반적인 함수 및 내부 함수에서의 동작과 차이 없음
  • outer의 LexicalEnvironment에 속하는 변수가 모두 가비지 컬렉터의 대상이 됩니다.

2. 외부 함수에 변수를 참조하는 내부 함수 (2)

var outer = function () {
  var a = 1;
  var inner = function () {
    return ++a;
  };
  return inner();
};
var outer2 = outer();
console.log(outer2);    // 2
  • 일반적인 함수 및 내부 함수에서의 동작과 차이 없음
  • a, inner 변수의 값들은 언젠가 가비지 컬렉터에 의해 소멸됩니다.

예제 1번과 2번은 outer 함수 실행 컨텍스트가 종료되기 이전에 inner 함수의 실행 컨텍스트가 종료돼 있으며, 이후 별도로 inner 함수를 호출할 수 없다는 공통점이 있습니다.

 

3. 외부 함수에 변수를 참조하는 내부 함수 (3)

var outer = function () {
  var a = 1;
  var inner = function () {
    return ++a;
  };
  return inner;
};
var outer2 = outer();
console.log(outer2());      // 2
console.log(outer2());      // 3
  • outer 함수는 실행 종료 시점에 inner 함수를 반환합니다. 외부함수인 outer의 실행이 종료되더라도 내부 함수인 inner 함수는 언젠가 outer2를 실행함으로써 호출될 가능성이 열려 있습니다.
  • inner 함수의 실행 컨텍스트가 활성화되면 outerEnvironmentReference가 outer 함수의 LexicalEnvironment를 필요로 할 것이므로 수집 대상에서 제외됩니다.

클로저와 메모리 관리

클로저는 객체지향과 함수형 모두를 아우르는 매우 중요한 개념입니다. 메모리 누수의 위험을 이유로 클로저 사용을 조심해야 한다고 주장하는 사람들도 있지만 메모리 소모는 클로저의 본질적인 특성입니다. 하지만 개발자가 의도적으로 참조 카운트를 0이 되지 않게 설계한 경우는 '누수'라고 할 수 없습니다.

 

클로저의 메모리 관리 방법

  • 클로저는 어떤 필요에 의해 의도적으로 함수의 지역변수를 메모리 소모하도록 함으로써 발생합니다.
  • 필요성이 사라진 시점에는 더는 메모리가 소모되지 않게 해주면 됩니다. 즉 참조 카운트를 0으로 만들어주면 됩니다.
  • 참조 카운트를 0으로 만들어주기 위해서는 기본형데이터(null이나 undefined)를 할당하면 됩니다.

클로저 메모리 관리 예제

// return에 의한 클로저의 메모리 해제
var outer = (funciton () {
  var a = 1;
  var inner = function () {
    return ++a;
  };
  return inner;
})();
console.log(outer());
console.log(outer());
outer = null;    // outer 식별자의 inner 함수 참조를 끊음

 

클로저 활용 사례

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

 

1-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);
  • (A)는 furits의 개수만큼 실행되며, 그때마다 새로운 실행 컨텍스트 활성화 
  • A의 실행 종료 여부와 무관하게 클릭 이벤트에 의해 각 컨텍스트 (B)가 실행될 때는 (B)의 outerEnvironmentReference가 (A)의 LexicalEnvironment를 참조하게 됩니다. 따라서 (B) 함수가 참조될 예정인 변수 fruit에 대해서는 (A)가 종료된 후에도 가비지 컬렉터대상에서 제외되고 계속 참조 가능합니다.

1-2. bind 메서드를 활용 - 인자 전달

- (B) 함수가 다른 곳에서도 쓰일 경우 반복을 줄이기 위해 (B) 함수를 외부로 분리 

fruits.forEach(function (fruit) {
  var $li = document.createElement('li');
  $li.innerText = fruit;
  $li.addEventListener('click', alertFruit.bind(null, fruit));
  $ul.appendChild($li);
  });
  • addEventListener은 콜백 함수를 호출할 때 첫 번째 인자에 '이벤트 객체'를 주입하기 때문에 bind메서드로 인자를 전달
    • 이벤트 객체가 인자로 넘어오는 순서가 바뀌는 점 및 함수 내부에서의 this가 원래의 그것과 달라지는 점을 감안해야합니다. 이런 변경사항이 생기게 하지 않기 위해서는 bind 대신 다른 방식으로 풀어야 합니다.
    • 다른 방식이란 고차함수를 활용하는 것으로, 함수형 프로그래밍에서 자주 쓰이는 방식이기도 합니다.

1-3. 고차함수 활용

var alertFruitBuilder = function (fruit) {
  return funciton () {
    alert('your choice is ' + furit);
  };
};
fruits.forEach(funciton (fruit) {
  var $li = document.createElement('li');
  $li.innerText = fruit;
  $li.addEventListener('click', alertFruitBuilder(fruit));
  $ul.appendChild($li);
});
  • alertFruitBuilder 함수 내부에서는 다시 익명함수를 반환합니다. 이 익명함수가 기존의 alertFruit 함수입니다.
  • 이벤트 핸들러에서 alertFruitBuilder 함수를 실행하면 fruit 값을 인자로 전달합니다. 그러면 이 함수의 실행 결과가 다시 함수가 되며, 이렇게 반환된 함수를 리스너에 콜백 함수로써 전달합니다. 즉 alertFruitBuilder의 실행 결과로 반환된 함수에는 클로저가 존재합니다.

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

정보 은닉은 어떤 모듈의 내부 로직에 대해 외부로의 노출을 최소화해서 모듈 간의 결합도를 낮추고 유연성을 높이고자 하는 현대 프로그래밍 언어의 중요한 개념 중 하나입니다.

접근 권한에는 public, private, protected 세 종류가 있습니다.

  1. public - 외부에서 접근 가능
  2. private - 내부에서만 사용하며, 외부에 노출되지 않는다

클로저를 이용하면 함수 차원에서 public 한 값과 private 한 값을 구분하는 것이 가능합니다.

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

outer 함수를 종료할 때 inner 함수를 반환함으로써 outer 함수의 지역변수인 a의 값을 외부에서도 읽을 수 있게 되었습니다. 이처럼 클로저를 활용하면 외부 스코프에서 함수 내부의 변수들 둥 선택적으로 일부의 변수에 대한 접근 권한을 부여할 수 있습니다. (return을 활용해서)

 

closure: '닫혀있음, 폐쇄성, 완결성'

  • outer 함수는 외부(전역 스코프)로부터 철저하게 격리된 닫힌 공간입니다. 외부에서는 outer 함수를 실행할 수 있지만, outer 함수 내부에서는 어떠한 개입도 할 수 없음
  • 외부에서는 외부 공간에 노출돼 있는 outer라는 변수를 통해 outer 함수가 reuturn 한 정보에만 접근할 수 있습니다. 즉 return 값이 외부에 정보를 제공하는 유일한 수단
  • 따라서 외부에 제공하고자 하는 정보들을 모아서 return 하고, 내부에서만 사용할 정보들은 return 하지 않는 것으로 접근 권한 제어가 가능한 것입니다. return 한 변수들은 공개 멤버(public)가 되고, 그렇지 않은 변수들은 비공개 멤버(private)가 되는 것입니다.

3. 부분 적용 함수

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

 

3-1 부분 적용 함수 구현

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(original{artialArgs, 1);
    var restArgs = Array.prototype.slice.call(arguments);
    return func.apple(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));        // 55

var dog = {
  name: '강아지',
  greet: partial(function(perfix, suffix) {
    return prefix + this.name + suffix;
    }, '왈왈, ')
  };
  dog.greet('입니다!');        // 왈왈, 강아지입니다.
  • 첫 번째 인자에는 원본 함수를, 두 번째 인자 이후부터는 미리 적용할 인자들을 전달하고, 반환할 함수에서는 다시 나머지 인자들을 받아 이들을 한데 모아(concat) 원본 함수를 호출(apply)합니다.
  • 실행 시점의 this를 그대로 반영함으로써 this에는 아무런 영향을 주지 않게 됩니다.

보통의 경우 부분 적용 함수는 이 정도로 충분합니다. 다만 부분 적용 함수에 넘길 인자를 반드시 앞에서부터 차례로 전달할 수밖에 없다는 점은 아쉽습니다. 

 

3-2 부분 적용 함수 구현

Object.defineProperty(window, '_', {
  value: 'EMPTY_SPACE',
  writable: false,
  configurable: false,
  enumerable: false
  });
  
  var partial2 = function () {
    var originalPartialArgs = arguments;
    var func = originalPartialArgs[0];
    if (typeof func !== 'function') {
      throw new Error('첫 번째 인자가 함수가 아닙니다.');
    }
    return function () {
      var partialArgs = Array.prototype.slice.call(ariginalPartialArgs, 1);
      var restArgs = Array.prototype.slice.call(arguments);
      for (var i = 0; i < partialArgs.length; i++) {
        if (partialArgs[i] ===_) {
          partialArgs[i] = restArgs.shift();
        }
      }
      return func.apply(this, partialArgs.concat(restArgs));
    };
  };
  var addPartial = partial2(add, 1, 2, _, 4, 5, _, _, 8, 9);
  console.log(addPartial(3, 6, 7, 10));                     // 55
  
  var dog = {
  name: '강아지',
  greet: partial2(function(prefix, suffix) {
    return prefix + this.name + suffix;
  }, '왈왈, ')
};
dog.greet('배고파요!');                    // 왈왈, 강아지 배고파요!
  • '비워놓음'을 표시하기 위해 미리 전역객체에 _라는 프로퍼티를 준비하면서 삭제 변경 등의 접근에 대한 방어 차원에서 여러 가지 프로퍼티 속성을 설정했습니다.
  • 처음에 넘겨준 인자들 중 _로 비워놓은 공간마다 나중에 넘어온 인자들이 차례대로 끼워 넣도록 구현했습니다. 또 부분 적용 함수를 만들 때 미리부터 실행할 함수의 모든 인자 개수를 맞춰 빈 공간을 확보하지 않아도 됩니다.

3-3. 부분 적용 함수 - 디바운스

디바운스란?

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

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', wheelHandler, 700));

 

  • 최초 event가 발생하면 7번째 줄에 의해 timeout의 대기열에 'wait 시간 위에 func를 실행할 것'이라는 내용이 담깁니다.
  • 그런데 wait 시간이 경과하기 이전에 다시 동일한 event가 발생하면 이번에는 6번째 줄에 의해 앞서 저장했던 대기열을 초기화하고, 다시 7번째 둘에서 새로운 대기열을 등록합니다.
  • 결국 각 이벤트가 바로 이전 이벤트로부터 wait시간 이내에 발생하는 한 마지막에 발생한 이벤트만이 초기화되지 않고 무사히 실행될 것입니다.
  • 이 예제의 디바운수 함수에서 클로저로 처리되는 변수에는 eventName, func, wait, timeoutId가 있습니다.

4. 커링 함수

커링 함수란 여러 개의 인자를 받는 함수를 하나의 인자만 받는 함수로 나눠서 순차적으로 호출될 수 있게 체인 형태로 구성한 것을 말합니다.

부분 적용 함수와 기본적인 맥락은 일치하지만 몇 가지 다른 점이 있습니다.

- 커링은 한 번에 하나의 인자만 전달하는 것을 원칙으로 합니다. 또한 중간 과정상의 함수를 실행한 결과는 그다음 인자를 받기 위해 대기만 할 뿐으로, 마지막 인자가 전달되기 전까지는 원본 함수가 실행되지 않습니다.

 

4-1. 커링 함수 예제

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
  • 부분 적용 함수와 달리 커링 함수는 필요한 상황에 직접 만들어 쓰기 용이합니다. 필요한 인자 개수만큼 함수를 만들어 계속 리턴해주다가 마지막에 조합해서 리턴해주면 되기 때문입니다.

4-2 커링 함수 예제

var curry5 = function (func) {
  return function (a) {
    return function (b) {
      return function (c) {
        return function (d) {
          return function (e) {
            return func(a, b, c, d, e);
          };
        };
      };
    };
  };
};
var getMax = curry5(Math.max);
console.log(getMax(1)(2)(3)(4)(5));
  • 다만 인자가 많아질수록 가독성이 떨어진다는 단점이 있습니다. ES6 화살표 함수를 사용하면 한 줄에 표기할 수 있습니다.
var curry5 = func => a => b => c => d => e => func(a, b, c, d, e);
  • 위처럼 화살표 함수로 구현하면 커링 함수를 이해하기 훨씬 수월합니다.
  • 각 단계에서 받은 인자들을 모두 마지막 단계에서 참조할 것이므로 CG의 대상이 되지 않고 메모리에 차곡차곡 쌓였다가, 마지막에 호출로 실행 컨텍스트가 종료된 후에 한꺼번에 CG의 수거 대상이 됩니다.

커링 함수가 유용한 경우

  • 당장 필요한 정보만 받아서 전달하고 또 필요한 정보가 들어오면 전달하는 식으로 하면 결국 마지막 인자가 넘어갈 때까지 함수 실행을 미루는 셈이 됩니다.
  • 이를 함수형 프로그래밍에서는 지연실행이라고 칭합니다. 원하는 시점까지 지연시켰다가 실행하는 것이 요긴한 상황이라면 커링을 쓰기에 적합할 것입니다.