[JavaScript] Closure

Hyein Son·2020년 8월 24일
0

JavaScript

목록 보기
5/10

closure는 함수와 그 함수가 선언됐을 때의 렉시컬 환경(Lexical environment)과의 조합이다.

Lexical Scope

  • 함수를 호출할 때가 아니라 어디에서 선언했는지에 따라 결정되는 것을 Lexical scoping이라고 한다.
function outerFunc() {
  var x = 10;
  var innerFunc = function () { console.log(x); };
  innerFunc();
}

outerFunc(); // 10

함수 innerFunc는 함수 outerFunc의 내부에서 선언되었기 때문에 innerFunc의 상위 스코프는 outerFunc이다. 만약 innerFunc가 전역에서 선언되었으면 innerFunc의 상위스코프는 전역 스코프다.

따라서 innerFunc은 자신이 속한 Lexical Scope를 참조할 수 있다.
내부함수 innerFunc이 자신을 포함하고 있는 외부함수 outerFunc의 변수 x에 접근하는 것은 Lexical Scope의 레퍼런스를 차례대로 저장하는 실행 컨텍스트의 스코프 체인(실행 컨텍스트가 참조할 수 있는 변수, 함수 선언 등의 정보를 담고 있는 리스트)을 자바스크립트 엔진이 검색했기 때문이다.(하위 > 상위 > 전역 순서로 참조한다. 검색에 실패하면 정의되지않은 변수에 접근하는 것이기 때문에 에러 발생)


Closure

위의 코드를 내부함수 innerFunc를 outerFunc내에서 반환하도록 변경한다.

function outerFunc() {
  var x = 10;
  var innerFunc = function () { console.log(x); };
  return innerFunc;
}

/*
 함수 outerFunc를 호출하면 내부 함수 innerFunc가 반환 후
 함수 outerFunc의 실행 컨텍스트는 소멸하므로 outerFunc의 변수 x는 더이상 유효하지 않게 된다. 
 */
var inner = outerFunc();
inner(); // 10 >>> 하지만 outerFunc의 지역변수 x가 동작

자신을 포함하고 있는 외부함수보다 내부함수가 더 오래 유지되는 경우, 외부 함수밖에서 내부함수가 호출되더라도 지역변수에 접근할 수 있다. 이것을 클로저(Closure)라고 부른다. 외부함수 실행 컨텍스트내의 활성객체(실행에 필요한 변수, 함수 선언등의 정보를 담고 있는 객체)는 내부함수에 의해 참조되는 한 유효하기 때문에 스코프체인을 통해 참조할 수 있다. 이때 변수의 복사본이 아니라 실제 변수에 접근한다는 것에 주의해야한다.

클로저(Closure)는 반환된 내부함수가 자신이 선언됐을때의 환경(Lexical Scope)를 기억해 Lexical Scope밖에서 호출되어도 그 스코프에 접근할 수 있는 함수를 말한다. 자신이 생성될 때의 환경을 기억하는 함수이다.


return 없이도 Closure가 발생하는 경우

setInterval / setTimeout

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

함수 A의 실행컨텍스트가 종료된 이후에도 setInterval에 전달할 콜백 함수(inner)내부에서 지역변수 a를 참조한다.

eventListener

function B () {
  var count = 0;
  var button = document.createElement('button');
  button.innerText = 'click';
  button.addEventListener('click', function () {
    console.log(++count, 'times clicked');
  });
  document.body.appendChild(button);
}
B();

함수 B의 실행컨텍스트가 종료된 이후에도 버튼을 클릭할 때마다 지역변수 count를 참조한다.


Closure 활용

1. 상태 유지

내부함수를 반환하는 외부함수에서 상태를 나타내는 변수로 선언하고 그 변수를 기억하는 클로저인 내부함수에서 변수의 값을 변경한다. 이때 외부함수의 변수는 클로저에 의해 참조되기 때문에 자신의 변경된 최신 상태를 계속 유지한다. 이처럼 현재상태를 기억하고 이 상태가 변경되어도 최신상태를 유지하는 상황에 유용하다. 클로저가 없으면 전역변수를 사용해야하는데 전역변수는 언제든지 접근하고 변경 가능하기 때문에 오류의 원인이 된다.


2. 전역 변수의 사용 억제

var incleaseBtn = document.getElementById('inclease');
var count = document.getElementById('count');

var increase = (function () {
  // 카운트 상태를 유지하기 위한 자유 변수
  var counter = 0;
  // 클로저를 반환
  return function () {
    return ++counter;
  };
}());

incleaseBtn.onclick = function () {
  count.innerHTML = increase();
};

increase내부에서 반환된 클로저는 increase가 호출된 후 소멸되어도 자유 변수 counter를 기억하기 때문에 counter에 접근할 수 있다. 변수 counter는 외부에서 접근할 수 없기 때문에 전역 변수를 사용했을 때 발생하는 의도하지 않은 변경을 걱정할 필요가 없다.


3. 콜백 함수 내부에서 외부 데이터 사용

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

