IT_Programming/JavaScript

[펌] Closure(함수 클로저)

JJun ™ 2017. 11. 13. 10:01



 * 출처

 : https://heropy.blog/2017/11/10/closure/




클로저란?

클로저(Closure)는 일급 객체 함수(first-class functions)의 개념을 이용하여 유효범위(scope)에 묶인 변수를 바인딩 하기 위한 일종의 기술이다. 기능상으로, 클로저는 함수를 저장한 레코드(record)이며, 스코프(scope)의 인수(Factor)들은 클로저가 만들어질 때 정의(define)되며, 스코프 내의 영역이 소멸(remove)되었어도 그에 대한 접근(access)은 독립된 복사본인 클로저를 통해 이루어질 수 있다.

자바스크립트 클로저(Closure)는 독립적인 변수(로컬로 사용되지만 둘러싼 범위에서 정의 된 변수)를 참조하는 함수입니다.
이 함수들은 생성될 당시의 환경을 기억 합니다.

이 방식으로 종료된 함수 내 특정 지역변수를 사용할 수 있습니다.
우선 간단한 예제를 살펴봅시다.

function plus() {
  var a = 0;
  return function () {
    return ++a;
  }
}
var p = plus();
console.log(p());  // 1
console.log(p());  // 2
console.log(p());  // 3

함수 plus는 변수 p에 할당되며 실행이 종료되었지만, p를 실행하면 plus함수 내에 선언된 변수 a를 사용할 수 있습니다.

변수 a가 사용되는 동안에는 GC이 되지 않습니다.
따라서 상황에 맞게 변수 a 참조를 없애주는 것이 좋습니다.

// 참조 제거
p = null;

반복문에서 클로저

배열 데이터에 값을 할당할 때 사용된 변수 i는 호출 시점에서 접근하기 때문에, 출력 시 반복문이 종료된 상태의 i의 값인 ‘5’만을 참조합니다.

var arr = [];
for (var i = 0; i < 5; i++) {
  arr[i] = function () {
    return i;
  }  
}
arr.forEach(function (el) {
  console.log(el());  // 5 / 5 / 5 / 5 / 5
});

즉시실행함수(IIFE)로 값을 할당하여, 호출과 참조 시점을 같게 해줍니다.

var arr = [];
for (var i = 0; i < 5; i++) {
  arr[i] = (function () {
    return i;
  }());
}
arr.forEach(function (el) {
  console.log(el);  // 0 / 1 / 2 / 3 / 4
});

좀 더 실용적인 예제를 살펴봅시다.
목록을 클릭하여 목록에 해당하는 숫자가 콘솔에 출력되도록 하는 예제를 만들겠습니다.

<ul class="click">
  <li>CLICK 1</li>
  <li>CLICK 2</li>
  <li>CLICK 3</li>
  <li>CLICK 4</li>
</ul>
var items = document.querySelectorAll('.click li');
for (var i = 0; i < items.length; i++) {
  items[i].onclick = function () {
    console.log('click' + i);  // 'click4'
  }
}

콘솔에는 'click4'만 출력됩니다.
onclick에 할당되는 핸들러(함수)가 실행되는 시점과, 변수 i를 참조하는 시점이 다르기 때문에 정상적으로 숫자가 출력되지 않습니다.
그렇다면 잘 동작할 수 있도록 아래와 같이 변경합니다.

var items = document.querySelectorAll('.click li');
for (var i = 0; i < items.length; i++) {
  items[i].onclick = (function (j) {
    return function () {
      console.log('click' + j);  // 'click0' / 'click1' / 'click2' / 'click3'
    }
  }(i))
}

핸들러를 반환하는 즉시실행함수(외부함수)를 생성하고, 지역변수(매개변수)를 만들어 핸들러가 값을 참조 가능하도록 합니다.
이제 핸들러는 반복문의 i를 참조하지 않고 외부함수의 매개변수인 j를 참조합니다.
콘솔을 확인하면 순서대로 숫자가 잘 출력됩니다.

ES2015(ES6)의 let 키워드를 사용하면 좀 더 쉽게 구현할 수 있습니다.

var items = document.querySelectorAll('.click li');
for (let i = 0; i < items.length; i++) {
  items[i].onclick = function () {
    console.log('click' + i);  // 'click0' / 'click1' / 'click2' / 'click3'
  }
}

클로저의 캡슐화, 은닉화

자바스크립트에서 일반적인 객체지향 프로그래밍 방식으로 prototype를 사용하는데 객체 안에서 사용할 속성을 생성할 때 this 키워드를 사용하게 됩니다.
아래 예제에서 속성 this._name은 _를 앞에 붙여줌으로 네이밍 컨벤션을 기준으로 외부 접근 불가(Private)의 의미를 가집니다.
그러나 자바스크립트에서는 privatepublicprotected 같은 ‘접근 수정자’를 제공하지 않기 때문에, 예제에서의 속성 _name은 ‘접근 불가’라는 의미만 가질 뿐 실제론 외부에서 접근이 가능합니다.
따라서 a._name으로 콘솔에 출력하면 값을 확인할 수 있습니다.

function Hello(name) {
  this._name = name;
}
Hello.prototype = {
  getName: function () {
    return this._name;
  },
  setName: function (name) {
    this._name = name;
  }
}
var a = new Hello('good');
console.log(a._name);  // 'good'
console.log(a.getName());  // 'good'
a.setName('nice');
console.log(a._name);  // 'nice'
console.log(a.getName());  // 'nice'

이제 외부에서 접근할 수 없도록 ‘캡슐화(Encapsulation)’ 해봅시다.
즉시실행함수를 활용합니다.
즉시실행함수 내부에 별도의 객체를 만들고 객체 자체를 반환하는 형태로 Hello 생성자를 만들어 줍니다.
이제 ‘은닉화(Hiding)’된 _name과 name은 외부에서 접근할 수 없습니다.

var Hello = (function () {  
  var _name;
  function _Hello(name) {
    _name = name;
  }
  _Hello.prototype = {
    getName: function () {
      return _name;
    },
    setName: function (name) {
      _name = name;
    }
  }
  return _Hello;
}());
var a = new Hello('good');
console.log(a._name);  // undefined
console.log(a.getName());  // 'good'
a.setName('nice');
console.log(a._name);  // undefined
console.log(a.getName());  // 'nice'

같은 예제라고 할 수는 없지만 좀 더 쉬운 이해를 위해서 위의 코드를 아래와 같이 단순화할 수 있습니다.

function hello(name) {
  var _name = name;
  return {
    getName: function () {
      return _name;
    },
    setName: function (name) {
      _name = name;
    }
  };
}

클로저와 this

아래 예제의 경우 this.name 키워드가 외부 함수의 this가 아닌 글로벌 this를 참조하고 있습니다.
비상식적으로 동작하지만 실제로 많은 부분에서 발생하는 문제입니다.

window.name = 'window';
var object = {
  name: 'object',
  getName: function () {
    return (function () {
      return this.name;
    }());
  }
}
console.log(object.getName());  // 'window'

이럴 경우 this가 참조 가능한 별도의 변수 _this를 생성할 수 있습니다.

window.name = 'window';
var object = {
  name: 'object',
  getName: function () {
    var _this = this;
    return (function () {
      return _this.name;
    }());
  }
}
console.log(object.getName());  // 'object'

참고 문서

클로저 - MDN

자바스크립트의 클로저 (JavaScript’s Closure)

클로저 - 생활코딩

JavaScript 클로저(Closure)