[코어 자바스크립트] 05 클로저

백은진·2021년 1월 20일
0

05 클로저

챕터 목표: 클로저의 뜻을 확실히 알고, 이를 다른 사람에게 간단히 소개할 수 있을 만큼 요약한다.
또한 클로저를 활용한 로직을 익혀, 추후 직접 사용해볼 수 있도록 익숙해진다.


1. 클로저의 의미 및 원리 이해

클로저는 함수형 프로그래밍에서 등장하는 보편적인 특성이나, 이를 설명한 글은 각기 다른 정의를 내놓는 경우가 많다.
클로저의 일반적인 정의로부터 그 의미를 파악하고 다양한 사례를 통해 성질을 살펴본 후, 이를 재조합하여 이해하기 쉬운 문장으로 바꿔보자.

우선 MDN에서는 클로저가 "A closer is the combination of a function and the lexical environment within which that function was declared."라고 소개한다.
(직역하자면, "클로저는 함수와 그 함수가 선언될 당시의 lexical 환경의 상호관계에 따른 현상"이다.)

여기에서 lexical environment란, 실행 컨텍스트의 구성 요소 중 하나인 outerEnvironmentReference에 해당한다. Lexical 환경의 environmentRecord와 outerEnvironmentReference에 의해 변수의 유효범위인 스코프가 결정되고 스코프 체인이 가능해진다.

이런 원리를 따라 어떤 컨텍스트 A에서 선언한 내부함수 B의 실행 컨텍스트가 활성화된 시점에는 B의 outerEnvironmentReference가 참조하는 대상인 A의 Lexical 환경에 접근이 가능하다.

그러나 내부함수에서 외부 변수를 참조하지 않는 경우 등 내부함수 B가 A의 Lexical 환경을 사용하지 않기도 한다. 즉, 내부함수에서 외부 변수를 참조하는 경우에 한해서만 combination(선언될 당시의 Lexical 환경과의 상호관계)이 의미가 있다.

예제를 통해 알아보자.

예제 1 ) 외부 함수의 변수를 참조하는 내부 함수 1

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

outer();  // 2

inner 함수 내부에서는 a를 선언하지 않았기 때문에 environmentRecord에서 값을 찾지 못하므로, outerEnvironmentReference에 지정된 상위 컨텍스트인 outer의 LexicalEvironment에 접근해서 다시 a를 찾는다.

outer 함수의 실행 컨텍스트가 종료되면 LexicalEnvironment에 저장된 식별자들(a, inner)에 대한 참조가 삭제된다. 그러면 각 주소에 저장되어 있던 값들은 자신을 참조하는 변수가 하나도 없게 되므로 Garbage collector의 수집 대상이 된다.

일반적인(예제 1) 콜스택 흐름

일반적인 함수 및 내부함수에서의 콜스택 동작과 동일하다.

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

var outer = function () {
  var a = 1;
  
  var inner = function () {
    return ++a;
  };
  
  return inner();
};

var outer2 = outer();
console.log(outer2);  // 2

2번 예제에서는 inner 함수 내에서 실행 결과를 return 한다. 결과적으로 outer 함수의 실행 컨텍스트가 종료된 시점에는 a 변수를 참조하는 대상이 없으므로, a와 inner 변수 값들은 GC에 의해 소멸하게 된다.

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

아래의 예제 3에서는 outer의 실행 컨텍스트가 종료된 후에도 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

3번 예제는 outer 함수에서 inner 함수 자체를 반환한다. 그러면 outer 함수의 실행 컨텍스트가 종료될 때, outer2 변수는 outer의 실행 결과인 inner 함수를 참조하게 된다. 이후 콘솔에 outer2 함수를 호출하면 앞서 반환된 inner 함수가 실행된다. 따라서 outer2 함수의 호출을 계속할수록 a의 값이 상승한다.

inner 함수의 실행 컨텍스트의 environmentRecord에는 수집할 정보가 없다. outerEnvironmentReference에는 inner 함수가 선언된 위치의 LexicalEnvironment가 참조복사된다.

