본문 바로가기
개발/Web

JS closure

by amkorousagi 2021. 3. 31.

JS closure에 대해 알아봅시다.

closure
closure

JS closure

클로저는 함수와 함수가 선언된 어휘적 환경의 조합이다. 클로저를 이해하려면 자바스크립트가 어떻게 변수의 유효 범위를 지정하는지(Lexical scoping)를 먼저 이해해야 한다.
-mozilla-

위 설명과 같이, JS에서 함수는 함수 선언 당시의 어휘적 환경(사용가능한 지역 변수 등)과 항상 함께 다닙니다.

이런 방식으로 함수를 처리하는 것이 왜 유용할까요?

JS closure 을 사용하는 이유

클로저는 어떤 데이터(어휘적 환경)와 그 데이터를 조작하는 함수를 연관시켜주기 때문에 유용하다. 이것은 객체가 어떤 데이터와(그 객체의 속성) 하나 혹은 그 이상의 메서드들을 연관시킨다는 점에서 객체지향 프로그래밍과 분명히 같은 맥락에 있다.
결론적으로 오직 하나의 메소드를 가지고 있는 객체를 일반적으로 사용하는 모든 곳에 클로저를 사용할 수 있다.
-mozilla-

위 설명과 같이, JS가 함수와 함수의 어휘적 환경을 묶어서 다루는 이유는 객체지향 프로그래밍(OOP) 때문입니다.

JS에서는 모든 것이 객체이죠.

그래서 함수를 선언하면 어디에서든지 (추가적인 지역변수 전달 없이도) 그 함수만을 독립적으로 사용할 수 있도록,

즉 함수와 그 lexical scoping을 객체로 다룰 수 있도록 JS closure이 기능하는 것입니다.

 

이런 생각은 특히 웹에서 이벤트 처리에서의 callback 함수의 사용으로, 함수가 함수를 리턴해야 할 때 유용하게 사용됩니다.

다음의 코드를 봅시다.

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;

위와 같이 callback 함수를 만들때, 그 callback 함수를 만들기 위해 선언된 지역 함수들을 인자로 넘기지 않아도,

lexical scope와 함수를 함께 처리하는 closure 덕분에 훨신 간편하게 이벤트 처리를 할 수 있습니다.

 

또 callback함수의 예뿐만 아니라, 프로그래머에게 무척 편리합니다.

함수를 선언할 때, 선언당시의 어휘 환경이 함수를 호출할 때도 유지가 되므로 함수를 어디에서 호출하던 항상 동작하다는 걸 보장할 수 있죠.

 

이뿐 아니라, closure은 private method를 흉내 낼 수도 있습니다.

var counter = (function() {
  var privateCounter = 0;
  function changeBy(val) {
    privateCounter += val;
  }
  return {
    increment: function() {
      changeBy(1);
    },
    decrement: function() {
      changeBy(-1);
    },
    value: function() {
      return privateCounter;
    }
  };
})();

console.log(counter.value()); // logs 0
counter.increment();
counter.increment();
console.log(counter.value()); // logs 2
counter.decrement();
console.log(counter.value()); // logs 1

위 예제에서 changeBy 함수는 오직 노출된 함수(increment, decrement, value..)로만 접근하여 객체 내부에서만 호출할 수 있죠. 또 익명 함수 호출하여 리턴 값만을 저장했기 때문에, privateCounter와 changeBy에 접근할 수 있는 방법은, 리턴된 세 함수의 closure를 통해서 접근하는 방법밖에 없습니다. 이로서 private 변수와 메서드를 구현했다고 말할 수 있죠.

 

이것은 closure를 통해 OOP에서 정보 은닉과 갭슐화 같은 이점을 그대로 누릴 수 있다는 걸 의미합니다.

 

반복문에서의 closure

function showHelp(help) {
  document.getElementById('help').innerHTML = help;
}

function setupHelp() {
  var helpText = [
      {'id': 'email', 'help': 'Your e-mail address'},
      {'id': 'name', 'help': 'Your full name'},
      {'id': 'age', 'help': 'Your age (you must be over 16)'}
    ];

  for (var i = 0; i < helpText.length; i++) {
    var item = helpText[i];
    document.getElementById(item.id).onfocus = function() {
      showHelp(item.help);
    }
  }
}

setupHelp();

위 코드를 보고 문제점을 찾으셨나요?

