1. 클로저

이미 생명주기가 끝난 외부 함수의 변수를 참조하는 함수를 클로저라 한다.

MDN 클로저는 함수와 그 함수가 선었되었을 때의 Lexical Environment의 조합이다.

  • 실제 개발 도구창에 closure는 외부 함수들의 변수들을 담고 있다.
    • 현재 context의 변수는 local에 위치에 있다.
    • 개발 도구창에서의 클로저는 생명주기가 끝난 외부 함수의 Lexical Environment이다.
  • 클로저로 참조된 외부 변수를 자유 변수(Free Variable)이라 하고 클로저라는 이름은 함수가 자유 변수에 대해 닫혀있다는 의미이다.(자유 변수를 간직한 함수)
function outerFunc() {
  var x = 1;
  return function inner() {
    console.log(x * 2);
  };
}

var inner = outerFunc();
  • 위 예제에서 외부 함수가 호출되고 외부 함수가 반환한 내부 함수(외부 함수의 변수를 참조하는)가 클로저이다.
  • 즉, 클로저는 반환된 내부 함수가 자신이 선언됐을 때의 환경(Lexical environment)인 스코프를 기억하여 자신이 선언됐을 때의 환경(스코프) 밖에서 호출되어도 그 환경(스코프)에 접근할 수 있는 함수이다.

2. 실행 컨텍스트 관점에서의 클로저

  • outerFunction이 실행될 때 outerFunction의 실행 컨텍스트는 아래와 같이 생성된다.
  • 이때 inner는 함수 선언이므로 함수명이 Environment Record의 프로퍼티로 추가되고, 생성된 함수 객체가 값으로 설정된다.
//outer EC
FunctionExectionContext = {
  LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Declarative",
      x : 1,
      inner : <Function Object>
    }
    outer: <Global Environment>,
    this: <Global Object>
  }
}
  • 생성된 함수 객체는 [[scope]] 프로퍼티를 가지고 있다.
  • [[scope]] 프로퍼티는 outer EC의 LexicalEnvironment를 가리킨다.
inner = {
    [[scope]] : <outer EC's LexicalEnvironment>
}
inner();
  • 반환된 내부 함수가 호출되면 내부 함수의 실행 컨텍스트가 실행 컨텍스트 스택에 쌓이고,
  • Lexical Environment의 Environment Record가 초기화되고, Outer Reference와 this가 결정된다.
  • 이때 Outer Reference는 inner 함수의 [[scope]] 프로퍼티가 가리키는 대상을 가리킨다.
  • 즉, outer 함수의 LexicalEnvironment를 가리킨다.
  • 이것이 렉시컬 스코프의 실체이다.
  • 내부 함수에서 x(외부 함수의)에 접근할 수 있는 것은 렉시컬 스코프의 레퍼런스를 차례대로 저장하고 있는 실행 컨텍스트의 스코프 체인을 검색해서이다.(outer Reference)

이처럼 외부 함수보다 내부 함수가 더 오래 유지되는 경우, 외부 함수 바깥에서 내부 함수가 호출되더라도 외부 함수의 지역 변수를 접근할 수 있다. 이러한 함수를 클로저라 한다.

3. 클로저의 활용

1) 상태 유지(전역 변수를 사용하지 않는)

var toggle = (function () {
  var isShow = false;
  return function () {
    // 클로저 반환
    box.style.display = isShow ? "block" : "none";
    isShow = !isShow; // 상태 변경
  };
})();
  • 클로저를 제거하지 않는 한 클로저가 기억하는 렉시컬 환경의 변수 isShow는 유지된다.
  • 즉, 현재 상태를 저장한다.
  • 이처럼 현재 상태를 유지하고, 상태가 변경되었을 때 이 최신 상태를 유지해야 하는 상황에서 클로저는 매우 유용하다.

2) 캡슐화(정보 은닉)

function Counter() {
  var counter = 0;
  this.increase = function () {
    // 클로저
    return ++counter;
  };
  this.decrease = function () {
    // 클로저
    return --counter;
  };
}
var counter = new Counter();
console.log(counter.increase()); // 1
console.log(counter.increase()); // 2
console.log(counter.decrease()); // 1
console.log(counter.decrease()); // 0
  • 생성자 함수 Counter는 increase, decrease 메소드를 갖는 인스턴스를 생성한다.
  • 이 메서드들은 자신이 생성됐을 때의 렉시컬 환경인 Counter 생성자 함수의 스코프에 속한 counter 변수를 기억하는 클로저이고 렉시컬 환경을 공유한다.
  • counter 변수는 this에 바인딩 된 프로퍼티가 아닌 변수다. 즉, 외부에서 접근할 수 없다.
  • 생성자 함수가 생성한 객체는 객체의 프로퍼티뿐 아니라, 자신이 기억하는 렉시컬 환경의 변수도 접근 가능하다.
  • 이러한 클로저의 특성을 이용해서 클래스 기반 언어의 private 키워드를 흉내 낼 수 있다.

4. 클로저 사용 시 주의할 점

  1. 루프 안에서의 클로저 사용
var arr = [];
for (var i = 0; i < 5; i++) {
  arr[i] = function () {
    return i;
  };
}
for (var j = 0; j < arr.length; j++) {
  console.log(arr[j]());
}
// 5
// 5
// 5
// 5
// 5
  • for 문에서 사용하는 i는 전역 변수이기 때문에 호출될 때의 전역 변수 i는 5이므로 5가 5번 찍히게 된다.
  • 클로저를 활용해서 해결할 수 있다.
var arr = [];
for (var i = 0; i < 5; i++) {
  arr[i] = (function (id) {
    //id에 i를 할당하고 내부 함수를 반환한다.
    return function () {
      return id; // id는 상위 스코프의 자유 변수이므로 값이 유지된다.
    };
  })(i);
}
for (var j = 0; j < arr.length; j++) {
  console.log(arr[j]());
}

let을 이용한 해결

let을 이용해서 더 간단하게 해결할 수 있다.

  • let으로 i를 선언하면 i는 for 문의 블록 스코프가 된다.
  • 이때 for 문의 반복 횟수만큼 클로저가 생성되고 for 문이 돌 당시의 클로저의 i에 접근하는 것이다.
var arr = [];
for (let i = 0; i < 5; i++) {
  arr[i] = function () {
    return i;
  };
}
for (var j = 0; j < arr.length; j++) {
  console.log(arr[j]());
}
// 0
// 1
// 2
// 3
// 4
  1. 클로저는 자신이 생성될 때의 환경을 기억해야 하므로 메모리 상의 손해를 볼 수 있다.
  2. 스코프 체인상 뒤쪽에 있는 객체를 참조하므로 성능 저하의 원인이 될 수 있다.
  3. 클로저의 프로퍼티 값이 쓰기 가능하므로 그 값이 여러 번 호출로 항상 변할 수 있다.
  4. 하나의 클로저가 여러 함수의 스코프 체인에 들어가 있는 경우도 있다.

참고

  • Learning Javascript
  • Inside Javascript
  • https://poiemaweb.com/js-closure
  • https://wit.nts-corp.com/2014/06/20/1560