티스토리 뷰

Javascript - 속깊은 자바스크립트[2. 자바스크립트의 스코프와 클로저 -클로저 ]


|시작하기에 앞서

앞서 스코프 개념에 대해서 살펴보았고, 고급 자바스크립트 개발자가 되기 위해 필수적 개념인 클로저에 대해 자세히 예제들과 함께 살펴보자.


| 2. 클로저란?


클로저란 아래와 같이 정의 할 수  있다.


특정 함수가 참조하는 변수들이 선언된 렉시컬 스코프 (lexical scope)는 계속 유지되는데, 그함수와 스코프를 묶어서 클로저라고 한다.


말로 설명하면 어려울 수있어 아래의 예제를 살펴보면서 이해해보자. 먼저 기본적으로 클로저가 나타나는 기본적인 환경은 스코프안에 스코프가 있을때, 즉 function안에 function이 선언 되었을때 이다

<script>
function outer () {
   var count = 0;
   var inner = function () {
       return ++count;
  };
   return inner;
}
var increase = outer();
console.log(increase());    // === 1
console.log(increase());    // === 2
</script>

원래 count 변수는 outer() 함수의 로컬 변수이므로 일반적인 방법으로는 외부에서 접근할 수 없다. 마치 객체지향 언어에서 흔히 말하는 private 변수와 비슷하다. 그런데 count 변수에 접근하는 또 다른 함수 inner를 outer() 함수의 반환값으로 지정하고, 이를 글로벌 영역에 있는 increase 변수에 할당함으로써, outer() 함수 외부에서도 increase 변수를 통해 count 변수에 접근할 수 있게 되었다. 이러한 상황이 클로저의 가장 기본적인 개념이다.
위의 예를 더 활용한 아래예제를 살펴보자.

<script>
function outer () {
   var count = 0;
   return {
       increase: function () {
           return ++count; },
       decrease: function () {
           return --count;
      }
  };
}
var counter = outer();
console.log(counter.increase());    // === 1
console.log(counter.increase());    // === 2
console.log(counter.decrease());    // === 1
var counter2 = outer();
console.log(counter2.increase());   // === ?
</script>
위의 ? 결과값은 1이나오게 된다. counter와 counter2 변수는 함수가 호출될때 별도의 스코프가 생성되어 count 변수가 따로따로 저장되기 때문이다.

여기서 들수 있는 의문점은 어떠한 사람은 count라는 값을 공유할수 있도록 count 변수를 static 변수처럼 만들고 싶어할수 있을것이다.  함수간 공유 할수있는 static 변수를 만들기 가장 쉬운 방법은 글로벌 변수를 사용하는것일지도 모른다. 하지만 많은 개발자들이 글로벌 변수의 사용을 자제하고 있으니 앞서 설명했던 IIFE와 클로저를 응용해서 문제를 풀어보자.
<script>
var countFactory = (function () {
   var staticCount = 0;
   return function () {
       var localCount = 0;
       return {
           increase: function () {
               return {
                   static: ++staticCount,
                   local: ++localCount
              };
          },
           decrease: function () {
               return {
                   static: --staticCount,
                   local: --localCount
              };
          }
      };
  };
}());
var counter = countFactory(),
   counter2 = countFactory();
console.log(counter.increase());
console.log(counter.increase());
console.log(counter2.decrease());
console.log(counter.increase());
</script>

앞의 counter 예에서 다시 IIFE로 스코프를 하나 더 추가하여 최상위에 staticCount 변수를 추가했다. 1번줄에서 IIFE로 statuc으로 사용할 로컬변수를 선언한다음, 앞의 예제에서 사용했던 함수를 반환한다. 해당함수가 실행되면 5번줄에 정의된 객체를 반환하여 counter별로 localCount 변수를 사용할수 있고, 공용으로 staticCount를 사용가능하다.
실행결과는 아래와 같다.
Object{static: 1, local: 1}

Object{static: 2, local: 2}

Object{static: 1, local: -1}

Object{static: 2, local: 3}
이처럼 IIFE를 한번 선언해 클로저를 생성하는 방법은다양한 곳에 활용될 수있고, 자바스크립트 개발자가 되려면 기본적으로 이해하고 있어야 할 내용이다. 위의 내용들을 살펴보면 클로저는 outer()와 같은 외부함수에서 inner와 같은 내부함수를 반환하여 다른곳에서 해당함수를 호출할때 발생한다는 것을 알 수 있다.


