[JavaScript] 클로저(Closure)


1. 클로저

  • 여러 함수형 프로그래밍에서 등장하는 보편적인 특성
  • 어떤 함수에서 선언한 변수를 참조하는 내부함수에서만 발생하는 현상
  • 어떤 함수 A에서 선언한 변수 a를 참조하는 내부함수 B를 외부에 전달할 경우 A의 실행 컨텍스트가 종료된 이후에도 변수 a가 사라지지 않는 현상

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

var outer = function () {
  var a = 1;
  var inner = function () {
    console.log(++a);
  };
  inner();
};
outer(); // 1
  • outer 함수에서 변수 a를 선언하고 outer 내부 함수인 inner 함수에서 a의 값을 1만큼 증가 시키고 출력 ⇒ inner 함수 내부에서 a를 선언하지 않았기 때문에 outerEnvironmentReference에 지정된 상위 컨텍스트 outerLexicalEnvironment에 접근해 a를 찾음
  • outer 함수의 실행 컨텍스트가 종료되면 LexicalEnvironment에 저장된 식별자에 대한 참조를 지우고 가비지 컬렉터 수집대상에 포함된다

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

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

var outer2 = outer();
console.log(outer2); // 2
  • 이 함수도 마찬가지로 실행 컨텍스트가 종료되면 참조 대상이 없어진다