문제점은 3개의 각각 다르게 선언된 함수가 하나의 lexical scope를 공유하고 있다는 점에 있습니다.

showHelp(item.help)에서 item은 해당 함수의 지역변수가 아닌 closure에서 참조하고 있습니다.

그리고 이 closure을 3개의 각각 정의된 함수에서 공유하고 있죠.

따라서, item은 가장 마지막에 초기화된 age에 대한 값입니다.

 

이런 상황을 해결하기 위해선 각 함수마다 독립된 closure를 생성해주면 됩니다.

다음 코드는 익명 closure를 사용하여 각함수 마다 독립된 closure를 생성해주는 코드입니다.

function showHelp(help) {
  document.getElementById('help').innerHTML = help;
}

function setupHelp() {
  var helpText = [
      {'id': 'email', 'help': 'Your e-mail address'},
      {'id': 'name', 'help': 'Your full name'},
      {'id': 'age', 'help': 'Your age (you must be over 16)'}
    ];

  for (var i = 0; i < helpText.length; i++) {
    (function() {
       var item = helpText[i];
       document.getElementById(item.id).onfocus = function() {
         showHelp(item.help);
       }
    })(); // Immediate event listener attachment with the current value of item (preserved until iteration).
  }
}

setupHelp();

 

또 다른 해결 방법이 있습니다.

더 많은 closure를 사용하는 것이 싫다면,

let 키워드를 사용하여 모든 closure가 블록 범위 변수를 바인딩하게 하여 추가적인 closure를 사용하지 않고도 동작하도록 할 수 있습니다.

function showHelp(help) {
  document.getElementById('help').innerHTML = help;
}

function setupHelp() {
  var helpText = [
      {'id': 'email', 'help': 'Your e-mail address'},
      {'id': 'name', 'help': 'Your full name'},
      {'id': 'age', 'help': 'Your age (you must be over 16)'}
    ];

  for (var i = 0; i < helpText.length; i++) {
    let item = helpText[i];
    document.getElementById(item.id).onfocus = function() {
      showHelp(item.help);
    }
  }
}

setupHelp();

let은 말 그대로 해당 블록에서만 유효한 변수를 만든다.(설령 변수 이름이 같더라도)

var은 선언 끌어올리기 및 같은 이름으로 초기화 시 어느 블록이든 모두 공통된 변숫값을 공유한다. 

Closure의 단점

closure로 해당 함수에 대한 lexical scope에 있는 지역 변수 등을 참조 가능하다는 의미는,

메모리에서 추가적인 공간을 필요로 한다는 것을 의미합니다.

 

따라서, 특정 작업에 closure가 필요하지 않는데 다른 함수 내에서 함수를 불필요하게 작성하는 것은 피해야 합니다. 위에서 말했듯이 처리 속도 및 메모리 소비 측면에서 스크립트 성능에 부정적인 영향을 미치기 때문이죠.

 

대표적인 예로는, 함수 생성자로 새로운 객체를 생성할 때 함수 생성자에 직접 함수를 선언하면, 해당 함수에 대한 클로저가 불필요하게 생깁니다.

따라서, 함수 생성자보다는 해당 함수의 프로토타입에 함수를 정의해야 합니다. 추가적인 메모리 소비 없이, 함수의 프로토타입에 대한 링크로 자식에서 해당 함수를 사용할 수 있기 때문이죠.

 

코드로 예시를 봅시다.

다음은 함수 생성자에 직접 함수를 선언하는 좋지 않은 예시 코드입니다.

function MyObject(name, message) {
  this.name = name.toString();
  this.message = message.toString();
  this.getName = function() {
    return this.name;
  };

  this.getMessage = function() {
    return this.message;
  };
}

다음은 위 코드를 수정한, 함수의 프로토타입에 함수를 선언하는 좋은 예시 코드입니다.

function MyObject(name, message) {
    this.name = name.toString();
    this.message = message.toString();
}
(function() {
    this.getName = function() {
        return this.name;
    };
    this.getMessage = function() {
        return this.message;
    };
}).call(MyObject.prototype);

 

참고한 사이트:

 

클로저 - JavaScript | MDN

클로저 클로저는 함수와 함수가 선언된 어휘적 환경의 조합이다. 클로저를 이해하려면 자바스크립트가 어떻게 변수의 유효범위를 지정하는지(Lexical scoping)를 먼저 이해해야 한다. 다음을 보자:

developer.mozilla.org

댓글