뮁이의 개발새발

[코어자바스크립트] Chapter 5. 클로저 본문

Front-end/Javascript

[코어자바스크립트] Chapter 5. 클로저

뮁뮁이 2024. 5. 18. 11:10

Chapter 5. 클로저

클로저란?

  • 어떤 컨텍스트 A에서 선언한 내부함수 B의 실행 컨텍스트가 활성화된 시점에 B의 outerEnvironmentReference가 참조하는 대상인 A의 LexicalEnvironment에 접근이 가능하게 됨.
  • 이때 A에서는 B에서 선언한 변수에 접근할 수 없지만 B에서는 A에서 선언한 변수에 접근이 가능함
    • 내부함수 B가 A의 LexicalEnvironment를 언제나 사용하는것은 아님.
    • 내부함수에서 외부 변수를 참조하는 경우에 한해서만 combination이 됨.

⇒ 어떤 함수에서 선언한 변수를 참조하는 내부함수에서만 발생하는 현상

// 외부 함수의 변수를 참조하는 내부 함수(1)
var outer = function() {
  var a = 1;
  var inner = function() {
    console.log(++a);
  };
  inner();
};
outer();
  • outer함수의 실행 컨텍스트가 종료되면 LexicalEnvironment에 저장된 식별자들(a, inner)에 대한 참조를 지움 ⇒ Gabage Collecter 대상이 됨
// 외부 함수의 변수를 참조하는 내부 함수(2)
var outer = function() {
  var a = 1;
  var inner = function() {
    return ++a; // 외부 함수의 변수를 참조 -> 클로저
  };
  return inner(); // 리턴하는 시점에 a를 참조하는 대상이 없어짐
};
var outer2 = outer();
console.log(outer2); // 2
  • inner를 리턴하는 시점에 a와 inner 변수의 값들은 가비지 컬렉터에 의해 소멸됨
  • 위 두 함수는 outer 함수의 실행 컨텍스트가 종료되기 이전에 inner 함수의 실행 컨텍스트가 종료돼 있으며, 이후 별도로 inner 함수를 호출할 수 없다는 공통점이 있음.
// 외부 함수의 변수를 참조하는 내부 함수(3)
var outer = function() {
  var a = 1;
  var inner = function() {
    return ++a;
  };
  return inner; // inner의 결과값이 아닌 함수 자체를 반환
};
var outer2 = outer();
console.log(outer2()); // 2
console.log(outer2()); // 3
  • inner 함수의 실행 컨텍스트의 environmentRecord에는 수집할 정보가 없음
  • outerEnvironmentReference에는 inner함수가 선언된 위치의 LexicalEnvironment가 참조 복사됨. (outer 함수의 LexicalEnvironment가 담기게됨)

클로저의 정의 정리

  • 어떤 함수에서 선언한 변수를 참조하는 내부함수에서만 발생하는 현상
    • ⇒ 외부 함수의 LexicalEnvironment가 가비지 컬렉팅되지 않는 현상
  • 어떤 함수 A에서 선언한 변수 a를 참조하는 내부함수 B를 외부로 전달할 경우 실행 컨텍스트가 종료된 이후에도 변수 a가 사라지지 않는 현상
    • 여기서 외부로 전달한다는것은 콜백함수, 외부 객체 등 return외에도 포함될 수 있음

클로저와 메모리 관리

  • 클로저로 인한 메모리 소모가 일부 존제하기 때문에 관리법을 잘 파악해서 적용해야 함.
  • 클로저가 발생하는 이유?
    • 어떤 필요에 의해 의도적으로 함수의 지역변수를 메모리를 소모하도록 함으로써 발생
    • 참조 카운트를 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);
})();

클로저 활용 사례

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

  • 이벤트리스너를 통한 예제
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);
  • 반복을 줄이기 위해 B를 외부로 분리하는것이 좋아보임
    • fruit를 인자로 받아 출력하는 형태로
var fruits = ['apple', 'banana', 'peach'];
var $ul = document.createElement('ul');

// 별도의 함수로 뺐음
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);
  $ul.appendChild($li);
});
document.body.appendChild($ul);
alertFruit(fruits[1]);
  • 위 코드는 각 li를 클릭하면 클릭한 대상의 과일명이 아닌 [object MouseEvent]라는 값이 출력됨.
  • 콜백함수 인자에 대한 제어권을 addEventListener가 가지고 있으며, addEventListener는 콜백 함수를 호출할 때 첫 번째 인자에 ‘이벤트 객체’를 주입하기 때문

위를 해결한 코드