inner 함수는 outer 함수의 내부에서 선언됐으므로, outer 함수의 LexicalEnvironment에 담긴다. 스코프 체이닝에 따라 outer에서 선언한 변수 a에 접근하여 1씩 증가시킨 후 그 값인 2를 반환하고, inner 함수의 실행 컨텍스트가 종료된다. 이후 다시 outer2 함수를 호출하면 같은 방식으로 a의 값을 2에서 3으로 증가시킨 후 3을 반환한다.

inner 함수의 실행 시점에 outer 함수는 이미 실행이 종료된 상태인데 outer 함수의 LexicalEnvironment에 접근할 수 있는 이유는 Garbage colletor의 동작 방식 때문이다.
GC는 어떤 값을 참조하는 변수가 하나라도 있다면 그 값은 수집 대상에 포함시키지 않는다.

3번 예제의 outer 함수는 실행 종료 시점에 inner 함수를 반환한다. 따라서 외부함수인 outer의 실행이 종료되더라도 내부 함수인 inner 함수는 언젠가 outer2를 실행함으로써 호출될 가능성이 열려 있다. 언젠가 inner 함수의 실행 컨텍스트가 활성화되면 outerEnvironmentReference가 outer 함수의 LexicalEnvironment를 필요로 할 것이므로 수집 대상에서 제외된다. 때문에 inner 함수가 이 변수에 접근할 수 있다.

클로저 발생(예제 3) 시의 콜스택 흐름

정리하자면, 클로저는 어떤 함수에서 선언한 변수를 참조하는 내부 함수에서만 발생하는 현상이다.
예제 1과 2에서는 일반적인 함수와 같이 outer의 LexicalEnvironment에 속하는 변수가 모두 GC의 수거대상이 된 반면, 예제 3에서는 변수 a가 GC의 대상에서 제외되었다.

이처럼 함수의 실행 컨텍스트가 종료된 후에도 LexicalEnvironment가 GC의 수집 대상에서 제외되는 경우는, 지역변수를 참조하는 내부함수가 외부로 전달된 경우가 유일하다.

즉, 클로저는 "외부 함수의 LexicalEnvironment가 GC되지 않은 현상"이다.
다시 말해, 클로저는 "어떤 함수 A에서 선언한 변수 a를 참조하는 내부함수 B를 외부로 전달할 경우, A의 실행 컨텍스트가 종료된 이후에도 변수 a가 사라지지 않는 현상"이다.

주의할 점은, return 뿐만 아니라 다른 경우에서도 클로저가 발생한다는 점이다.

예제 4 ) return 없이 클로저가 발생하는 경우 1

// setIntercal / setTimeout

(function () {
  var a = 0;
  var intervalId = null;
  var inner = function () {
    if (++a >= 10) {
      clearInterval(intervalId);
    }
    console.log(a);
  };
  intervalId = setInterval(inner, 100);
})();

별도의 외부객체인 window의 메서드(setIntercal / setTimeout)에 전달할 콜백 함수 내부에서 지역변수를 참조한다.

예제 5 ) return 없이 클로저가 발생하는 경우 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);
})();

별도의 외부객체인 DOM의 메서드(addEventListener)에 등록할 handler 함수 내부에서 지역변수를 참조한다.

두 상황 모두 지역변수를 참조하는 내부 함수를 외부에 전달했기 때문에 클로저이다.


2. 클로저와 메모리 관리

클로저는 객체지향과 함수형 모두를 아우르는 매우 중요한 개념이다.
또한 메모리 소모는 클로저의 본질적인 특성이다. 이러한 특성을 정확히 이해하고 잘 활용하는 것이 중요하다.

메모리 누수의 위험을 이유로 클로저 사용을 지양해야한다는 의견이 있으나, 클로저와 같이 개발자가 의도적으로 참조 카운트를 0이 되지 않게 설계한 경우는 누수라고 할 수 없다.