addEventListener는 콜백 함수를 호출할 때 첫 번째 인자에 '이벤트 객체'를 주입한다. 함수 alertFruit를 콜백 함수로 사용하면 li를 클릭시 클릭한 대상의 과일명이 아닌 [object MouseEvent]라는 값이 출력된다. 이를 해결하기 위해 bind메서드를 사용해 fruit를 인자로 넘겨준다. bind메서드의 첫 번째 인자로 바인딩할 객체가 들어가기 때문에 this가 addEventListener를 호출한 주체가 아닌 null로 바뀌는 단점이 있다.

this가 바뀌는 것을 해결하기 위해서 고차함수(함수를 인자로 받거나 함수를 리턴하는 함수)를 사용할 수 있다. 이를 사용하면 클로저를 활용하게 된다.

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

함수 alertFruitBuilder가 실행하면서 fruit를 인자로 넘겨 함수를 반환한다. 클릭하면 반환된 함수의 실행 컨텍스트가 열리면서 인자로 넘어온 fruit를 참조한다.


4. 정보의 은닉

function Counter() {
  // 카운트를 유지하기 위한 자유 변수
  var counter = 0;

  // this로 바인딩해 외부에서 접근이 가능한 함수형태의 프로퍼티(메소드), 클로저
  this.increase = function () {
    return ++counter;
  };

  // this로 바인딩해 외부에서 접근이 가능한 함수형태의 프로퍼티(메소드), 클로저
  this.decrease = function () {
    return --counter;
  };
}

// 함수 Counter : 메소드 increase, decrease를 갖는 인스턴스 생성
const counter = new Counter();

console.log(counter.increase()); // 1
console.log(counter.decrease()); // 0
console.log(counter.counter); // undefined

메소드 increase, decrease는 Counter의 스코프에 속한 변수 counter를 기억하는 클로저이며 렉시컬 환경을 공유한다. Counter 내부에 있는 변수 counter는 this로 바인딩된 프로퍼티가 아니라 변수이다. this로 바인딩이 되면 외부에서 접근할 수 있는 public프로퍼티가 되지만 counter는 Counter 내부에서만 접근할 수 있으므로 private변수이다. Counter 스코프에 의해 보호되고 외부에서 마음대로 접근할 수 없게 되는 것이다. return한 변수(increase, decrease)들은 public프로퍼티가 되고 그렇지 않은 변수(counter)들은 private 프로퍼티가 된다. 메소드 increase, decrease는 클로저이므로 Lexical scope내의 변수 counter에 접근할 수 있다.


5. 부분 적용 함수

n개의 인자를 받는 함수에 미리 m개의 인자만 넘겨 기억시켰다가, 나중에 (n-m)개의 인자를 넘기면 원래 함수의 실행 결과를 얻도록 하는 함수이다. bind메서드를 사용하면 부분 적용 함수를 사용할 수 있지만 this를 바인딩하기 때문에 this값이 변화하는 단점이 있다. 이를 해결하기 위해서 클로저를 사용해 this에 관여하지 않는 함수를 구현할 수 있다. 여러개의 인자를 먼저 전달하고 다시 재실행할 때 원본 함수가 실행된다.
실무에서는 디바운스를 부분 함수로 사용한다.

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 처리');
};

document.body.addEventListener('mousemove', debounce('move', moveHandler, 500));

mousemove이벤트가 발생할 때마다 인자로 넘어온 moveHandler가 500ms후에 실행된다. 500ms가 다 지나기전에 이벤트가 발생하게 되면 이전에 저장한 timeoutId가 clearTimeout에 의해 초기화된다. 새로운 timeoutId를 저장한다. 결국 마지막에 발생한 이벤트만 초기화되지 않고 moveHandler가 실행된다.


6. 커링 함수

여러 개의 인자를 받는 함수를 하나의 인자만 받는 함수로 나눠서 순차적으로 호출되도록 체인 형태로 구성한 것이다. 마지막 인자가 전달되기 전까지 원본 함수가 실행되지 않는다. 당장 가지고 있는 정보만 받아서 전달하고 또 필요한 정보가 들어오면 전달하는 식으로 마지막 인자가 넘어갈 때까지 함수 실행을 미루는데 이를 지연실행이라고 한다.

var curry = function (func) {
  return function (a) {
    return function (b) {
      return function (c) {
        return func(a,b,c);
      }
    }
  }
}
var getMax = curry(Math.max);
console.log(getMax(1)(2)(3));

인자가 많아질수록 가독성이 떨어진다는 단점이 있다. es6의 화살표 함수를 사용하면 한 줄에 표기할 수 있다.

var curry = func => a => b => c => func(a,b,c);

redux 미들웨어 thunk는 커링 함수를 사용한 경우다.

const thunk = store => next => action => {
  return typeof action === 'function'
  ? action(dispatch, store.getState)
  : next(action);

thunk에 store와 next를 미리 넘겨서 반환된 함수에 저장시켜놓고, 이후에 action만 받아서 처리한다.


참고
poiemaweb, 코어 자바스크립트

0개의 댓글