var fruits = ['apple', 'banana', 'peach'];
var $ul = document.createElement('ul');

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.bind(null, fruit)); // bind를 통해 제어권을 alertFruit가 가지게끔 함.
  $ul.appendChild($li);
});
document.body.appendChild($ul);
  • 이벤트 객체가 인자로 넘어오는 순서가 바뀜
  • 함수 내부에서의 this가 원래의 그것과 달라지는 점은 감안해야함
    • 따라서 bind가 아닌 ‘고차함수’를 활용해야 함.
var fruits = ['apple', 'banana', 'peach'];
var $ul = document.createElement('ul');

var alertFruitBuilder = function(fruit) {
  return function() { // alertFruit를 반환 (함수형태, fruit로 인해 클로저가 존재
    alert('your choice is ' + fruit); 
  };
};
fruits.forEach(function(fruit) {
  var $li = document.createElement('li');
  $li.innerText = fruit;
  $li.addEventListener('click', alertFruitBuilder(fruit)); // 해당 함수(alertFruitBuilder)의 실행 결과가 함수가 됨.
  $ul.appendChild($li);
});
document.body.appendChild($ul);

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

  • 정보 은닉
    • 어떤 모듈의 내부 로직에 대해 외부로의 노출을 최소화해서 모듈간의 결합도를 낮추고 유연성을 높이고자 하는 현대 프로그래밍 언어의 중요한 개념중 하나
      • public, private, protected
      • public : 외부에서 접근 가능한 것
      • private : 내부에서만 사용하며 외부에 노출되지 않는 것
    • 클로저를 통해 이러한 값들을 구분하는것이 가능함
    • 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 함수의 지역변수인 a를 외부에서도 읽을 수 있게됨.
    • ⇒ 클로저를 활용하면 외부 스코프에서 함수 내부의 변수들 중 선택적으로 일부의 변수에 대한 접근 구너한을 부여할 수 있게 됨. (return을 활용)
  • 외부에 제공하고자 하는 정보들을 모아서 return(공개 멤버), 내부에 사용할 정보들은 return하지 않는 형태로 접근 권한 제어를 진행(비공개 멤버)
var car = {
  fuel: Math.ceil(Math.random() * 10 + 10), // 연료(L)
  power: Math.ceil(Math.random() * 3 + 2), // 연비(km/L)
  moved: 0, // 총 이동거리
  run: function() {
    var km = Math.ceil(Math.random() * 6);
    var wasteFuel = km / this.power;
    if (this.fuel < wasteFuel) {
      console.log('이동불가');
      return;
    }
    this.fuel -= wasteFuel;
    this.moved += km;
    console.log(km + 'km 이동 (총 ' + this.moved + 'km)');
  },
};
  • 위 코드에서는 변수에 마음대로 접근이 가능하여 주작이 가능함 (car.fuel, car.power 등등..)
    • 객체가 아닌 함수로 만들고, 필요한 멤버만을 return할 수 있도록 제어해야함. (클로저로 보호)
var createCar = function() {
  var fuel = Math.ceil(Math.random() * 10 + 10); // 연료(L)
  var power = Math.ceil(Math.random() * 3 + 2); // 연비(km / L)
  var moved = 0; // 총 이동거리
  return {
    get moved() {
      return moved;
    },
    run: function() {
      var km = Math.ceil(Math.random() * 6);
      var wasteFuel = km / power;
      if (fuel < wasteFuel) {
        console.log('이동불가');
        return;
      }
      fuel -= wasteFuel;
      moved += km;
      console.log(km + 'km 이동 (총 ' + moved + 'km). 남은 연료: ' + fuel);
    },
  };
};
var car = createCar();
  • createCar라는 함수를 통해 객체를 생성함으로써, fuel, power 등에 대한 변수에 대한 접근이 불가능하게끔 함
    • 여기서 비공개 멤버 / 공개 멤버는?
    • 외부에서는 run과 moved 값을 확인하는 두가지 동작만 가능하게 됨.
  • run을 다른 내용으로 덮어씌우는 어뷰징은 가능하긴 함.
    • 이를 막기 위한 코드
    • var createCar = function() { var fuel = Math.ceil(Math.random() * 10 + 10); // 연료(L) var power = Math.ceil(Math.random() * 3 + 2); // 연비(km / L) var moved = 0; // 총 이동거리 var publicMembers = { // moved 보호 get moved() { return moved; }, run: function() { // run 보호 var km = Math.ceil(Math.random() * 6); var wasteFuel = km / power; if (fuel < wasteFuel) { console.log('이동불가'); return; } fuel -= wasteFuel; moved += km; console.log(km + 'km 이동 (총 ' + moved + 'km). 남은 연료: ' + fuel); }, }; Object.freeze(publicMembers); return publicMembers; }; var car = createCar();

클로저를 활용해 접근 권한을 제어하는 방법

  1. 함수에서 지역변수 및 내부함수 등을 생성
  2. 외부에 접근권한을 주고자 하는 대상들로 구서왼 참조형 데이터(대상이 여럿일 때는 객체 또는 배열, 하나일 때는 함수)를 return함
    1. return한 변수들은 공개 멤버가 되고, 그렇지 않은 변수들은 비공개 멤버가 됨.

부분 적용 함수

  • n개의 인자를 받는 함수에 미리 m개의 인자만 념겨 기억시켰다가, 나중에 나머지(n-m)의 인자를 넘기면 비로소 원래 함수의 실행 결과를 얻을 수 있게끔 하는 함수
// 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); // bind를 사용해야 함.
console.log(addPartial(6, 7, 8, 9, 10)); // 55
  • this의 값을 변경할 수 밖에 없는 상황
// 부분 적용 함수 구현 (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(originalPartialArgs, 1);
    var restArgs = Array.prototype.slice.call(arguments);
    return func.apply(this, partialArgs.concat(restArgs)); // concat 사용
  };
};

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(prefix, suffix) {
    return prefix + this.name + suffix;
  }, '왈왈, '),
};
dog.greet('입니다!'); // 왈왈, 강아지입니다.
  • concat을 이용하여 반환할 함수들의 인자를 모아서 원본함수를 호출함. this에는 영향을 주지 않음
  • 다만, 반드시 앞에서부터 차례대로 전달할 수 밖에 없음