클로저는 어떤 필요에 의해 의도적으로 함수의 지역변수를 메모리를 소모하도록 함으로써 발생한다. 그 필요성이 사라진 시점에는 더이상 클로저가 메모리를 소모하지 않도록 해야 한다.

보통 식별자에 참조형이 아닌 기본형 데이터(보통 null이나 undefined)를 할당하여 참조 카운트를 0으로 만든다.

예제 1 ) 클로저의 메모리 관리 1 : return에 의한 클로저의 메모리 해제

var outer = (function () {
  var a = 1;
  
  var inner = function () {
    return ++a;
  }
  return inner;
})();

console.log(outer());  // 2
console.log(outer());  // 3
outer = null;  // outer 식별자의 inner 함수 참조를 끊음

예제 2 ) 클로저의 메모리 관리 1 : 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 ) 클로저의 메모리 관리 1 : 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);
})();

3. 클로저 활용 사례

클로저는 다양한 곳에서 광범위하게 활용되고 있다. 실제로 어떤 상황에서 클로저가 등장하는지 살펴보자.

3-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);

($태그이름은 관례상 document.요소관련메서드()의 줄임말처럼 쓰인다. 아티클 참고)

위의 예제에서 fruits 변수를 순회하며 li를 생성하고, 각 li를 클릭하면 해당 이벤트 리스너에 기억된 콜백 함수를 실행하게 했다.

A(익명의 콜백 함수)는 그 내부에서 외부 변수를 사용하지 않고 있으므로 클로저가 없지만, B(addEventListener에 넘겨준 콜백 함수)는 fruit라는 외부 변수를 참조하고 있으므로 클로저가 있다.

A는 fruits의 요소 개수만큼 실행되며, 그때마다 새로운 실행 컨텍스트가 활성화된다.
A의 실행 종료 여부와 무관하게 클릭 이벤트에 의해 각 컨텍스트의 B가 실행될 때는, B의 outerEnvironmentReference가 A의 LexicalEnvironment를 참조한다.

따라서 A 함수가 종료된 후에도 B 함수가 참조할 예정인 변수 fruit는 GC 대상에서 제외되어 계속 참조가 가능하다.

이때 B 함수의 쓰임새가 콜백 함수에 국한되지 않는 경우라면, 반복을 줄이기 위해 B를 외부로 분리하여 fruit를 인자로 받아 출력하는 형태가 더 나을 수 있다. (아래 예제 2 확인)

예제 2 ) 콜백 함수와 클로저 2

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]);  // your choice is banana

예제 2에서는 콜백 함수를 공통 함수로 쓰고자 외부로 꺼내어 alertFruit라는 변수에 담았다.

그런데 각 li를 클릭하면 your choice is + fruit 형식의 텍스트가 아닌 [object MouseEvent]라는 값이 출력된다.

콜백 함수의 인자에 대한 제어권을 addEventListener가 가진 상태이며, addEventListener는 콜백 함수를 호출할 때 첫 번째 인자에 '이벤트 객체'를 주입하기 때문이다.

이 문제는 bind 메서드를 활용하여 해결할 수 있다. (예제 3 확인)

예제 3 ) 콜백 함수와 클로저 3

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));
  $ul.appendChild($li);
});

document.body.appendChild($ul);
alertFruit(fruits[1]);

이 방법은 bind의 특성상 함수 내부에서의 this가 원래의 this와 달라지고, 이벤트 객체가 인자로 넘어오는 순서(두번째로)가 바뀌는 특징이 있다.

이런 변경사항을 원치 않는 경우라면, bind 메서드가 아닌 고차함수를 활용하여 해결할 수 있다. (예제 4 확인)

예제 4 ) 콜백 함수와 클로저 4

고차함수: 함수를 인자로 받거나, 함수를 리턴하는 함수

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

var alertFruit = function (fruit) {  // 고차함수
  return function () {
    alert('your choice is ' + fruit);
  };
};

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

document.body.appendChild($ul);
alertFruit(fruits[1]);

alertFruit 함수가 익명함수를 반환하도록 수정했다.

