[코어 자바스크립트] 클로저

변진상·2024년 5월 30일
0

학습 기록

목록 보기
25/31

클로저의 의미 및 원리 이해

클로저란?

  • 함수형 프로그래밍 언어에서 등장하는 보편적 특징.
  • 그래서 ECMAScript의 명세에서도 클로저의 정의를 다루지 않음 → 클로저에 대한 정확한 정의가 없어 여러 방법으로 설명된다.

자신을 내포하는 함수의 컨텍스트에 접근할 수 있는 함수
- 더글라스 크록포드, 《자바스크립트 핵심 가이드), 한빛미디어(p68)

함수가 특정 스코프에 접근할 수 있도록 의도적으로 그 스코프에서 정의하는 것
- 에단 브라운, 《러닝 자바스크립트》, 한빛미디어 (p196)

함수를 선언할 때 만들어지는 유효범위가 사라진 후에도 호출할 수 있는 함수
- 존 레식, 《자바스크립 트 닌자 비급》, 인사이트(p116)

이미 생명 주기상 끝난 외부 함수의 변수를 참조하는 함수
- 송형주 고현준, 《인사이드 자바스크립 트), 한빛미디어(p157)

자유변수가 있는 함수와 자유변수를 알 수 있는 환경의 결합
- 에릭 프리먼, <Head First Javascript Programming》, 한빛미디어(p534)

→ 정리: “어떤 함수에서 선언한 변수를 참조하는 내부함수에서 발생하는 현상”

let outer = function () {
  let a = 1;
  let inner = function () {
    console.log(++a);
  }
  inner();
}

outer();
  • inner 함수가 동작하면 함수 내부에 a 가 없기 때문에 outerEnvironmentReference를 타고 나가면서 식별자 a 변수를 찾는다. 찾으면 가산하고 출력 후 실행컨텍스트가 종료되면 lexicalEnvironment에 저장된 a, inner에 대한 참조를 지운다. → 이후 가비지 컬렉션의 대상이 되어 메모리에서 제거된다.
let outer = function () {
  let a = 1;
  let inner = function () {
    console.log(++a);
  }
  return inner;
}

const inner = outer();

inner(); // 2
inner(); // 3
  • 클로저의 개념 정리: 내부함수에서 외부함수에서 선언한 변수를 참조하고 있어 가비지 컬렉션의 대상에서 제외되어, 외부 함수의 실행 컨텍스트가 종료된 이후에도 외부 함수의 변수가 사라지지 않는 현상이다.

2. 클로저와 메모리 관리

  • 클로저 현상이 발생했다는 것은 가비지 컬렉팅이 되어야할 변수가 참조 카운트가 0이 되지 않아 수거 대상이 되지 않는다는 것이다. → 메모리 누수라고 할 수 있다.
  • 해결 방법: 식별자에 참조형이 아닌 기본형 데이터를 할당하면 된다.
(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. 클로저 활용 사례

5-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
    console.log('your choice is ' + fruit);
  })

  $ul.appendChild($li);
})

document.body.appendChild($ul)
  • 콜백함수 B가 외부함수 A의 fruit 변수를 참조한다. → 클로저 현상 발생
  • 클릭 이벤트가 발생할 때 B의 outerEnvironmentReference가 A의 LexicalEnvironment를 참조한다. → B 함수가 참조할 예정인 fruit은 GC 대상에서 제외되어 계속 사용 가능
  • 참조: 핸들러 함수를 외부 기명함수로 빼 재사용시 클로저 이용하는 방법
    • addEventListener의 경우 함수에 첫 인자로 이벤트 객체([object MouseEvent])를 주입한다.
    • 이를 해결하기 위해 함수를 리턴하는 함수를 콜백으로 넘겨준다.
    var fruits = ['apple', 'banana', 'peach'];
    var $ul = document.createElement('ul');
    
    var consoleFruit = function (fruit) {
    
    	return function () { // [object MouseEvent] 첫번째 인자로 주입.
    		console.log('your choice is ' + fruit);
    	}
    }
    
    fruits.forEach(function (fruit) { 
      var $li = document.createElement('li');
      $li.innerText = fruit;
      $li.addEventListener('click', consoleFruit(fruit))
    
      $ul.appendChild($li);
    })
    
    document.body.appendChild($ul)

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

ES5 기준으로는 아직 private class field(#)가 도입되기 전이다. ES2019에 도입

  • 접근 권한에는 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라는 전역 스코프로부터 격리된 공간안에 위치한 a 변수가 외부에 제공된다. → return을 통해 제공되는데 이렇게 외부에 제공된 변수들은 public 멤버가 된다. 나머지는 private.

5-3-3 부분 적용 함수 구현

  • 부분 적용 함수: n개의 인자를 받는 함수에 미리 m개의 인자만 넘겨 기억시켰다가, 나중에 (n-m)개 인자를 넘기면 함수의 실행 결과를 얻을 수 있게하는 함수이다.
    • bind 함수가 this 바인딩을 제외하면 실행 결과가 부분 적용 함수
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 partial = function () {
  var originalPartialArgs = arguments;
  var func = originalPartialArgs[0];

  if (typeof func !== 'function') {
    throw new Error('첫 번째 인자가 함수가 아닙니다.');
  }

  return function () {
    var partialArgs = Array.prototype.slice.call(originalPartialArgs, 1);
    var restArgs = Array.prototype.slice.call(arguments);
    return func.apply(this, partialArgs.concat(restArgs));
  }
}

var add = function () {
  var result = 0;
  for (var i = 0; i < arguments.length; i++) {
    result += arguments[i];
  }
  return result;
};

var addPartial = partial(add, 1, 2, 3, 4, 5);
console.log(addPartial(6, 7, 8, 9, 10)); // 55

5-3-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
console.log(getMaxWith10(13)); // 13
  • 각 단계에서 인자를 마지막 단계에서 참조할 것이므로 GC 되지 않고 메모리에 저장되어있다가 마지막 호출로 실행 컨텍스트가 종료되고 나서야 수거된다.
  • 마지막 인자가 넘어갈 때까지 함수 실행을 미루게 되는데 이를 함수형 프로그래밍에서 지연실행이라고 칭한다.

4. 정리

  • 클로저란 어떤 함수에서 선언한 변수를 참조하는 내부함수를 외부로 전달할 경우, 함수의 실행 컨텍스트가 종료된 후에도 해당 변수가 사라지지 않는 현상이다.
  • 외부로 내부함수를 전달하는 방법은 return이나 콜백함수도 해당한다.
  • 메모리 누수의 가능성이 있어 관리가 필요하다.
profile
자신을 개발하는 개발자!

0개의 댓글