클로저란

  1. MDN에서 정의한 클로저의 정의는 다음과 같다.


“A closure is the combination of a function and the lexical environment within which that function was declared.” 클로저는 함수와 그 함수가 선언됐을 때의 렉시컬 환경(Lexical environment)과의 조합이다.

굉장히 딱딱한 용어들로 함축적인 의미를 담아 정의되어 있다. 여기서 포인트는 ‘렉시컬 환경’ 의 의미를 파악하는것으로 보여진다.

다음 예제를 통해 어떤 특징이 있는지 확인해 보자.

예제)

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

outerFunc(); // 10

위의 함수 구조에서는 중첩 함수구문으로 되어 있다. 외부의 outerFunc이 존재하고 그 내부에 innerFunc이 선언되어 있다. 이때 특징을 보면 내부 함수에서 외부함수의 변수에 접근할 수 있다는 점이 있다.


렉시컬 스코핑(Lexical scoping)이란 스코프를 정의할 때 함수를 호출하는 시점에 따른게 아니라 함수를 어디에서 선언하였는지에 따라 정해진다. 위 예제의 함수 innerFunc는 함수 outerFunc의 내부에서 선언되었기 때문에 함수 innerFunc의 상위 스코프는 함수 outerFunc이다. 함수 innerFunc가 전역에 선언되었다면 함수 innerFunc의 상위 스코프는 전역 스코프가 된다.

따라서 위의 innerFunc의 렉시컬 스코프는 전역, outerFunc, 자신 이렇게 3가지 영역의 접근이 가능하게 되는 것이다.

위의 예제를 조금 변형한 아래의 예제를 살펴봄으로써 클로저에 대해 알아보도록 하자


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

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


위의 예제에서는 outerFunc내에서 innerFunc을 호출함으로써 콜스택에 유지된 채로 innerFunc을 호출하는 것이 아니라 outerFunc()이 호출됨과 동시에 내부의 innerFunc을 반환하고 자신의 기능은 소멸되게 되는 구조로 바뀌었다. 따라서 변수 x의 값도 메모리에서 제거되게 된다.
하지만 예상과 달리 inner() 함수를 호출하였을때 innerFunc()의 내부에서 호출하는 x 의 존재가 여전히 살아 있어 10 이라는 값을 출력하게 된다.
어떻게 소멸된 x 의 값이 여전히 살아 있어 출력을 할 수 있는 것일까?

이것이 바로 클로저의 특징이다.

이처럼 자신을 포함하고 있는 외부함수보다 내부함수가 더 오래 유지되는 경우, 외부 함수 밖에서 내부함수가 호출되더라도 외부함수의 지역 변수에 접근할 수 있는데 이러한 함수를 클로저(Closure)라고 부른다.

이제 다시 MDN의 클로저 정의를 생각해보면 이해가 될 것이다.

함수그 함수 가 선언 되었을 때 렉시컬 환경 -> 여기서 함수는 ‘innerFunc’이고 렉시컬 환경은 innerFunc이 호출되었을때의 스코프, 즉 전역,outerFunc,자기자신의 영역을 나타낸다. 따라서, 클로저는 반환된 내부함수가 자신이 선언되었을 때의 환경인 스코프를 기억하여, 자신이 나중에 환경 밖에서(스코프 밖)에서 호출된다 할지라도 그 환경(스코프)에 접근할 수 있는 함수를 의미하는 것이다.


한 문장으로 정의하면,

클로저는 자신이 생성될 때의 환경을 기억한는 함수이다

그렇다면 x는 왜 소멸되지 않고 남아있는것인가? 어떠한 형태로 남아 있는 것인가?


클로저에서 참조하고 있는 outerFunc의 변수 x자유변수 라고 부른다. 클로저라는 용어의 의미가 여기서 자유변수에 함수가 닫혀있다(엮여있다) 라는 의미로서 클로저로 불리운다.