$li.addEventListener('click', alertFruit(fruit)); 줄을 보면, alertFruit 함수를 실행하면서 fruit 값을 인자로 전달했다. 따라서 이 함수의 실행 결과가 다시 함수가 되며, 이렇게 반환된 함수를 리스너에 콜백 함수로써 전달할 것이다. 이후 클릭 이벤트가 발생하면 이 함수의 실행 컨텍스트가 열리면서 alertFruit의 인자로 넘어온 fruit를 outerEnvironmentReference에 의해 참조할 수 있다.

즉, alertFruit의 실행 결과로 반환된 함수에는 클로저가 존재한다.

소개한 세 방법의 장단점을 각기 파악하여, 구체적인 상황에 따라 어떤 방법을 도입하는 것이 가장 효과적일지 고민해보자.

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

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

접근 권한에는 public(외부에서 접근 가능), private(내부에서만 사용, 외부에 노출되지 않음), protected 세 종류가 있다.

자바스크립트는 기본적으로 변수 자체에 이러한 접근 권한을 직접 부여하도록 설계되어있지 않다.
그러나 클로저를 이용하면 함수 차원에서 public한 값과 private한 값을 구분하여 접근 권한을 제어할 수 있다.

예제 1 ) public

var outer = function () {
  var a = 1;
  
  var inner = function () {
    return ++a;
  };
  
  return inner;
};

var outer2 = outer();
console.log(outer2());  // 2
console.log(outer2());  // 3

예제 1에서 outer 함수를 종료할 때 inner 함수를 반환함으로써, outer 함수의 지역변수인 a의 값을 외부에서도 읽을 수 있게 되었다.

이처럼 클로저를 활용하면 외부 스코프에서 함수 내부의 변수들 중 선택적으로 일부의 변수에 대한 접근 권한을 return 을 통해 부여할 수 있다.

closure라는 영어 단어는 사전적으로 '닫혀있음, 폐쇄성, 완결성' 이라는 의미를 가진다. '폐쇄성'에 주목해보면, 예제 1이 조금 다르게 느껴질 수 있다.

outer 함수는 외부(전역 스코프)로부터 철저하게 격리된 닫힌 공간이다. 외부에서는 외부 공간에 노출되어있는 outer 함수를 실행할 수는 있지만, outer 함수 내부에는 어떠한 개입도 할 수 없다. 외부에서는 오직 outer 함수가 return 한 정보에만 접근할 수 있다.
즉, return 값이 외부에 정보를 제공하는 유일한 수단이다.

이러한 흐름을 이용하여 외부에 제공하고자 하는 정보를 모아 return 하고, 내부에서만 사용할 정보들은 return 하지 않는 방법으로 접근 권한 제어가 가능하다.
return 한 변수들은 public member(공개 멤버), return 하지 않은 변수들은 private member(비공개 멤버)가 된다.

3-2-1. 접근 권한 제어 예시 (보드게임)

간단한 보드 게임을 통해 접근 권한을 제어해보자.

자동차 경주 게임이며, 규칙은 다음과 같다.

  • 각 턴마다 주사위를 굴려 나온 숫자만큼 이동한다.
  • 차량별로 연료량(fuel)과 연비(power)는 무작위로 생성된다.
  • 남은 연료가 이동할 거리에 필요한 연료보다 부족하면 이동하지 못한다.
  • 모든 유저가 이동할 수 없는 턴에 게임이 종료된다.
  • 게임 종료 시점에 가장 멀리 이동해 있는 사람이 승리한다.

예제 1 ) 자동차 객체

var car = {
  fuel: Math.ceil(Math.random() * 10 + 10),
  power: Math.ceil(Math.random() * 3 + 2),
  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 객체를 사람 수만큼 생성하여 각자의 턴에 run을 실행하면서 게임을 즐겨보기 전에,
무작위로 정해지는 연료, 연비, 이동거리를 마음대로 바꿀 수 없게 클로저로 미리 제어 해보자.

클로저를 활용하기 위해서는 위의 객체예제를 함수로 변경하고, 필요한 멤버만을 return 하도록 수정해야 한다.

예제 2 ) 클로저로 변수를 보호한 자동차 객체 1

