티스토리 뷰
[속깊은 자바스크립트 강좌] Closure 쉽게 이해하기/실용 예제 소스
Unikys 2013. 1. 30. 13:56
* Closure는 자바스크립트에서 수 많은 응용들을 할 수 있는 정말로 중요한 개념이나 자바스크립트라는 언어를 더욱더 빛내줄 수 있는 특징이다. Closure를 모르고 자바스크립트를 개발하는 것은 10년전의 웹 언어 중심의 개발 방법론에 머무르고 있는 것과 같은 것이기 때문에 10년전 웹개발자에서 진정한 자바스크립트 개발자로 나아가기 위한 기본을 이제부터 들여다보자.
- 이전 글
2012/12/10 - [속깊은 자바스크립트 강좌] 시작 (예고편)
2012/12/17 - [속깊은 자바스크립트 강좌] 자바스크립트의 Scope와 Closure 기초
2013/01/07 - [속깊은 자바스크립트 강좌] function declaration vs function expression 차이점
2013/01/10 - [속깊은 자바스크립트 강좌] 함수를 호출하는 방법과 this의 이해
2013/01/21 - [속깊은 자바스크립트 강좌] Closure의 이해 / 오버로딩 구현하기
* Closure?
: 이전글에서 Closure에 대하여 이해를 할 수 있는 여러 가지 예들을 들여다 봤지만 실제적으로 이해는 약간 힘들었을 것이다. Closure는 자바스크립트에 있어서 C로 치자면 C에서 포인터를 바라보는 관점하고 똑같다. C에서 포인터를 이해하기를 포기하고 돌아서게 되면 진정으로 깊이 있는 C 개발자가 못 되듯이 자바스크립트에서도 Closure를 이해하지 못하면 깊이 있는 자바스크립트 개발자, 또는 웹 개발자가 되지 못하게 되는 것이다. 하지만 중요한 것은 "Closure는 뛰어난 기술이 아니다"라는 것이다. 포인터의 개념을 주소라는 개념으로 받아들이기 시작하면 아주 쉽게 이해하듯이 closure 또한 scope chain에서 하나의 scope를 생성해준다는 개념으로 이해한다면 아주 쉽게 이해가 가능할 것이다. 그렇다면 먼저 scope chain에 대해서 알아봐야할 것이다.
* Scope chain
: Scope chain은 이전 글에서 이미 다뤘던 내용이다. 하지만 이 개념과 closure를 반드시 연결해서 생각해야만 closure를 이해할 수 있다. Scope chain에 대해서 설명을 다시 하겠지만, 이전 글을 다시 훑어보고 와도 괜찮을 것이다.
- 참고
2012/12/17 - [속깊은 자바스크립트 강좌] 자바스크립트의 Scope와 Closure 기초
: 여기서 들었던 예는 다음과 같았다.
<div id="divScope0">Click me! DIV 0</div> <div id="divScope1">Click me! DIV 1</div> <div id="divScope2">Click me! DIV 2</div> <script> function setDivClick(index) { document.getElementById("divScope" + index).addEventListener("click", function () { // #1 alert("You clicked div #" + index); }, false); } var i, len = 3; for (i = 0; i < len; i++) { setDivClick(i); } </script>
: 여기서 이전글에서 말했던 closure가 생성되는 규칙을 발견할 수 있을 것이다. function 키워드를 따라가면서 확인해보면 위의 setDivClick 함수가 선언된 부분 안에 #1의 부분에 함수가 하나 선언 되어 addEventListener 함수의 인자로 넘어가고 있다. 바로 function 안에 function을 넣은 것이다. 그리고 이전 글 중에서 [속깊은 자바스크립트 강좌] 자바스크립트의 Scope와 Closure 기초의 글에서 Scope는 function을 기반으로 생성된다는 말을 했다. 그렇다면 function 안에 다른 function을 다시 선언한다는 것은 다른 말로 하나의 scope안에 다른 scope를 선언한다고도 볼 수 있다. 이것이 바로 scope 안에 scope를 만들어서 scope chain을 생성하는 과정이다. 위의 글에 있었던 그림도 다시 한번 보자.
위의 예에서 scope chain이 형성된 그림
: 여기서 setDivClick으로 생성된 scope의 안에 각 addEventListener의 인자로 선언된 함수들의 scope가 하위에서 setDivClick 함수의 scope를 참조하는 것을 알 수 있다. 이렇게 여러 개의 함수 안에 함수가 호출되면 하위 scope를 생성하여 상위 scope의 변수들을 접근할 수 있는 것을 scope chain이라고 하는 것이다. 여기서 중요한 것은 하위 scope에 해당하는 function이 살아있다면 상위의 scope들은 죽지 않고 계속 살아있게 된다는 것이고 이것이 closure의 가장 기본적인 개념이다. 즉, 위의 div0.onclick 이라는 함수가 살아있는 동안 setDivClick을 통해 생성되었던 scope는 계속 살아있게 된다.
* 기본적인 예
: Closure를 이해하기 쉽게 sum 이라는 함수를 선언해보자.
function sum(base) { var inClosure = base; // #1 return function (adder) { // #2 return inClosure + adder; }; }; // #3 var fiveAdder = sum(5); // #4: inClosure를 5로 설정하고 #2의 함수 리턴 fiveAdder(3); // === inClosure(5) + adder(3) === 8 var threeAdder = sum(3); // #5: inClosure를 3으로 설정하고 #2의 함수 리턴
: 위의 예에서도 보면 function 안에 function이 선언되고 내부의 function이 리턴 되는 것을 볼 수 있고 외부 함수에서 base 인자가 넘어오면 내부의 함수에서는 inClosure 변수를 설정하게 된다. 이렇게 소스코드를 통해서 scope가 생성되는 것을 한번 살펴보면 아래와 같이 된다.
: 이것은 scope의 뼈대, 또는 template이라고 보면 된다. 중요한 것은 위의 scope가 실제로 생성 된것이 아니라는 점이다. 그냥 만약에 scope가 생성된다면 위의 구조를 가지게 된다는 것을 그림으로 표현해본 것이다. 만약 sum 함수의 내부 함수를 받아서 사용하게 된다면 위의 scope를 따르게 된다. 그럼 위의 뼈대를 토대로 하나씩 실행되는 과정을 한줄씩 살펴보자.
var fiveAdder = sum(5);
: 위의 구문을 통해 function sum이 호출 되고 base는 5로 넘어오고 inClosure 변수도 5로 설정한다. 그리고 inClosure 변수를 참조하는 내부 함수를 리턴하여 fiveAdder에 대입한다. 현재 fiveAdder의 scope 상황은 다음과 같을 것이다.
: 위의 scope 뼈대에서 fiveAdder는 실제로 사용하게 되는 함수 A를 할당 받게 되어 위의 scope 뼈대를 통해 하나의 scope chain을 생성하여 가지고 있게 된다. fiveAdder는 오른쪽의 A 함수를 받아 가지고 있게 된다. 여기서 fiveAdder에서 할당 받는 함수 A는 어디서 온것인가 보면, 바로 #2에서 리턴한 그 함수가 바로 A인 것이다.
: 이제부터 fiveAdder를 통해 함수를 접근하게 되면 위의 scope chain을 따르게 된다. 여기서 하나 짚고 넘어가자면, 그림상으로 왠지 순환 구조를 가지고 있는 듯 하지만 모든 scope는 global scope에서 끝나게 된다. 그리고 global scope에 있는 fiveAdder는 scope chain이 이어진 것이 아니라 오른쪽 함수 A의 레퍼런스, C에서 말하자면 포인터를 가지고 있는 것이고, 나중에 fiveAdder를 실제로 '호출'하게 될 때에 위의 오른쪽 A는 해당하는 scope chain을 사용하게 된다는 것이다. 이 scope chain은 fiveAdder가 또 다른 함수의 인자로 넘어가든 fiveAdder가 메모리에서 사라질때까지 계속 유지하게 된다. 이것이 이전 글에서 말했던 '퍼포먼스의 문제'에 대하여 조금 더 깊게 이해할 수 있을 것이다. 이제 fiveAdder(3)을 호출하게 되면 오른쪽 A의 adder 인자에 3이 들어가게 되고, 내부 함수에서 inClosure + adder를 하게 되면 바로 위의 scope에 있는 inClosure = 5와 인자인 adder = 3을 이용하여 8이라는 값을 리턴하게 된다. 여기까지는 그렇게 특별할 것이 없다. 이제 다음으로 넘어가면,
var threeAdder = sum(3);
: 그렇다면 이번에는 다시 threeAdder를 호출하게 되면 어떻게 될까. 이번에는 상당히 생각이 많아질지도 모른다. 기분상 fiveAdder의 inClosure까지 변환되어 버릴 것 같지만 그렇지 않다.
: 위와 같이 함수 B가 사용하는 하나의 새로운 scope chain이 생성되어 threeAdder에서 사용하도록 된다. 같은 함수를 통해서 받은 리턴 값의 함수가 A와 B 2개로 각각 생성되어 이제 fiveAdder(3)을 하게 되면 8의 결과 값이, threeAdder(3)을 하게 되면 6의 결과 값이 나오게 된다. 이렇게 closure를 통해서 각 함수들은 자기만의 고유의 값을 가지고 scope chain을 유지하면서 그 chain 안에 있는 모든 변수의 값을 유지하게 된다.
* Scope chain의 생성
: 단순히 보면 왜 그때그때 scope chain이 생성될까 이해하기 힘들지도 모르지만 속에 돌아가는 구조를 자세하게 살펴보면 이해를 할 수 있을 것이다. Closure에 대해서 더 깊어지기 전에 위의 현상에 대해서 이해하고 넘어가자. 위의 예에서 '익명 함수'를 이용하고 있다. 익명 함수는 함수의 이름을 지정 안하고 사용하는 경우를 뜻하고 위의 예에서는 다음의 부분이다.
return function (adder) { // #2 return inClosure + adder; };
: 이 부분에서 #2는 익명 함수가 선언되어 리턴되는 부분으로 내부에서는 이것은 새로운 Function 객체를 만들어 리턴을 하게 되는 과정과 같다. 다르게 표현하면 내부적으로는 아래와 '비슷한' 프로세스가 일어나게 된다.
return new Function("adder", "return inClosure + adder;");
: 자바스크립트에서 {}는 object literal, []는 array literal이라고 하고 위의 function () {} 는 function literal이라고 하는 것은 내부적으로 각각 {}는 Object 객체, []는 Array 객체, function () {}는 Function 객체를 만들기 때문이다. 이렇게 sum 함수를 호출함으로써 내부에서 #2를 거치게 될 때마다 매번 새로운 함수를 생성하여 리턴하는 것이라고 보면 되고, 이럴 때마다 각 함수의 scope chain은 새롭게 할당되어 저장하게 된다. 따라서 위의 예에서 fiveAdder = sum(5)를 호출 할 때 new Function과 비슷한 과정을 통해 함수 A가 생성되어 리턴되고, threeAdder = sum(3)을 호출 할 때 또 new Function을 통해 함수 B가 생성 된 것이다. 이렇게 보면 매번 sum을 호출할 때마다 새로운 함수와 그 함수의 scope chain이 따로따로 생성된 것을 이해할 수 있을 것이다. 여기서 재밌는 것은 fiveAdder와 threeAdder의 외부 표현식은 같다는 것이다. toString() 함수를 통하여 호출해보면 둘다 아래와 같이 나오게 된다.
: 하지만 fiveAdder !== threeAdder이다. 이렇게 똑같은 모양의 함수들이 매번 새롭게 나오고 있는 것이다. 이렇게 두 함수는 일치한 모양을 가지고 있지만 둘의 동작이 달라지는 것은 바로 숨겨져 있는 closure 때문이다.
- 덧: 위에서 new Function과 '비슷한' 프로세스가 일어난다고 말한 것은 new Function을 이용해서 생성한 함수는 로컬 변수만 이용 가능하지만 function literal로 생성한 함수는 Closure를 생성하여 외부의 scope에 있는 변수들을 접근 가능하다는 점이다. 따라서 closure를 생성할 때에는 new Function을 이용하면 안되고 function () {} 으로 함수를 생성해야한다.
- 덧2: 위의 그림에서 보면 base와 inClosure의 값은 항상 같고 같은 scope에 자리하고 있다. 따라서 inClosure 변수는 사실상 필요없지만 closure 내부의 로컬 변수도 유지 된다는 것을 보여주고자 추가적으로 선언했다.
* Closure를 쓰는 실제 예
: 이전 글에서 closure의 이용 방법에 대하여 몇가지를 이야기하기도 했지만 '어디서' '언제' 사용할지에 대해서는 감을 잡기가 어려울 것이다. closure를 가장 많이 사용하는 것은 이전 글에서 말했던 경우들, 라이브러리에서 private이나 나의 변수를 보호하고 싶을때라던가 self-defining function인 경우, static으로 변수를 이용하고 싶은 경우에도 있지만 가장 일상적으로는 closure를 활용하는 경우는 콜백함수에 추가적인 값들을 넘겨주거나 처음에 초기화 시켰던 값을 계속 유지하고 싶을 때일 것이다. 사실 이렇게 글로 실컷 읽어봤자 위의 fiveAdder 등과 같이 실용적이지도 않은 곳에만 쓰이는 탁상공론에 불과한 개념이라고 느낄 수 있을 것이다. 따라서 실제 상황에서도 사용할 수 있는 간단한 예를 한번 보자.
- 목적: 특정 div에 버튼1에 대한 콜백으로 div를 추가/버튼 2에 대한 콜백으로 img를 계속 추가
<div id="wrapper"> <button data-cb="1">Add div</button> <button data-cb="2">Add img</button> <button data-cb="delete">Clear</button> 아래에 추가<br/> <div id="appendDiv"></div> </div> <script> (function () { var appendDiv = document.getElementById("appendDiv"); // #1 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"); // #2 div.innerHTML = "1번"; return function () { return div.cloneNode(true); // #3 } }()), "2":(function () { var img = document.createElement("img"); img.src = "http://www.google.co.kr/images/srpr/logo3w.png"; return function () { return img.cloneNode(true); } }()), "delete":function () { appendDiv.innerHTML = ""; return document.createTextNode("Cleared"); } }; }()); </script>
: 여기서 Closure를 활용한 곳을 보면 크게 2가지로 볼 수 있다. 바로 #1 부분에서 전체 함수들이 공통적으로 접근하고자하는 변수(appendDiv)를 선언하여 한번의 초기화 만으로 이후에 함수들이(여기서는 append(e) 함수) 지속적으로 접근 가능하도록 한 부분과 #2에서 현재 화면에 안 보이는 가상의 노드를 만들어 보관하고 있는 #2 부분이다. 나중에 콜백 함수에서는 append 함수를 호출하여 클릭한 버튼에 따라 변수 callback에 선언되어있는 다른 내부 함수를 호출하게 되고, 이벤트가 발생하게 되면 해당 콜백 함수가 호출되어 #3에서 #2의 가상 노드의 복제 노드를 생성하여 리턴하여 appendDiv에 추가하게 된다. 이렇게 콜백 함수를 동적으로 생성할 때 초기화 되어있는 값들을 유지하는 것이 퍼포먼스상 유리한 경우, 특히 DOM을 생성하거나 탐색하여 가져오는 경우 한번 로드했던 DOM 객체를 보관하고 있는 것이 여러 모로 유리하기 때문에 이렇게 DOM을 적극적으로 활용할 때야말로 Closure를 진정으로 효과적으로 사용할 수 있을 것이다.
: 그렇다고 위의 예제에서 closure는 필수적인 요소가 아니다. 이렇게 closure는 언제든 사용할수도 있고 안 할수도 있지만, 위에서 말했듯 중복된 DOM 탐색이나 DOM 생성을 할 때에 효과적으로 closure를 이용한다면 월등한 퍼포먼스를 가져올 수 있을 것이다. 이렇게 실용에서 closure를 가장 많이 활용할 수 있는 부분을 살펴보면, 다음과 같이 말할 수 있을 것이다.
- 반복적으로 같은 작업을 할 때 같은 초기화 작업이 지속적으로 필요할 때, 콜백 함수에 동적인 데이터를 넘겨주고 싶을 때 Closure를 사용하자!
- 덧: 이 예제에서는 실제로 활용 가능한 다양한 예들이 같이 포함되어 있다.
- closure로 appendDiv를 한 번만 검색하고 조회하여 초기화하고 계속 보관하는 활용 방법
- div, img 등 가상 노드를 만들어놓고 필요할 때마다 복제하여 생성할 수 있는 활용 방법
- appendDiv에만 이벤트 핸들러를 추가하여 관리할 수 있는 event delegation 활용 방법
- 이벤트가 발생한 target element를 크로스 브라우져에서 가져올 수 있는 방법
- var callback = {...}를 활용하여 대상에 따라 동적으로 콜백 함수를 변화 시키는 활용 방법
- HTML5의 스펙에 맞게 사용자 정의 속성을 "data-*" 여기서는 "data-cb"로 설정한 것
- 만약 callback 함수에 인자를 넣어주게 되면 div를 추가하되, 안의 내용 또한 동적으로 설정할 수 있는 위의 예 응용 방법 등
: 나중에 어느 정도 강좌를 진행하고 나면 이 예제를 다시 가져와서 더 자세하게 세부적으로 들어가서 정말로 실용에서 사용할 자바스크립트 개발 방법론에 대하여 공부해보도록 하자. 이 간단한 예제는 자바스크립트 개발에서 사용할 수 있는 기본적인 틀을 하나 제시하고 있고, 지금이라도 천천히 하나씩 뜯어보면 이해할 수 있는 자바스크립트만의 독특한 개발 방법들이다. 이 방법들을 제대로 이해하고 활용할 줄 안다면 자바스크립트를 더욱더 깊이 있게 다룰 수 있게 될 것이다.
* 정리
- Closure는 function 안에 function이 있게 되면 기본적으로 생성된다.
- Closure는 scope chain이 생성됐을 때의 변수 값들을 보존하고 기억하게 된다.
- 함수가 메모리에서 없어질 때까지 따라다니게 된다.
- 같은 모양의 함수이더라도 다른 closure를 가지고 있을 수 있다.
- 함수가 다른 곳에서 사용되더라도 처음에 생성되었던 scope chain이 끝가지 따라다니게 된다.
- 다른 곳에서 사용되는 대표적인 경우: 함수를 리턴하여 사용, 다른 함수의 인자로 넘겨줘서 사용, 콜백으로 사용
- Closure는 아주 쉽다!
[속깊은 자바스크립트 강좌] Closure 쉽게 이해하기/실용 예제 소스 끝.
- 다음 편
2013/02/13 - [속깊은 자바스크립트 강좌] 쉬어가기: 웹 개발 방법론의 변화/자바스크립트의 재발견
2013/02/22 - [속깊은 자바스크립트 강좌] 객체지향의 기본: prototype
2013/10/04 - [속깊은 가바스크립트 강좌] 상속, new와 Object.create의 차이
2013/10/29 - [속깊은 자바스크립트 강좌] 글로벌(전역) 변수와 window 객체
2013/11/06 - [속깊은 자바스크립트 강좌] 변수 선언 방법에 대하여
2016/11/13 - [속깊은 자바스크립트 강좌] 마무리(는 책으로!)
- Total
- Today
- Yesterday
- 뽐뿌
- Writing
- Python
- 사진
- 안드로이드
- lecture
- 서울
- 속깊은 자바스크립트 강좌
- java
- HTML5 튜토리얼
- Android
- 자바스크립트
- GX-10
- 탐론 17-50
- mini project
- ny-school
- 삼식이
- gre
- TIP
- Javascript
- K100D
- 강좌
- 안드로이드 앱 개발 기초
- gae
- c++
- php
- 팁
- HTML5
- google app engine
- 샷
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 | 31 |