(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 함수의 LexicalEnvironment 접근 가능 이유

  • 가비지 컬렉터의 동작 방식 때문 ⇒ 어떤 값을 참조하는 변수가 하나라도 있다면 그 값은 수집대상에 포함시키지 않음
  • 따라서 outer의 실행이 종료되도 inner 함수는 언젠가 outer2를 실행함으로써 호출될 가능성이 열린다
  • inner함수의 실행 컨텍스트가 활성화되면 outerEnvironmentReferenceouter 함수의 LexicalEnvironment를 필요로 하면서 수집 대상에서 제외됌

(4) return없이 클로저가 발생하는 경우

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

// 1
// 2
// 3
// 4
// 5
// 6
// 7
// 8
// 9
// 10
  • setInterval에 전달할 콜백 함수 내부에서 지역 변수를 참조 ⇒ 지역변수를 참조하는 내부함수를 외부에 전달함

2. 클로저 메모리 관리

  • 객체 지향 및 함수형에서 매우 중요한 개념임
  • 메모리 누수는 개발자의 의도와 달리 참조 카운트가 0이 되지 않아서 Gabage Collector의 수거 대상이 되지 않는 경우이다

참조 카운트를 0으로 만드는 방법은 식별자에 참조형이 아닌 기본형 데이터(null 또는 undefined)를 할당함


(1) return에 의한 클로저의 메모리 해제

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

console.log(outer());
console.log(outer());
console.log(outer);
outer = null; // outer 식별자에 null값을 직접 할당하여 inner 함수의 참조를 끊음
console.log(outer);
2
3
[Function: inner]
null

[Done] exited with code=0 in 0.054 seconds

(2) setInterval에 의한 클로저의 메모리 해제

(function () {
  var a = 0;
  var intervalID = null;
  var inner = function () {
    if (++a >= 10) {
      clearInterval(intervalID);
      inner = null; // inner 식별자에 null값을 할당하여 함수 참조를 끊음
    }
    console.log(a);
  };
  intervalID = setInterval(inner, 1000);
})();
1
2
3
4
5
6
7
8
9
10

[Done] exited with code=0 in 10.05 seconds

(3) eventListener에 의한 클로저의 메모리 해제

(function () {
  var count = 0;
  var button = document.createElement("button");
  button.innerText = "click";
  var clickHandler = function () {
    console.log(++count, "time clicked");
    if (count >= 10) {
      button.removeEventListener("click", clickHandler);
      clickHandler = null; // clickHandler 식별자에 null값을 할당하여 함수 참조를 끊음
    }
  };
})();

3. 클로저 활용

(1) 콜백 함수 내부에서 외부 데이터 가져올때

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

var alertFruitBuilder = function (fruit) {
  return function () {
    alert(`your choice is ${fruit}`);
  };
};

fruit.forEach(function (fruit) {
  var $li = document.createElement("li");
  $li.innerText = fruit;
  $li.addEventListener("click", alertFruitBuilder(fruit));
  $ul.appendChild($li);
});
  • 함수 내부에서 익명 함수를 반환
  • 함수의 실행 결과가 함수가 되고 반환된 함수를 콜백 함수로 전달
  • 클릭 이벤트가 발생하면 실행 컨텍스트가 열리고 인자로 넘어온 fruit을 outerEnvironmentReference에 참조가 가능해짐

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

자동차 경주 게임

  • 각 턴마다 주사위를 굴려 나온 숫자 (km)만큼 이동
  • 차량별 연료량(fuel)과 연비(power)는 무작위 생성
  • 남은 연료가 이동할 거리에 필요한 여뇰보다 부족하면 이동 불가
  • 모든 유저가 이동할 수 없는 턴에 게임 종료
  • 게임 종료 시점에 가장 멀리 이동해 있는 사람 승리
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 / power;
    if (fuel < wasteFuel) {
      console.log("이동 불가능");
      return;
    }
    fuel -= wasteFuel;
    moved += km;
    console.log(`${km} km 이동 (총 ${moved} km) 남은 연료: ${fuel}`);
  },
};
  • 자동차 객체를 만들어서 실행시켰지만 누군가 어뷰징을 사용하면 무작위 값이 변경된다.

    car.fuel = 1000; // 어뷰징으로 연료에 1000 할당
    console.log("어뷰징 후 연료", car.fuel);
    car.run();
    
    car.power = 100; // 어뷰징으로 연비에 1000 할당
    console.log("어뷰징 후 연비", car.power);
    car.run();
    
    car.moved = 1000; // 어뷰징으로 거리를 1000 할당
    console.log("어뷰징 후 총 이동거리", car.moved);
    
  • 어뷰징을 했을때 결과값 변경
    어뷰징 후 연료 1000
    1 km 이동 (총 1 km) 남은 연료: 999.75
    어뷰징 후 연비 100
    2 km 이동 (총 3 km) 남은 연료: 999.73
    어뷰징 후 총 이동거리 1000
    
  • 이럴때 클로저를 활용해서 객체가 아닌 함수로 만들어주고 필요한 멤버만 return 하는 방법으로 만들어야 한다

  • createCar 함수를 실행하여 객체를 생성하고 fuel , power 변수는 비공개 멤버로 지정해서 외부 접근을 제한
  • moved 변수는 getter만 부여하여 읽기 전용 속성을 부여
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 = {
    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}`);
    },
  };
  Object.freeze(publicMembers);
  return publicMembers;
};

var car = createCar();

car.run();

console.log(car.moved);
console.log(car.fuel);
console.log(car.power);

car.fuel = 1000; // 어뷰징으로 연료에 1000 할당
console.log("어뷰징 후 연료", car.fuel);
car.run();

car.power = 100; // 어뷰징으로 연비에 1000 할당
console.log("어뷰징 후 연비", car.power);
car.run();

car.moved = 1000; // 어뷰징으로 거리를 1000 할당
console.log("어뷰징 후 총 이동거리", car.moved);
car.run();
어뷰징 후 연료 undefined
1 km 이동 (총 4 km) 남은 연료: 17.666666666666668
어뷰징 후 연비 undefined
6 km 이동 (총 10 km) 남은 연료: 15.666666666666668
어뷰징 후 총 이동거리 10
2 km 이동 (총 12 km) 남은 연료: 15.000000000000002
  • 객체를 리턴하기 전에 미리 변경 할 수 없게 만듬

(3) 부분 적용 함수

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

실무에선 주로 디바운스에서 자주 사용

  • 짧은 시간 동안 동일 이벤트가 많이 발생할 경우 이를 전부 처리하지 않고 처음 또는 마지막 발생 이벤트에 대해 한번만 처리
  • 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("마우스 이벤트");
};
var wheelHandler = function (e) {
  console.log("마우스 휠 이벤트");
};

document.body.addEventListener("mousemove", debounce("move", moveHandler));
document.body.addEventListener("mousewheel", debounce("wheel", wheelHandler));
  • eventName , 실행 함수(func), 마지막 이벤트 여부 판단을 위한 wait을 받음
  • setTimeout을 사용하기 위해 this를 별도의 변수로 담고 clearTimeout으로 대기큐를 초기화
  • setTimeout으로 wait 시간만큼 지연 시키고 원래의 func를 호출함
  • 따라서 wait 시간이 경과하기 이전에 동일한 event가 발생하면 대기열을 초기화 시키고 timeoutId에 새로운 대기열을 등록함 ⇒ 마지막 발생 이벤트만이 초기화 되지 않고 실행됌

(4) 커링 함수

  • 여러 개의 인자를 받는 함수를 하나의 인자만 받는 함수로 나눠서 순차적으로 호출될 수 있게 체인 형태로 구성 한 것
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과 8 큰 값 10
console.log(getMaxWith10(25)); // 10과 25 큰 값 25

var getMaxWith10 = curry3(Math.min)(10);
console.log(getMaxWith10(8)); // 10과 8 작은 값 8
console.log(getMaxWith10(25)); // 10과 25 작은 값 10
10
25
8
10

[Done] exited with code=0 in 0.056 seconds

커링 함수의 장단점

장점

  • 마지막 단계에서 참조 하므로 GC 되지 않고 메모리에 쌓였다가 마지막 호출로 실행 컨텍스트가 종료될 때 한번에 GC 수거 대상이 됌

단점

  • 인자가 많을 수록 가독성이 떨어짐

활용

var getInformation = function (baseUrl) {
  // 서버에 요청할 주소
  return function (path) {
    // path 값
    return function (id) {
      // id 값
      return fetch(`${baseUrl}${path}/${id}`); // 실제 서버에 정보 요청
    };
  };
};

ES6 문법

var getInformation = (baseUrl) => (path) => (id) =>
  fetch(`${baseUrl}${path}/${id}`);

Redux Middleware

// Redux Middleware
const thunk = (store) => (next) => (action) => {
  return typeof action === "function"
    ? action(dispatch, store.getState)
    : next(action);
};

Redux logger나 thunk에 store,next 미리 넘겨서 반환 함수를 저장하고 이후에 action만 받아서 처리가능하다