var createCar = function () {
  var fuel = Math.ceil(Math.random() * 10 + 10);
  var power = Math.ceil(Math.random() * 3 + 2);
  var moved = 0;
  return {
    get moved () {
      return moved
    },
    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)`);
    }
  };
};

var car = createCar();

fuel, power 변수는 비공개 멤버로 지정해 외부에서의 접근을 제한했고, moved 변수는 getter만을 부여함으로써 읽기 전용 속성을 부여했다.

이제 외부에서는 'run 메서드 실행, 현재의 moved 값 확인'이라는 두가지 동작만 할 수 있다.

이 예제에서 run 메서드를 다른 내용으로 덮어씌우는 어뷰징까지 가능하지 않도록 보완해보자.

예제 3 ) 클로저로 변수를 보호한 자동차 객체 2

var createCar = function () {
  var fuel = Math.ceil(Math.random() * 10 + 10);
  var power = Math.ceil(Math.random() * 3 + 2);
  var moved = 0;
  var publicMembers = {
    get moved () {
      return moved
    },
    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)`);
    }
  };
  Object.freeze(publicMembers);
  return publicMembers;
};

var car = createCar();

Object.freeze() 메서드를 통해 동결된 객체가 더 이상 변경될 수 없도록 처리했다.

정리: 클로저를 활용하여 접근권한을 제어하는 방법

  1. 함수에서 지역변수 및 내부함수 등을 생성한다.
  2. 외부에 접근권한을 주고자 하는 대상들로 구성된 참조형 데이터(대상이 여럿일 때는 객체 또는 배열, 하나일 때는 함수)를 return 한다.

=> return 한 변수 = 공개 멤버 / return 하지 않은 변수 = 비공개 멤버

3-3. 부분 적용 함수

부분 적용 함수(partially applied function): n개의 인자를 받는 함수에 미리 m개의 인자만 넘겨 기억시켰다가, 나중에 (n-m)개의 인자를 넘기면 비로소 원래 함수의 실행 결과를 얻을 수 있게끔 하는 함수.

(this를 바인딩해야하는 점을 제외하면) bind 메서드의 실행 결과가 부분 적용 함수이다.

예제 1 ) 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));

addPartial 함수는 인자 5개를 미리 적용하고, 추후 추가적으로 인자들을 전달하면 모든 인자를 모아 원래의 함수가 실행되는 부분 적용 함수이다. add 함수는 this를 사용하지 않으므로 bind 메서드만으로도 문제 없이 구현되었다.

예제 2 ) 부분 적용 함수 직접 구현 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));
  };
};

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('입니다!');  // '왈왈, 강아지입니다!'

partial 함수는 첫 번째 인자에 원본 함수를 두 번째 인자 이후부터는 미리 적용할 인자들을 전달하고, 반환할 함수(부분 적용 함수)에서는 다시 나머지 인자들을 받아 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(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, 3, 4, 5);
console.log(addPartial(6, 7, 8, 9, 10));  // 55

var dog = {
  name: '강아지',
  greet: partial2(function(prefix, suffix) {
    return prefix + this.name + suffix;
  }, '왈왈, ')
};

dog.greet(' 배고파요!');  // '왈왈, 강아지 배고파요!'

'비워놓음'을 표시하기 위해 미리 전역객체에 _라는 프로퍼티를 준비하면서 삭제 변경 등의 접근에 대한 방어 차원에서 여러 가지 프로퍼티 속성을 설정했다.
이를 통해 처음에 넘겨준 인자들 중 _로 비워놓은 공간마다 나중에 넘어온 인자들이 차례대로 끼워넣도록 구현했다.

부분 적용 함수를 만들 때, 미리부터 실행할 함수의 모든 인자 개수를 맞춰 빈 공간을 확보하지 않아도 된다. 함수 실행 시 인자 개수와 상관없이 실행이 잘 이루어질 것이다.

