closure

나혜수·2023년 1월 30일
0

자바스크립트 

목록 보기
3/14

클로저

렉시컬 스코핑, 스코프 체인

클로저는 함수를 일급 객체로 취급하는 함수형 프로그래밍 언어에서 사용되는 중요한 특성이다.

function outerFunc() {
 var x = 10;
 var innerFunc = function () { console.log(x); };
 innerFunc();
}

outerFunc(); // 10

함수 outerFunc 내에서 내부 함수 innerFunc가 선언되고 호출되었다. 이때 내부 함수 innerFunc는 자신을 포함하고 있는 외부 함수 outerFunc의 변수 x에 접근할 수 있다. innerFuncouterFunc의 내부에 선언되었기 때문이다.

스코프는 함수를 어디에 선언하였는지에 따라 결정된다. 이를 렉시컬 스코핑 (Lexical scoping)이라 한다. 위 예제의 innerFuncouterFunc의 내부에 선언되었기 때문에 innerFunc의 상위 스코프는 함수 outerFunc이다.

innerFunc는 자신이 속한 렉시컬 스코프 (전역, outerFunc, 자신의 스코프)를 참조할 수 있다.
자바스크립트 엔진은 변수를 찾을 때 우선 자신이 속한 스코프에서 찾고 그 스코프에 식별자가 없으면 상위 스코프에서 다시 찾아 나가는데 이를 스코프 체인이라 한다.

  1. innerFunc 스코프 (자신의 스코프) 내에서 변수 x를 검색한다. 검색이 실패하였다.
  2. innerFunc 함수를 포함하는 outerFunc 스코프에서 변수 x를 검색한다. 검색이 성공하였다.

클로저 정의

이번에는 innerFuncouterFunc 내에서 호출하는 것이 아니라 반환하도록 변경해 보자.

function outerFunc() {
  var x = 10;
  var innerFunc = function () { console.log(x); };
  return innerFunc;
}

/* 함수 outerFunc를 호출하면 내부 함수 innerFunc가 반환된다.
   그리고 함수 outerFunc의 실행 컨텍스트는 소멸한다. */

var inner = outerFunc();
inner(); // 10

outerFuncinnerFunc를 반환하고 생을 마감했다. 즉, outerFunc는 실행된 이후 콜스택 (실행 컨텍스트 스택)에서 제거되었으므로 outerFunc의 변수 x 또한 더이상 유효하지 않게 된다.
그러나 위 코드의 실행 결과는 변수 x 값인 10이다. 이미 life-cycle이 종료되어 콜스택에서 제거된 outerFunc의 지역 변수 x가 다시 부활이라도 한 듯 동작하고 있다.

이처럼 자신을 포함하고 있는 외부 함수보다 내부 함수가 더 오래 유지되는 경우, 외부 함수 밖에서 내부 함수가 호출되더라도 외부 함수의 지역 변수에 접근할 수 있는데 이러한 함수를 클로저라고 부른다. 즉, 클로저는 반환된 내부 함수가 자신이 선언됐을 때의 환경(Lexical environment)을 기억하여 환경 밖에서 호출되어도 그 환경에 접근할 수 있는 함수를 말한다.


클로저의 활용

클로저는 자신이 생성될 때의 환경 (Lexical environment)을 기억해야 하므로 메모리 차원에서 손해를 볼 수 있다. 하지만 클로저는 자바스크립트의 강력한 기능으로 이를 적극적으로 사용해야 한다.

1. 상태 유지

클로저가 가장 유용하게 사용되는 상황은 현재 상태를 기억하고 변경된 최신 상태를 유지하는 것이다. 아래 예제를 살펴보자.

<!DOCTYPE html>
<html>
<body>
  <button class="toggle">toggle</button>
  <div class="box" style="width: 100px; height: 100px; background: red;"></div>

  <script>
    var box = document.querySelector('.box');
    var toggleBtn = document.querySelector('.toggle');

    var toggle = (function () {
      var isShow = false;
      // ① 클로저를 반환
      return function () {
        box.style.display = isShow ? 'block' : 'none';
        // ③ 상태 변경
        isShow = !isShow;
      };
    })();

    // ② 이벤트 프로퍼티에 클로저를 할당
    toggleBtn.onclick = toggle;
  </script>
</body>
</html>

① 즉시 실행 함수는 함수를 반환하고 즉시 소멸한다. 반환된 함수는 자신이 생성됐을 때의 렉시컬 환경에 속한 변수 isShow를 기억하는 클로저다. 클로저가 기억하는 변수 isShow는 box 요소의 표시 상태를 나타낸다.

② 클로저를 이벤트 핸들러로서 이벤트 프로퍼티에 할당했다. 클로저를 제거하지 않는 한 클로저가 기억하는 렉시컬 환경의 변수 isShow는 소멸하지 않는다. 다시 말해 현재 상태를 기억한다.