처음 위의예제와 글들을 읽었을때, 이해가 잘되지 않았다... IIFE와 클로저에 대해서 다시 학습하고 싶으면, http://ideveloper2.tistory.com/77 의 클로저를 이용한 문제해결을 다시 참고하길 바란다. 이 예제에서 for문을 돌면서 index를 즉시실행함수와 클로저로 스코프체인을 형성해 새로운 각각의 스코프를 만들었는데, 그 개념을 이해하고 다시 위의 예제에 접목시키면 좀 더 이해가 쉬울것이다.

>2.1 클로저 쉽게 이해하기

이번에는 스코프 체인이 내부적으로 어떻게 구성되는지를 살펴보면서 다시한번 클로저를 이해해보자.

<script>
function sum(base) {
   var inClosure = base;
   return function (adder) {
   return inClosure + adder; };
};
var fiveAdder = sum(5);     // inClosure = 5 and return function
console.log(fiveAdder(3));  // === inClosure(5) + adder(3) === 8
var threeAdder = sum(3);    // inClosure = 3 and return a new function
</script>
  • 위의 예제는 같은 함수를 통해서 받은 값은 각각 A와 B로 2개의 함수가 생성되어, fiveAdder로 호출하면 A의 스코프체인을 사용하고, threeAdder로 호출하면 B의 스코프체인을 사용한다.
  • 이처럼, 클로저를 통해 각 함수는 자기만의 고유한 값을 보유하고, 스코프 체인을 유지하면서 그 체인안에 있는 모든 변수의 값들을 유지한다. 
  • 위예에선, sum(5)를 호출할때 새로운 함수를 만들고, sum(3)을 만들때 다시 새로운 함수를 만드는 과정을 거친다. 다시말하면, sum() 함수를 호출할때마다 새로운 함수와 그함수가 참조하는 스코프체인이 각각 생성된다는 것을 이해할 수 있을것이다.
  • 그리고 참고할점은 sum()함수를 호출할때마다 같은 모양의 함수들이 매번 새롭게 나오지만, 두함수가 같지 않은 이유는 두 함수가 할당받은 스코프 체인, 숨겨져있는 클로저가 다르기 때문이다.

지금까지는 함수 안에 함수가 있어서 내부의 함수가 반환 될때 클로저가 발생하고 사용된다고 하였는데, 클로저가 생기는 다른 상황이 있다. 이미 많은 사람이 사용하고 있는 패턴에서도 클로저를 사용하고 있을지도 모른다. 그 예를 아래에서 살펴보자.

<html>
<body>
   <button id="btnToggle">Toggle Pending</button>
   <div id="divPending">Pending</div>
   <script>
(function () {
   var pendingInterval = false,
       div = document.getElementById("divPending"),
       btn = document.getElementById("btnToggle");
       function startPending() {
           if (div.innerHTML.length > 13) {
               div.innerHTML = "Pending";
          }
           div.innerHTML += ".";
      };
       btn.addEventListener("click", function () {
           if (!pendingInterval) {
               pendingInterval = setInterval(startPending, 500);
          } else {
               clearInterval(pendingInterval);
               pendingInterval = false;
          }
      });
  }());
   </script>
</body>
</html>

위는 클로저를 이용한 아주 간단한 예이면서 자바스크립트 라이브러리 또는 모듈의 기본 형태와 비슷하다고한다. pedingInterval 아래 3줄에서는 내부함수에서만 접근 할 수 있는 private 변수를 선언했다.
글로벌 변수가 아니라 클로저를 이용해 내부에 로컬변수를 사용함으로써, 글로벌 변수를 사용하지 않는 아주 좋은 코드이다.
위의 상황에서 클로저가 발생한 경우는 크게 두가지인데, div.innerHTML="pending" 부분과, setInterval() 함수에서 첫번째 인자로 startPending 함수를 사용하는 부분이다. 이러한 경우 함수안에 함수가 있어서, 내부에 있는 함수가 반환되지 않고 이벤트 콜백함수로 호출될때도 클로저가 발생한다.

>2.2 클로저의 실제 활용 예

클로저를 가장 많이 사용하는것은 자바스크립트 라이브러리나 모듈에서 private 로 나의 변수를 보호하고 싶을때나 static 변수를 사용하고 싶을때이다. 그리고 일상적으로 콜백 함수에 추가적인 값들을 넘겨줘서 활용하거나 처음에 초기화했던 값을 계속 유지하고 싶을때도 유용하게 사용 할 수 있다.