내부함수가 유효한 상태에서 외부함수의 활성객체(변수,함수선언) 를 참조하고 있다면, 외부함수의 실행 컨텍스트가 반환된다 하더라도 내부함수가 스코프 체인을 통해 참조할 수 있게된다. 즉, 외부함수 내의 변수는 내부함수가 참조하는게 있다면 계속 유지되고 이는 얕은 복사가 아닌 깊은 복사로 실제 변수에 접근할 수 있게 된다.

조금더 응용적인 예제 코드를 확인해보도록 하자

function makeAdder(x) {
  var y = 1;
  return function(z) {
    y = 100;
    return x + y + z;
  };
}

var add5 = makeAdder(5);
var add10 = makeAdder(10);
//클로저에 x와 y의 환경이 저장됨

console.log(add5(2));  // 107 (x:5 + y:100 + z:2)
console.log(add10(2)); // 112 (x:10 + y:100 + z:2)
//함수 실행 시 클로저에 저장된 x, y값에 접근하여 값을 계산

위의 코드를 분석해보면 makeAdder() 함수는 안에 변수 하나가 존재하고 또다른 내부함수를 반환하는 함수이다. 따라서 함수를 생성하는 함수로서 역할을 하는 존재이다. 이때 x라는 인자를 받아 클로저함수의 렉시컬 환경을 가변적으로 바꿔서 생성하는 부분이 존재한다. 결과에서 보여지듯이 add5 라는 클로저함수를 만들때 makeAdder(5)에서 보이듯이 5 라는 x값을 전달하여 add5 클로저함수가 생성될 때 x=5 라는 환경정보를 저장하게 하여 클로저함수를 생성하였다. 따라서 add5 함수가 생성될 때는 ‘x=5’ , ‘y=100’ 이라는 환경적 상황을 가진채 생성됨으로서 클로저의 역할을 수행하게 된다.


이처럼 클로저는 어떤 데이터와 그 데이터를 조작하는 함수를 연관시켜주기 때문에 유용하다. 이것은 객체가 어떤 데이터와 하나 이상의 메소드들을 연관시킨다는 점에서 객체지향 프로그래밍과 같은 맥락이라고 볼 수 있다.


이러한 클로저는 어디에서 많이 사용될 수 있을까 ?

하나의 메소드를 가지고 있는 객체를 사용하는 곳에서 클로저를 주로 사용한다 따라서 일반적으로 웹, 프론트 영역에서 주로 사용될 수 있다. 특히 이벤트 코드는 콜백으로 처리 되기 때문에 이벤트에 응답하여 실행되는 단일 함수로 클로저가 자주 사용 된다.

아래 예제를 통해 확인해보면

function makeSizer(size) {
  return function() {
    document.body.style.fontSize = size + 'px';
  };
}

var size12 = makeSizer(12);
var size14 = makeSizer(14);
var size16 = makeSizer(16);

document.getElementById('size-12').onclick = size12;
document.getElementById('size-14').onclick = size14;
document.getElementById('size-16').onclick = size16;

특정 id의 글자크기를 유동적으로 변경하려 하는 이벤트를 생성할 때, 각각의 size 데이터를 보유하고 있는 size12,size14,size16 클로저를 생성하여 각각의 이벤트 핸들러로 바인딩시켜 사용될 수 있다.



클로저의 기능성

다음 예제들을 통해 클로저의 활용성을 알아보도록 하자.

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 변수를 toggle 내부함수에서 계속 유지하고 있으며 상태를 변경해 나갈 수 있다는 부분이다. 이때 isShow의 위치가 중요하다. 내부함수내에 위치시키게 되면 isShow는 의미가 없어진다. 왜냐하면 호출될 때마다 초기화가 되기 때문에 상태값이 늘 똑같을 수 밖에 없기 때문이다. 그렇다면 이 변수는 호출문 밖에서 생성되어야 한다.
일반적인 클래스 구문에서는 클래스 멤버 변수에 선언하고 함수에서 이 멤버변수를 참조하는 식의 방식이 있고, 이 객체 내에서만 사용되는 변수가 아니라면 전역에 선언하여 모든 서로 다른 클래스의 객체들이 참조하여 상태를 참조 할 수 있는 방식이 있다.
여기서는 전자의 가까운 상황으로 활용된다. 자바스크립트에서 클로저라는 기능이 없다면 상태를 유지하기 위해 전역변수를 사용할 수 밖에 없는데, 전역변수는 언제든지 누구나 접근,수정이 가능하기 때문에 큰 리스크와 부작용을 유발할 수 있어 사용에 자제를 해야하는 부분이다. 따라서 이러한 상황에서 클로저함수를 사용하여 상태를 유지하고, 접근을 제한 하는 특성을 부여한다.