③ 버튼을 클릭하면 이벤트 프로퍼티에 할당한 이벤트 핸들러인 클로저가 호출된다. 이때 .box 요소의 표시 상태를 나타내는 변수 isShow의 값이 변경된다. 변수 isShow는 클로저에 의해 참조되고 있기 때문에 유효하며 자신의 변경된 최신 상태를 게속해서 유지한다.


2. 전역 변수의 사용 억제

<!DOCTYPE html>
<html>
  <body>
  <p>클로저를 사용한 Counting</p>
  <button id="inclease">+</button>
  <p id="count">0</p>
  <script>
    var incleaseBtn = document.getElementById('inclease');
    var count = document.getElementById('count');

    var increase = (function () {
      // 카운트 상태를 유지하기 위한 자유 변수
      var counter = 0;
      // 클로저를 반환
      return function () {
        return ++counter;
      };
    }());

    incleaseBtn.onclick = function () {
      count.innerHTML = increase();
    };
  </script>
</body>
</html>

스크립트가 실행되면 즉시 실행 함수가 호출되고 변수 increase에는 함수 function ( ) { return ++counter; }가 할당된다. 이 함수는 자신이 생성됐을 때의 렉시컬 환경을 기억하는 클로저다. 즉시 실행 함수는 호출된 이후 소멸되지만, 즉시 실행 함수가 반환한 함수는 변수 increase에 할당되어 increase 버튼을 클릭하면 클릭 이벤트 핸들러 내부에서 호출된다. 이때 클로저인 이 함수는 지역 변수 counter를 기억한다. 따라서 변수 counter에 접근할 수 있고 변수 counter는 자신을 참조하는 함수가 소멸될 때까지 유지된다.

즉시 실행 함수는 한번만 실행되므로 increase가 호출될 때마다 변수 counter가 재차 초기화될 일은 없을 것이다. 변수 counter는 외부에서 직접 접근할 수 없는 private 변수이므로 전역 변수를 사용했을 때와 같이 의도되지 않은 변경을 걱정할 필요가 없다. 따라서 안정적인 프로그래밍이 가능하다.

변수 값은 언제든지 변경될 수 있어 오류 발생의 근본적 원인이 된다. 불변성을 지향하는 함수형 프로그래밍에서 Side effect를 최대한 억제하여 오류를 피하고 프로그램의 안정성을 높이기 위해 클로저는 적극적으로 사용된다.

아래는 함수형 프로그래밍에서 클로저를 활용하는 간단한 예제이다.

// 함수를 인자로 전달받고 함수를 반환하는 고차 함수
// 이 함수가 반환하는 함수는 클로저로서 카운트 상태를 유지하기 위한 자유 변수 counter을 기억한다.
function makeCounter(predicate) {
  // 카운트 상태를 유지하기 위한 자유 변수
  var counter = 0;
  // 클로저를 반환
  return function () {
    counter = predicate(counter);
    return counter;
  };
}

// 보조 함수
function increase(n) {
  return ++n;
}

// 보조 함수
function decrease(n) {
  return --n;
}

// 함수로 함수를 생성한다.
// makeCounter 함수는 보조 함수를 인자로 전달받아 함수를 반환한다
const increaser = makeCounter(increase);
console.log(increaser()); // 1
console.log(increaser()); // 2

// increaser 함수와는 별개의 독립된 렉시컬 환경을 갖기 때문에 카운터 상태가 연동하지 않는다.
const decreaser = makeCounter(decrease);
console.log(decreaser()); // -1
console.log(decreaser()); // -2

3. 정보의 은닉

function Counter() {
  // 카운트를 유지하기 위한 자유 변수
  var counter = 0;

  // 클로저
  this.increase = function () {
    return ++counter;
  };

  // 클로저
  this.decrease = function () {
    return --counter;
  };
}

const counter = new Counter();

console.log(counter.increase()); // 1
console.log(counter.decrease()); // 0

생성자 함수 Counter는 increase, decrease 메소드를 갖는 인스턴스를 생성한다. 이 메소드들은 모두 자신이 생성됐을 때의 렉시컬 환경인 생성자 함수 Counter 스코프에 속한 변수 counter를 기억하는 클로저이며 렉시컬 환경을 공유한다. 생성자 함수가 생성한 객체의 메소드는 객체의 프로퍼티에만 접근할 수 있는 것이 아니며 자신이 기억하는 렉시컬 환경의 변수에도 접근할 수 있다.

Counter의 변수 counter는 this에 바인딩된 프로퍼티가 아니라 변수다. counter가 this에 바인딩된 프로퍼티라면 Counter가 생성한 인스턴스를 통해 외부에서 접근이 가능한 public 프로퍼티가 되지만, Counter 내에서 선언된 변수 counter는 Counter 외부에서 접근할 수 없다.
하지만 Counter가 생성한 인스턴스의 메소드인 increase, decrease는 클로저이기 때문에 변수 counter에 접근할 수 있다. 이러한 클로저의 특징을 사용해 클래스 기반 언어의 private 키워드를 흉내낼 수 있다.

profile
오늘도 신나개 🐶

0개의 댓글