<html>
<body>
   <div id="wrapper">
       <button data-cb="1">Add div</button>
       <button data-cb="2">Add img</button>
       <button data-cb="delete">Clear</button>
       Adding below...<br/>
       <div id="appendDiv"></div>
   </div>

   <script>
  (function () {
       var appendDiv = document.getElementById("appendDiv");
       document.getElementById("wrapper").addEventListener("click", append);
       function append(e) {
           var target = e.target || e.srcElement || event.srcElement;
           var callbackFunction = callback[target.getAttribute("data-cb")];
           appendDiv.appendChild(callbackFunction());
      };
       var callback = {
           "1": (function () {
               var div = document.createElement("div");
               div.innerHTML = "Adding new div";
               return function () {
                   return div.cloneNode(true);
              }
          }()),
           "2": (function () {
               var img = document.createElement("img");
               img.src="https://t1.daumcdn.net/cfile/tistory/011F554E50FD140F2B";
               return function () {
                   return img.cloneNode(true);
              }
          }()),
           "delete":function () {
               appendDiv.innerHTML = "";
               return document.createTextNode("Cleared");
          }
      };
  }());
   </script>
</body>
</html>
위의 예에서도 클로저를 활용한 곳을 보면 크게 두곳으로 볼수있는데, appendDiv 부분과 (한번의 초기화로 함수들이 계속 접근함), callback 변수 안에서의 div와 img 부분이다. 위 예제는 특별히 스코프 체인이 어떻게 연결되어 있는지 책을 참고해 첨부한다.


위의 예제와 같이 최초에 초기화 된 고정적인 값이나 변수를 자주 이용하는 경우 클로저를 통해 최초에 초기화해두고 콜백함수에서 지속해서 참조하는것이 퍼포먼스 상 유리하게 작용 할 수 있고, 객체의 속성이 자유롭고 쉬운 자바스크립트에서는 이러한 디자인 패턴이 아주 효율적이라고 한다. 특히 dom 같은 엘리먼트를 적극적으로 이용할 때 이방법이 아주 좋다고 한다.


>2.2 클로저의 단점


  • 클로저는 메모리를 소모한다.

  • 스코프 생성과 이후 변수 조회에 따른 퍼포먼스 손해가 있다.

위의 두가지 단점은 클로저를 사용하면 어떠한 상황에서도 발생한다고 한다. 따라서 클로저는 아무 때나 남발해서 사용하기 보다는 정말로 필요한 곳에 요긴하게 사용해야 한다고한다.

클로저는 메모리를 소모한다.

  • IE 최신버전에서는 해결되었지만 구버전에서는 특정 DOM에 이벤트를 할당해 놓고, DOM 삭제를 하면 메모리 누수까지 발생한다고 한다. 클로저는 이러한 메모리 누수를 가속화 한다고한다.
  • 따라서 클로저를 생성할때는 하나의 거대한 클로저를 생성하기 보다는 각 변수나 함수의 생명주기를 분석하여, 효율적으로 나누는 것이 좋다고 한다.

스코프 생성과 이후 변수 조회에 따른 퍼포먼스 손해가 있다.


  • with 구문과 마찬가지로 클로저의 하위에 있는 함수에서 상위에 있는 변수에 접근하고자 할때 클로저로 생성한 스코프를 탐색해야 하는 문제이다. 
  • 최근 자바스크립트 엔진들이 이러한 스코프 체인 내의 변수 탐색 최적화를 하고 있어서 스코프 체인이 하나나 두개이면 큰차이는 없다고 한다. 하지만 과하게 쌓이면 퍼포먼스에 영향을 끼친다고 한다.
  • 클로저는 익숙하지 않으면 이해하기 어렵다는 점이 있기에, 

  1.어떠한 목적으로 클로저를 생성하고

  2.어디에서 생성되었는지 불분명할때가 많다


고한다. 따라서 협업할때는 명확한 주석과 문서화가 필요하다고한다.


| 마치며


자바스크립트가 단순한 스크립트언어, 그리고 웹언어의 보조 역할일때는 필요없는 기능이였으나, 클라이언트 기반의 웹을 개발할 때 클로저를 활용하면 자원을 효율적으로 사용할 수 있다고 한다. 그리고 든 생각은 클로저가 모듈화와 관련해서는 정말 필수적인 개념이라고 생각했다. 왜냐하면 코드를 잘쪼개고 모듈화 할때 필수적인것이 전역변수를 쓰지않고 모듈내에서 스코프 관리를 잘해야 하기 때문이다. 아직은 이해가 확실히는 되지않은것 같고 흐름은 잡은것 같아, 필수적개념인 클로저에 대해 계속적으로 학습해 나가야겠다.



댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
TAG
more
«   2024/04   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30
글 보관함