직접 구현한 부분 적용 함수 2개는 모두 클로저를 핵심 기법으로 사용했다. '미리 일부 인자를 넘겨두어 기억하게끔 하고 추후 필요한 시점에 기억했던 인자들까지 함께 실행하게 한다'는 개념이 클로저의 정의에 정확히 부합한다.

예제 2 ) Debounce 개념을 이용한 부분 적용 함수

디바운스: 짧은 시간 동안 동일한 이벤트가 많이 발생할 경우 이를 전부 처리하지 않고 처음 또는 마지막에 발생한 이벤트에 대해 한 번만 처리하는 기능.

프론트엔드 성능 최적화에 큰 도움을 주며, scroll, wheel, mousemove, resize 등에 적용하기 좋다.

마지막에 발생한 이벤트만 처리해도 괜찮고, 어느 정도의 시간 지연이 크게 문제되지 않는 경우라면, Lodash 같은 라이브러리를 사용하지 않고 직접 간단하게 구현할 수 있다.

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));

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

반환될 함수 내부에서는 this를 self라는 변수에 담고, clearTimeout을 통해 대기큐를 초기화한다.
그 다음 setTimeout에서 wait 시간만큼 지연시킨 다음, 원래의 func을 호출한다.

해당 함수를 호출하면, 최초 event가 발생할 때 timeoutId = setTimeout(func.bind(self, event), wait); 줄에 의해 'wait 시간 뒤에 func를 실행할 것'이라는 내용이 대기열에 담긴다.
이 때 wait 시간이 경과하기 전에 동일한 이벤트가 발생하면, clearTimeout(timeoutId); 줄에 의해 앞서 저장했던 대기열을 초기화하고, 새로운 대기열을 다시 담는다.
동일한 이벤트의 발생 없이 wait 시간이 초과하면 해당 이벤트가 실행된다.

이 debounce 함수에서 클로저로 처리되는 변수는 eventName, func, wait, timeoutId이다.

3-4. 커링 함수

추후 추가 예정

4. 정리

  1. 클로저란?

    • 어떤 함수에서 선언한 변수를 참조하는 내부함수를 외부로 전달할 경우, 함수의 실행 컨텍스트가 종료된 후에도 해당 변수가 사라지지 않는 현상이다.
    • 외부 함수의 LexicalEnvironment가 GC되지 않은 현상이다.
    • 어떤 함수 A에서 선언한 변수 a를 참조하는 내부함수 B를 외부로 전달할 경우, A의 실행 컨텍스트가 종료된 이후에도 변수 a가 사라지지 않는 현상이다.
    • 지역변수를 참조하는 내부 함수를 외부에 전달한 후 지역변수가 계속 메모리에 남아있는 현상이다.
  2. 내부 함수를 외부로 전달하는 방법은 '함수를 return / 콜백으로 전달'이 있다.

  3. 클로저는 그 본질이 메모리를 계속 차지하는 개념이므로 더는 사용하지 않게된 클로저는 GC이 되도록 관리해줄 필요가 있다.

  4. 클로저는 다양한 곳에서 활용할 수 있는 중요한 개념이다.

profile
💡 Software Engineer - F.E

1개의 댓글

comment-user-thumbnail
2021년 6월 3일

클로저로 변수를 보호한 자동차 객체 예제 2, 예제 3 코드 틀린 부분이 있습니다.
첫 번째 예제와 다르게 예제2,3 번은 함수를 실행 했기 때문에 this 키워드가 필요가 없습니다!
// 개발자 도구 scope start
Local
km: 4
this: Object
moved: (...)
run: ƒ ()
get moved: ƒ moved()
proto: Object
// 개발자 도구 scope end
car.run() : run()은 car의 메서드로써 호출했기 때문에 run이 실행되면 this는 위와같이 moved, run(f),get moved(f)를 가리키게 됩니다!
글 잘봤습니다!

답글 달기