자바스크립트의 클로저

이희제·2024년 4월 23일
post-thumbnail

코어 자바스크립트 5장을 통해 클로저에 대해 알아보자

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

MDN 기준으로 클로저의 의미를 살펴보면 "클로저는 함수와 그 함수가 선언될 당시의 lexical environment의 상호관계에 따른 현상" 으로 해석할 수 있다.

상호관계는 내부함수가 외부 변수를 참조하는 경우에만 의미가 있다.

여기서 lexical environment는 실행 컨텍스트 구성 요소 중 하나인 outerEnvironmentReference에 해당된다.

참고
OuterEnvironmentRecord Reference에 할당되는 값은 함수 객체의 내부슬롯 [[Environment]]에 저장된 렉시컬 환경에 대한 참조

예시를 통해 클로저의 의미에 대해 살펴보자.

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 함수를 호출하면 inner 함수를 return 하고 실행 컨텍스트가 종료된다.

inner 함수의 실행 컨텍스트의 environmentRecord에는 수집할 정보가 없고, outerEnvironmentReference에는 outer 함수의 LexicalEnvironment가 담긴다.

이상한 점은 이미 outer 실행이 종료된 상태인데 inner 함수는 outer 함수 내 변수 a에 접근할 수 있다.

이는 가비지 컬렉터의 동작 방식 때문이다. 가비지 컬렉터는 어떤 값을 참조하는 변수가 하나라도 있으면 수집 대상에 포함 시키지 않는다.

따라서 inner 함수에서 변수 a에 접근이 가능한 것이다.

2019년 기준으로 크롬이나 Node.js 등에서 사용 중인 V8 엔진의 경우 내부 함수에서 실제로 사용하는 변수만 남겨두고 나머지는 GC한다.

클로저의 정의에 가장 근접한 표현 3가지

  1. 함수를 선언할 때 만들어지는 유효범위가 사라진 후에도 호출할 수 있는 함수
  2. 이미 생명 주기가 끝난 외부 함수의 변수를 참조하는 함수
  3. 자신이 생성된 때의 스코프에서 알 수 있었던 변수들 중 언젠가 자신이 실행될 때 사용할 변수들만을 기억하여 유지시키는 함수

참고
호출 장소와 상관없이 함수가 자신이 태어난 곳을 기억할 수 있는 건 바로 이 [[Environment]] 프로퍼티 덕분이다.

외부 전달이 곧 return만을 의미하진 않는다. 예시 코드를 통해 살펴 보자.

// (1) setInterval/setTimeout
(function() {
  var a = 0;
  var intervalId = null;
  var inner = function() {
    if (++a >= 10) { // 내부 변수 참조
      clearInterval(intervalId); // 내부 변수 참조
    }
    console.log(a);
  };
  intervalId = setInterval(inner, 1000);
})();

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

2. 클로저와 메모리 관리

클로저를 사용할 때 메모리 소모 특정에 대해 이해하고 사용해야 한다.

클로저는 필요에 의해 함수의 지역변수를 메모리를 소모하도록 하는데, 필요성이 사라지면 메모리를 해제시키면 된다. (즉, 참조 카운트를 0으로 만들면 된다.)

참조 카운트를 0으로 만들기 위해서는 기본형 테이터(보통 null, undefined)를 할당하면 된다.

// (1) return에 의한 클로저의 메모리 해제
var outer = (function() {
  var a = 1;
  var inner = function() {
    return ++a;
  };
  return inner;
})();
console.log(outer());
console.log(outer());
outer = null; // outer 식별자의 inner 함수 참조를 끊음

// (2) 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) 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. 콜백 함수 내부에서 외부 데이터를 사용하고자 할 때

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

[고차함수 활용 예시]
고차함수란? 함수를 인자로 받거나 함수를 리턴하는 함수

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

var alertFruitBuilder = function(fruit) {
  return function() {
    alert('your choice is ' + 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);

반환된 함수의 실행 컨텍스트가 열리면서 fruit를 참조할 수 있다. 반환된 함수에 클로저가 존재하게 된다.

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

자바스크립트는 기본적으로 변수 자체에 접근 권한(public, private, protected)을 직접 부여하도록 설계되어 있지 않다.

클로저를 활용하면 public한 값과 private한 값을 구분하는 것이 가능하다.

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란 변수를 통해 함수를 실행할 수는 있지만 함수 내부에는 개입할 수 없다.

외부에 제공하고자 하는 정보들은 return하고 내부 사용 정보는 return하지 않으면 접근 권한 제어가 가능하다.

3-3. 부분 적용 함수

부분 적용 함수란 n개의 인자를 받는 함수에 미리 m개의 인자만 넘겨 기억했다가, 나중에 n-m개의 인자를 넘기면 함수가 실행되어 결과를 얻을 수 있는 함수이다.

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

부분 적용 함수를 활용한 디바운스 구현

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

쓰로틀링 (Throttling)

쓰로틀링은 정해진 시간 간격(예: 100ms)마다 최대 한 번씩만 함수가 실행되도록 제한한다. 이 기간 동안 추가적으로 발생하는 이벤트 호출은 무시된다. 즉, 이 기간 내에서는 첫 번째 또는 마지막 호출만 실행되고 나머지는 무시된다.

  • 적용 예: 스크롤 이벤트나 마우스 무브 이벤트 처리
  • 주요 목적: 일정한 간격으로 반복 실행을 보장하여 빠른 반복 이벤트에서도 일정한 반응을 유지

디바운싱 (Debouncing)

디바운싱은 연속된 이벤트 호출에서 마지막 이벤트가 발생한 후 정해진 딜레이(예: 300ms)가 지나야 함수가 실행되도록 한다. 이 기간 동안 새로운 이벤트가 발생하면 타이머가 리셋된다. 따라서 함수는 연속된 이벤트 호출이 멈춘 후에만 실행된다.

  • 적용 예: 입력 필드에서 사용자의 타이핑 끝을 감지하여 검색어 자동 완성
  • 주요 목적: 불필요한 함수 실행을 최소화하여 성능을 최적화

3-4. 커링 함수(currying function)

커링 함수란 여러 개의 인자를 받는 함수를 하나의 인자만 받는 함수로 나눠서 순차적으로 호출될 수 있게 체인 형태로 구성한 것을 말한다.

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

const getMax = curry5(Math.max);
console.log(getMax(1)(2)(3)(4)(5));

커링 함수는 인자가 넘어갈 때까지 함수 실행을 미루는데 이를 함수형 프로그래밍에서는 지연실행
(lazy execution) 이라고 한다.


같이 보면 좋은 자료

https://ko.javascript.info/closure

profile
그냥 하자

0개의 댓글