2. 정보 은닉

위의 예제를 통해 잠깐 언급했지만, 접근을 제한한다는 특징이 있다. 따라서 클래스의 private 와 같은 접근제한자의 특징을 부여할 수 있다.

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는 2개의 함수 (increase, decrease)를 객체의 프로퍼티의 바인딩 하였고, 변수 counter는 바인딩하지 않았다. 변수 counter가 바인딩 되었다면 counter.counter을 통해 접근이 가능한 public 변수였겠지만, 그렇지 않기 때문에 접근할 수 없는 private 변수로서의 정보은닉의 특성을 지니게 된다.
이처럼 클로저란 그 함수가 가져야하는 상태 정보를 안전하게 보유한채 고유의 기능을 수행하는 함수로 생각할 수 있다.

3. 클로저에서 발생가능한 실수


클로저를 다룰때 발생할 수 있는 실수 중에 하나가 바로 varlet의 사용성에 따라 다르게 나타날 수 있다.


다음 예제를 통해 알아보도록 하자

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

단순하게 생각해볼 수 있는 기대값으로는 ‘0,1,2,3,4’의 출력이다. 하지만 실제 결과는 다를것이다. 왜냐하면 var 변수타입은 전역변수이기 때문에 i가 변경될 때마다 arr[] 배열 각각에 들어있는 클로저 함수들의 i 값 또한 계속 바뀌게 되어 최종적으로 5가 저장된 5개의 클로저함수들이 각각의 배열 요소로 저장되게 된다. 따라서 결과값은 ‘5,5,5,5,5’ 가 된다.

이를 올바른 기대값을 얻도록 변경하려면 i를 지역변수로 바꿔주기 위해 클로저 함수를 한번 더 감싸면 된다.

var arr = [];

for (var i = 0; i < 5; i++){
  arr[i] = (function (id) { // ②
    return function () {
      return id; // ③
    };
  }(i)); // ①
}

for (var j = 0; j < arr.length; j++) {
  console.log(arr[j]());
}

위의 함수부분에서는 id 값을 매개변수로 받는 함수를 선언하고 즉시실행함수로 i 값을 전달해주어, i가 내부함수의 지역변수로 사용되게끔 전달해주었다. 따라서 내부 function()이 반환될때 id 값은 외부에서 접근할 수 없는 private이자 자유변수로 저장되어 유지되는 상태로 클로저함수가 생성된다. 따라서 원하는 결과값인 ‘0,1,2,3,4’ 의 값을 얻을 수 있다.


위와 같은 코드를 let 을 사용하면 조금 더 간결하게 구현할 수 있다.

const arr = [];

for (let i = 0; i < 5; i++) {
  arr[i] = function () {
    return i;
  };
}

for (let i = 0; i < arr.length; i++) {
  console.log(arr[i]());
}

위에서 사용된 let 은 블록생명주기를 가진 지역변수이기 때문에 i가 변경 될때마다 클로저 생성시 내부적으로 사용되는 i 값에 영향을 주지 않는다. 따라서 보다 쉽게 구현할 수 있다.

람다식으로 표현하면 다음과 같이 표현할 수 있다.

const arr = [];

for (let i = 0; i < 5; i++) {
  arr[i] = () => i;
}

for (let i = 0; i < arr.length; i++) {
  console.log(arr[i]());
}


이번 포스팅을 통해 클로저에 대한 개념을 다시 한번 정리해보았다.



Reference
https://developer.mozilla.org/ko/docs/Web/JavaScript/Closures
https://poiemaweb.com/js-closure