프론트엔드 개발

코어 자바스크립트 - 클로저 본문

후기/코어 자바스크립트

코어 자바스크립트 - 클로저

태나미 2021. 10. 7. 08:31
이 글은 코어 자바스크립트 책에서 클로저를 공부하고 정리하는 목적으로 남깁니다.

목표

  • 클로저의 개념과 메모리 관리에 대해 알 수 있습니다.
  • 커링함수에 대해 알 수 있습니다.

목차

    1. 클로저의 의미 및 원리
    2. 클로저와 메모리 관리
    3. 클로저 활용사례
    4. 정리

클로저의 의미 및 원리

클로저는 함수와 그 함수가 선언될 당시의 lexical environment의 상호관계에 따른 현상 - MDN

예시 1)

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

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

 

예시 1에서는 outer 함수의 실행 컨텍스트가 종료되기 이전에 inner 함수의 실행 컨텍스트가 종료되어 inner 함수를 호출할 수 없습니다. 그러면 outer함수의 실행 컨텍스트가 종료된 후에도 inner 함수를 호출할 수 있을지 궁금증이 생깁니다.

예시 2)

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

var outer2 = outer();

console.log(outer2());  // 2
console.log(outer2());  // 3

예시 2에서는 inner 함수 자체를 반환하였고, outer 함수의 실행 컨텍스트가 종료될 때 outer2 변수는 inner 함수를 참조하게 될 것입니다. 이후 outer2를 호출하면 반환된 함수인 inner 함수가 실행됩니다. inner 함수의 실행 시점에는 outer 함수는 이미 종료된 상태인데 outer 함수의 LexicalEnvironment에 접근할 수 있는 이유는 가비지 컬렉터의 동작 방식 때문입니다. inner 함수의 실행 컨텍스트가 활성화되면 outer 함수의 LexicalEnvironment를 필요로 할 것이므로 수집 대상에서 제외됩니다.

 

앞서 클로저를 다시 정리해보면 어떤 함수 A에서 선언한 변수 a를 참조하는 내부 함수 B를 외부로 전달할 경우 A의 실행 컨텍스트가 종료된 이후에도 a가 사라지지 않는 현상을 말합니다.

 

외부로 전달하는 경우에는 return 외에도, setInerval/setTimeout, eventListener 등이 있습니다.

클로저와 메모리 관리

'메모리 누수'는 어떤 값의 참조 카운트가 0이 되지 않아 GC(Garbage Collector)의 수거 대상이 되지 않는 것입니다.

'메모리 소모'에 대한 관리법은  클로저가 참조하고 있는 변수를 참조 해제시키면 되는데, 참조 카운트를 0으로 만드는 방법을 이용할 수 있습니다. 식별자에 기본형 데이터 (null이나 undefined)를 할당하면 됩니다.

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

var outer2 = outer();

console.log(outer2());
console.log(outer2());

outer = null    // outer 식별자의 inner 함수 참조 해제

클로저 활용 사례

예제 3)

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("당신의 선택은 " + fruit);
  });
  $ul.appendChild($li);
});

document.body.appendChild($ul);

예제 3에서, 콜백 함수(B)에는 fruits라는 외부 변수를 참조하고 있으므로 클로저가 존재합니다.

 

(A)는 fruits의 개수만큼 실행되며, 그때마다 새로운 실행 컨텍스트가 활성화되고, (B)가 실행될 때는 (B)의 outerEnvironmentReference가 (A)의 LexicalEnvironment를 참조하게 됩니다. 

 

따라서 (B) 함수가 참조할 예정인 변수 fruits에 대해서 A가 종료된 후에도 GC대상에서 제외됩니다.

 

예제 4)

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

var alertFruitBuilder = function(fruit){
  return function() {
    alert("너의 선택은 " + fruit);
  };
}

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

document.body.appendChild($ul);

예제 4는 콜백 함수를 *고차 함수로 바꿔서 클로저를 활용한 방안입니다. 

addEventListener함수 안에서 콜백 함수로 alertFruitBuilder 함수를 실행하면서 fruit 값을 인자로 전달했습니다. 이후 클릭 이벤트가 발생하면 실행 컨텍스트가 활성화되면서 fruit를 outerEnvironmentReference에 의해 참조할 수 있어, alertFruitBuilder  실행 결과로 반환된 함수에는 클로저가 존재합니다.

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

정보은닉은 어떤 모듈의 내부 로직에 대해 외부로의 노출을 최소화해서 모듈 간의 겹합도를 낮추고 유연성을 높이기 위한 개념입니다.

접근 권한에는 public, private, protected, 세 종류가 있습니다. 자바스크립트는 변수 자체에 이러한 접근 권한을 직접 부여하도록 설계되어 있진 않지만 클로저를 이용해 함수 차원에서 public 한 값과 private 한 값을 구분하는 것이 가능합니다.

 

클로저를 활용하면 외부 스코프에서 함수 내부의 변수들 중 일부의 변수에 대한 접근 권한을 부여할 수 있습니다. (예제 2에서처럼 return을 활용)

 

자동차 경주 게임을 만들면서 배워 보고자 합니다. 게임의 규칙은 다음과 같습니다

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

예제 5)

var createCar = function() {
  var fuel = Math.ceil(Math.random() * 10 + 10); // 연료
  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이동 (총 ' + movd + 'km). 남은 연료: ' + fuel);
    }
  };
};

var car = createCar();
console.log(car); // {run: f}

createCar 함수를 실행하여 객체를 생성하고 fuel, power 변수는 비공개 멤버로 지정해 외부에서의 접근 제한, moved 변수는 getter 부여함으로써 읽기 전용 속성을 부여했습니다. 외부에서는 run 메서드를 실행하는 것과 moved 값을 확인만 할 수 있습니다.

 

정리하면 클로저를 활용해 접근권한을 제어하는 방법은 외부에 제공하고자 하는 정보들 즉, return 한 변수들은 공개 멤버가 되고, 그렇지 않은 변수들은 비공개 멤버가 됩니다.

부분 적용 함수

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

커링 함수

커링 함수란 여러 개의 인자를 받는 함수를 하나의 인자만 받는 함수로 나눠서 순차적으로 호출될 수 있게 체인 형태로 구성한 것입니다. 마지막 인자가 전달되기 전까지 원본 함수가 실행되지 않습니다.

const curryFun = func => a => b => c => d => e => func(a,b,c,d,e);

마지막 호출로, 실행 컨텍스트가 종료된 후에야 GC의 수거대상이 됩니다. 마지막 인자가 넘어갈 때까지 함수 실행을 미루는 것이 함수형 프로그래밍에서는 지연 실행이라고 합니다.


정리

클로저란?

  • 어떤 함수에서 선언한 변수를 참조하는 내부함수를 외부로 전달할 경우, 함수의 실행 컨텍스트가 종료된 후에도 해당 변수가 사라지지 않는 현상
  • 그 본질이 메모리를 계속 차지하는 개념이므로 더는 사용하지 않게 된 클로저에 대해서는 메모리를 차지하지 않도록 관리해줄 필요가 있음 (GC 관련)

내부함수를 외부로 전달하는 방법

  • 함수를 return하는 경우
  • 콜백으로 전달하는 경우

 

*고차 함수란 함수를 인자로 받거나 함수를 리턴하는 함수입니다.

Comments