5 클로저

클로저

클로저는 함수형 프로그래밍 언어에서 보편적으로 등장하는 “특성”이다.

MDN에서는 “A closure is the combination of a function and the lexical environment within which that function was declared.” 라고 소개하는데, 직역하면 “함수와 함수가 선언될 당시의 lexical environment의 상호관계에 따른 현상” 이다. 여기서 선언될 당시의 lexical environment는 바로 실행 컨텍스트의 outerEnvironmentReference이다. 외부 변수와 어떤 함수 내부의 상호관계에 따라 발생하는 현상이라고 볼 수 있다.

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

var inner2 = outer(); // 반환된 inner 함수가 들어감
console.log(inner2()); // 1
console.log(inner2()); // 2

이렇게 함수를 반환하면 외부에서도 inner 함수를 호출할 수 있는데, a 변수가 어떻게 계속해서 참조될 수 있는 것인지 의문이 생긴다. var inner2 = outer(); 라인에서 outer 함수가 이미 실행이 종료되었기 때문에 실행 컨텍스트가 콜스택에서 사라졌을 텐데, 여전히 inner2(inner)에서 a를 참조하고 있다. 이는 가비지 컬렉터의 동작 방식 때문이다. 가비지 컬렉터는 어떤 값을 참조하고 있는 변수가 하나라도 있다면 그 값은 제거 대상에서 제외한다. a 역시 사라졌어야 했지만 inner 함수 안에서 참조되고 있고, 계속해서 참조될 수 있기 때문에 제거하지 않고 남겨둔 것이다.

클로저의 정의를 다시 고쳐보면.

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

주의할 점은 외부로 전달하는 경우가 반드시 return만을 의미하는 것은 아니라는 것이다. 콜백 함수에서 지역 변수를 참조하는 것 역시 클로저이다.

클로저와 메모리

클로저는 일반적인 상황에서 가비지 컬렉팅 되었어야 할 메모리를 제거하지 않고 계속 갖고 있는 것이다. 따라서 클로저를 만드는 것이 메모리 누수는 아니지만, 불필요한 클로저를 많이 갖고 있다면 메모리를 효율적으로 사용하지 못하고 있는 것은 맞다. 따라서 사용이 종료된 클로저는 참조를 해제해 줘야 한다.

참조를 해제하는 방법은 식별자에 참조형이 아닌 기본형 데이터(보통 null이나 undefined)를 할당하는 것이다.

클로저의 활용

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

  2. 정보 은닉 (필요한 정보만 노출하는 것)

    외부에서는 오직 return한 정보에 대해서만 접근할 수 있다.

    // 이렇게 선언하면 car.power = 100000; 이렇게 맘대로 수정할 수 있다.
    var car = {
      fuel : 10000, // 연료
      power : 100, // 연비 (고정)
      moved: 1000, // 이동거리 (직접 변경 x)
    }
    
    var createCar = function () {
      var fuel = 10000;
      var power = 100;
      var moved = 1000;
      
      return {
        // 읽기 전용 속성
        get moved () {
          return moved
        },
        
        // 1km 이동
        run () {
          // 연비에 따라 연료 감소
          // 연비에 따라 이동거리 증가...
        }
      }
    }
    
    var car = createCar();
    car.power = 100000; // 외부에서 접근이 불가능하다. (실패!)
    console.log(car.moved); // 1000 (읽기만 가능)
  3. 부분 적용 함수 : n개의 인자를 받는 함수에 m개의 인자만 넘겨서 기억시켰다가 나중에 나머지 인자를 넘기면 원래 함수의 실행 결과를 얻을 수 있는 함수. bind와 비슷한 개념이다. 부분 적용 함수는 디바운스에 활용하면 좋다.

  4. 커링 함수 : 여러 개의 인자를 받는 함수를 하나의 인자만 받는 함수로 나눠서 순차적으로 호출될 수 있게 체인 형태로 구성한 것. 상위 매개변수를 계속 기억하는 클로저 덕분에 가능한 구조이다.

    let curry5 = func => a => b => c => d => e => func(a, b, c, d, e);

    원하는 시점까지 기다렸다가 실행하는 지연 실행이 필요한 상황이나, 함수의 매개변수가 비슷하고 한 개만 계속 바뀌는 상황에 유용하게 쓰일 수 있다.

    let getFetchData = baseUrl => path => id => fetch(baseUrl + path + "/" + id);
    
    // 이미지 타입별 요청 함수
    let getImage = getFetchData("https://dummyimage.com/");
    let getSquareImage = getImage("600x600");
    
    // 색깔별 정사각 이미지
    const blackSquareImage = getSquareImage("000");
    const whiteSquareImage = getSquareImage("fff");