// 부분적용 함수 구현(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(originalPartialArgs, 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 add = function() {
  var result = 0;
  for (var i = 0; i < arguments.length; i++) {
    result += arguments[i];
  }
  return result;
};
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(' 배고파요!'); // 왈왈, 강아지 배고파요!
  • 미리 실행할 함수의 모든 인자 개수를 맞춰 빈 공간을 확보하지 않아도 됨.
  • 최종 실행 시, 인자 개수가 많든 적든 잘 실행됨

⇒ 위 두 코드 모두 클로저를 핵심 기법으로 활용

미리 일부 인자를 넘겨두어 기억하게끔 하고 추후 필요한 시점에 기억했던 인자들까지 함께 실행하게 함.

디바운스

  • 짧은 시간 동안 동일한 이벤트가 많이 발생할 경우 이를 전부 처리하지 않고 처음 또는 마지막에 발생한 이벤트에 대해 한 번만 처리하는 것
  • 성능 최적화에 큰 도움을 줌. (scroll, wheel, mousemove, resize 등)
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)
);
  • wait 시간이 경과하기 이전에 동일한 event가 발생하면, 앞서 저장했던 대기열을 초기화하고 다시 새로운 대기열을 등록하게 됨.
  • 마지막에 발생한 이벤트만이 초기화되지 않고 무사히 실행될 것
  • eventName, func, wait, timeoutId 등
  • [참고] 전역 공간에 대한 침범을 안하기 위해 “Symbol.for”라는 메서드를 활용하기도 함.
    • 어디서든 접근 가능하면서 유일무의한 상수를 맞들고자 할 때 적합함

커링 함수

  • 여러개의 인자를 받는 함수를 하나의 인자만 받는 함수로 나눠서 순차적으로 호출될 수 있게 체인 형태로 구성한 것
  • 한 번에 하나의 인자만 전달하는 것을 원칙으로 함
  • 마지막 인자가 전달되기 전까지는 원본 함수가 실행되지 않음.
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
  • 다만 인자가 많아질수록 가독성이 떨어짐
    • 이를 해결하기 위해 ES6에서 화살표 함수가 등장
    • var curry5 = func => a => b => c => d => e => func(a,b,c,d,e);
  • 당장 필요한 정보만 받아서 전달하고 또 필요한 정보가 들어오면 전달하는 식으로 수행. 마지막 인자가 넘어갈 때까지 함수 실행을 미룰 수 있음 → 지연 실행

  • 공통적인 요소는 먼저 기억시켜두고 특정한 값(id)만으로 서버 요청을 수행하는 함수

정리

  1. 클로저란?
    1. 어떤 함수에서 선언한 변수를 참조하는 내부함수를 외부로 전달할 경우, 함수의 실행 컨텍스트가 종료된 후에도 해당 변수가 사라지지 않는 현상
  2. 내부함수를 외부로 전달하는 방법?
    1. 함수를 return하는 경우, 콜백으로 전달하는 경우
  3. 클로저는 메모리를 계속 차지하는 개념
    1. 더는 사용하지 않게 된 클로저에 대해서는 메모리를 차지하지 않도록 관리해줄 필요가 있음
Comments