* Closure는 자바스크립트에서 수 많은 응용들을 할 수 있는 정말로 중요한 개념이나 자바스크립트라는 언어를 더욱더 빛내줄 수 있는 특징이다. Closure를 모르고 자바스크립트를 개발하는 것은 10년전의 웹 언어 중심의 개발 방법론에 머무르고 있는 것과 같은 것이기 때문에 10년전 웹개발자에서 진정한 자바스크립트 개발자로 나아가기 위한 기본을 이제부터 들여다보자.
: 이전글에서 Closure에 대하여 이해를 할 수 있는 여러 가지 예들을 들여다 봤지만 실제적으로 이해는 약간 힘들었을 것이다. Closure는 자바스크립트에 있어서 C로 치자면 C에서 포인터를 바라보는 관점하고 똑같다. C에서 포인터를 이해하기를 포기하고 돌아서게 되면 진정으로 깊이 있는 C 개발자가 못 되듯이 자바스크립트에서도 Closure를 이해하지 못하면 깊이 있는 자바스크립트 개발자, 또는 웹 개발자가 되지 못하게 되는 것이다. 하지만 중요한 것은 "Closure는 뛰어난 기술이 아니다"라는 것이다. 포인터의 개념을 주소라는 개념으로 받아들이기 시작하면 아주 쉽게 이해하듯이 closure 또한 scope chain에서 하나의 scope를 생성해준다는 개념으로 이해한다면 아주 쉽게 이해가 가능할 것이다. 그렇다면 먼저 scope chain에 대해서 알아봐야할 것이다.
* Scope chain
: Scope chain은 이전 글에서 이미 다뤘던 내용이다. 하지만 이 개념과 closure를 반드시 연결해서 생각해야만 closure를 이해할 수 있다. Scope chain에 대해서 설명을 다시 하겠지만, 이전 글을 다시 훑어보고 와도 괜찮을 것이다.
<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에 대해서 더 깊어지기 전에 위의 현상에 대해서 이해하고 넘어가자. 위의 예에서 '익명 함수'를 이용하고 있다. 익명 함수는 함수의 이름을 지정 안하고 사용하는 경우를 뜻하고 위의 예에서는 다음의 부분이다.
: 이 부분에서 #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는 functional language에서 이전 글들에서 closure가 사용되었던 예들을 다시 한번 살펴보자. 아래의 소스들은 지금까지 공부해왔던 내용 중에 나왔던 소스들이다. 앞 뒤의 소스들은 간단하게 생략한 것들도 있다.
function setDivClick(index) {
document.getElementById("divScope" + index).addEventListener("click",
function () { // #1
alert("You clicked div #" + index);
}, false);
}
for (i = 0; i < len; i++) {
document.getElementById("divScope" + i).addEventListener("click", (function (index) { // #1
return function () { // #2
alert("You clicked div #" + index);
};
}(i)), false); //#3
}
function bind(obj, func) {
return function () {
func.apply(obj, arguments);
};
}
document.getElementById("clickDiv3").addEventListener("click", bind(unikys, unikys.say));
: 위의 각 소스들은 지금까지 closure를 이용해왔던 예들이다. 위의 소스들의 공통점이 무엇인지 잘 살펴보면 closure는 어떻게 생성되는지 쉽게 이해할 수 있을 것이다. 그냥 훑어가며 봤을 때에는 약간 어려울지도 모르겠지만, 첫번째 특징은 closure는 function 안에 function이 선언될 때 생성된다는 것을 알 수 있다. 두번째 특징은 바로 function에 선언된 scope가 아닌 다른 scope에서 호출 될 때이다. 이는 비동기적으로 활용될수도 있고, function을 return 해서 사용할 때에도 적용이 가능한 것이고, 위의 이벤트 핸들러에서 활용하고 있는 것이 가장 대표적인 예이다. 그럼 먼저 closure의 특징을 살펴보자.
* Closure의 특징
: Closure가 나타나는 가장 기본적인 환경은 바로 함수 안에 함수가 다시 선언되어 호출되었을 때이다. 이는 가장 기본적인 예로 inner function과 outer function을 통해서 나타낼수 있다.
function outer () {
var count = 0; // #1
var inner = function () { // #2
return ++count;
};
return inner; // #3
}
var increase = outer(); // #4
increase(); // === 1 #5
increase(); // === 2
: 여기서 짧게 설명을 한다면 #1 count 변수는 outer의 local 변수이고 #2에서 outer의 로컬 함수로 inner 함수가 선언이 된다. 이때에 #2 안에서는 outer 함수의 count 변수가 접근 가능하다. 이러한 상태에서 inner 함수는 #3에서 outer 함수의 결과로 리턴이 된다. 이 때 #4에서 outer 함수가 호출이 되고, #3에서 리턴된 inner가 #4의 increase 변수에 저장이 되고 #5에서 리턴된 inner 함수가 호출이 되는 것이 순서다. 여기서 중요한 것은 바로 inner 함수가 리턴되면서 다른 곳에서 호출이 될 때에도 inner 함수가 선언되었던 당시에 접근 가능했던 변수 count가 계속 접근이 가능하다는 것이다. 그리고 scope의 개념으로 볼 때에 #4, #5 등 outer 함수 외부에서는 outer 함수의 local 변수인 #1의 count에 접근할 방법이 없게 된다. 즉, 자바스크립트에서도 일반적인 객체지향에서 말하는 private 개념이 적용이 가능한 것이다. 이것이 closure의 가장 기본적인 특징이고 개념이 되는 것이다. 조금 다른 예제를 살펴보자. 이번 예제는 라이브러리를 이용한다면 아주 자주 이용할 패턴이다.
function outer () {
var count = 0;
return { // #1
increase: function () {
return ++count;
},
decrease: function () {
return --count;
}
};
}
var counter = outer(); // #2
counter.increase(); // === 1
counter.increase(); // === 2
counter.decrease(); // === 1
var counter2 = outer();
counter.increase(); // === ? #3
: 이번에도 매우 비슷하다. 하지만 이번에는 이전에 함수를 바로 리턴하던것과는 다르게 #1에서 object 리터럴 {}를 이용해서 함수 2개를 묶어서 리턴하게 했다. #2에서 outer()함수를 호출 할 때에는 #1에서 선언한 object가 리턴이 되어 counter에 들어가게 된다. 이러한 경우에도 똑같이 closure가 생성되고, 이번에는 #1의 object 안에 있는 두개의 함수가 동일한 scope를 보게 되어 같은 작업을 할 수 있게 되었다. 그렇다면 outer() 함수를 한번 더 호출하게 되면 어떻게 될까? 개발자 콘솔에서 직접 실행해보면 아래와 같은 결과가 나온다.
: counter와 counter2는 서로 다른 scope를 생성하여 따로따로 저장하게 된다. 그렇다면 리턴하는 모든 함수들이 같은 값을 사용하도록 static하게 만드는 방법이 있을까?
* 즉시 호출 함수 (immediate function)
: 즉시 호출 함수라 하면 자바스크립트가 익숙하지 않은 사람은 가장 어색하고 당황할 함수 호출 방식이다. 하지만 이미 이전 글들에서도 종종 나왔었다. 맨 위에서 2번째 소스의 예가 바로 즉시 호출 함수의 예이다. 이 즉시 호출 함수를 이용하면 모든 함수들이 공통으로 사용하는 변수를 만들수 있게 된다.
var countFactory = (function () { // #1
var staticCount = 0; // #2
return function factory () { // #3
var localCount = 0; // #4
return { // #5
increase: function () {
return {
static: ++staticCount,
local: ++localCount
};
},
decrease: function () {
return {
static: --staticCount,
local: --localCount
};
}
};
};
}());
var counter = countFactory(), counter2 = countFactory();
counter.increase();
counter.increase();
counter2.decrease();
counter.increase();
: 그냥 보면 무언가 복잡해진것 같지만, 아주 쉽다. 그냥 위의 예를 하나의 함수로 더 덧씌우면서 closure 하나를 더 생성한 것이다. 즉, #1에서 closure를 생성하는 즉시 호출 함수를 선언하고, #2에서는 static으로 활용할 변수를 선언하고, #3 에서는 즉히 호출 함수에서 리턴 값으로 사용할 함수, 위의 예제에서 사용했던 함수를 리턴하게 되는 것이다. 즉, #1의 리턴 값은 #3이 되어 countFactory 변수에는 #3의 함수가 들어가게 된다. 나머지 #4와 #5는 위의 예제와 똑같고, 단지 local과 static의 차이를 나타내기 위하여 리턴 값에 그 두가지를 묶어서 리턴하도록 했다. 위의 실행 결과는 아래와 같이 나온다.
: 이렇게 즉시 호출 함수를 선언함으로써 closure를 하나 바로 생성하는 방법은 다양한 곳에서 활용될 수 있고, 이것은 기존의 웹 개발과는 확연하게 다른 자바스크립트만의 새로운 개발 방법론으로 자리 잡고 있으므로 반드시 이해하고 넘어가길 바란다. 이제 눈치가 좀 빠른 사람이라면 이렇게 closure를 사용하는 가장 간단한 방법이 바로 함수를 return하면서 사용하는 것이라는 것도 알게 되었을 것이다.
* 라이브러리에서의 활용
: 위와 같이 함수로 한번 둘러싸는 경우 가장 많이 사용하는 것이 바로 라이브러리일 것이다. 라이브러리일 때 뿐만아니라 사용자의 접근을 제한하고, 변수의 조작을 불가능하게 하기 위해서는 필수로 위와 같은 방법을 사용해야한다. 하지만 이것보다도 더 큰 이유는 바로 다른 라이브러리들과 함께 사용되는 경우 서로간 충돌을 없애기 위해 반드시 해야한다. 전역변수를 사용했다가 다른 라이브러리 가져왔는데 그 라이브러리에서 덮어씌워버린다면 이유도 모르고 멀쩡하던 웹 페이지에서 에러가 발생하게 될 것이다.
: 아주 간단한 예를 들어보면, 모두가 많이 사용하는 var xmlhttp를 전역변수로 사용했는데, 잘 못 만든 라이브러리 하나를 가져왔더니 거기서 XMLHttpRequest를 워낙에 빈번하게 사용하다보니 전역변수로 선언해서 사용하고 있는데, 그게 하필 같은 xmlhttp를 전역변수로 사용하기라도 한다면 이전에 선언되고 호출 되었던 부분들이 덮어씌워져 버릴 것이다. 그래서 라이브러리를 만들때에도, 활용할 때에도 위와 같이 자신의 중요한 변수들은 즉시 호출 함수로 감싸서 보호를 해주는 것이 마땅하고 전역변수로부터의 접근은 네임스페이스를 활용함으로써 그 가능성을 최소화 시키는 것이 필요하다. 때로는 전역변수의 사용이 불가피하다고 느낄때가 있겠지만, 거의 모든 상황에서 전역변수의 사용은 회피할 수 있다. 그 이유는 바로 closure로 인해 function을 인자로 넘겨줄 경우 function이 참조하고 있는 scope째로 왔다갔다 하게 되기 때문에, 이쪽의 scope와 다른 곳에서의 scope를 함수를 전달 시킴으로써 공유하게 만드는 것이다.
* Closure가 발생하는 또 다른 경우
: 위처럼 return을 통해서도 closure가 생성되기도 하지만 이외에도 많은 방법으로 생성할 수 있다. 위의 return으로 생성하는 방법은 자바스크립트에 매우 친숙한 사람이라면 다 해봤겠지만, 일반 프로그래머들도 많이 해봤을 경우가 있다. 하지만 아마 본인도 closure가 생성된다는 것 자체도 모르고 활용할 방법도 이해 못하고 있었을 확률이 높다. 첫번째 예는 이 글 맨 위의 첫 예이다.
function setDivClick(index) {
document.getElementById("divScope" + index).addEventListener("click",
function () { // #1
alert("You clicked div #" + index);
}, false);
}
: 일단 위의 Closure의 특징에서 말한 특징을 찾으면 같은 구조로 함수 안에 함수가 존재하는 것을 발견할 수 있다. 하지만 위의 특징에서와는 다르게 return을 하고 있지는 않다. 아래의 예도 closure가 존재한다는 것을 모르고 자주 사용했을 예이다. 이전에 팁으로 썼던 글 중에서 setInterval에 대한 글 중의 소스이다.
<script>
var pendingInterval = false;
function setPending() {
if (!pendingInterval) {
pendingInterval = setInterval(startPending, 500);
} else {
clearInterval(pendingInterval);
pendingInterval = false;
}
}
function startPending() {
var div = document.getElementById("pending");
if (div.innerHTML.length > 12) {
div.innerHTML = "Pending";
}
div.innerHTML += ".";
}
</script>
<button onclick="javascript:setPending();">Toggle Pending</button>
<div id="pending">Pending</div>
: 위의 소스를 보면 setInterval(startPending)을 하고 있다. 이것은 실제적으로 startPending의 scope를 그대로 옮겨와서 closure를 하나 생성하게 된다. 하지만 전역벽수를 이용하고 있기 때문에 만약 사용자가 pendingInterval = true;를 시켜버리면 라이브러리의 동작을 수정하여 어떠한 일이 일어날지 예측하지 못할지도 모른다. 위와 같이 간단한 예에서는 미치는 영향이 적을지 모르지만 고객 정보를 다루거나 응모 이벤트 같은 곳에서 잘못 짜여진 웹페이지를 사용자가 건드리는건 아주 식은죽 먹기이다. 따라서 위의 라이브러리에서의 closure 활용처럼 오류를 방지하도록 수정해보고 덤으로 한번 퍼포먼스를 향상시켜보자.
<button onclick="javascript:setPending2();">Toggle Pending</button>
<div id="pending2">Pending2</div>
<script>
var setPending2 = (function () {
var pendingInterval = false, div = document.getElementById("pending2"); // #1
function startPending() { // #2
if (div.innerHTML.length > 13) {
div.innerHTML = "Pending2";
}
div.innerHTML += ".";
};
return function () { // #3
if (!pendingInterval) {
pendingInterval = setInterval(startPending, 500);
} else {
clearInterval(pendingInterval);
pendingInterval = false;
}
};
}());
</script>
Pending2
: Closure를 이용하는 아주 간단한 예이고 라이브러리의 기본 형태와 비슷하다. setPending2 변수에는 #3에 선언된 함수가 리턴되면서 설정하게 되고, #1과 #2에 있는 변수와 함수는 closure 내부에서만 접근할 수 있는 private 변수와 함수 같이 된 것이다. 여기서 퍼포먼스를 향상 시켰다는 말은 어디로부터 올 수 있었는지 살펴보면, 바로 매번 startPending 함수가 호출될때마다 div = getElementById()를 하던 것을 한번만 하도록 closure에 변수로 저장해둔 것이다. 이것은 전역변수가 아니라서 전역변수를 싫어하는 개발자들의 마음을 아프게하지도 않는다. 이전까지 전역변수가 난무하던 자바스크립트의 개발 방법이 closure에 대한 이해가 늘어나면서 이러한 식으로 encapsulate하고 자기의 변수를 보호하는 방식의 개발 방법론으로 개선되고 있는 것이다. 여기서 중요한 것은 closure를 이용했기 때문에 전역변수를 이용하지 않고도 위의 setPending2 함수는 인자로 보내든, 다른 라이브러리에서 사용하든 어디서든 똑같은 동작을 하게 되는 것이다. 하지만 위의 구현 방법이 만능은 아니다. 아래와 같은 단점들을 대표적으로 꼽을 수 있다.
즉시 호출 함수는 소스가 로드되면 바로 실행을 하게 되므로 소스의 로딩시간이 길어진다.
소스가 바로 실행되므로 html 소스보다 아래에 있어야한다.
: 이러한 단점들은 물론 극복 가능하거나 다른 방법으로 고민을 해볼 수 있다. 일단 두 번째 단점 때문에 대표적인 해결 방법이 즉시 호출 함수를 window.onload 이벤트에 넣는 방법이 있다. 이렇게 되면 html 소스 등 웹페이지의 로딩이 다 끝나고난 뒤에 함수를 호출하게 되므로 소스가 어디에 있든 상관없게 된다. 이렇게 즉시 호출 함수가 많아짐에 따라 window.onload의 활용은 이전보다 확연하게 많아진 것이다. 첫 번째 단점은 여러 모로 조금 고민을 해봐야한다. 다음 중에서 가장 중요한 것을 고민해볼 수 있을 것이다.
페이지의 첫 로딩시간은 조금 느리지만 사용자의 지속적인 인터렉션 반응 속도의 단축
사용자의 첫 클릭에서의 반응 속도는 느리지만 지속적인 반응 속도 단축
조금 느리지만 사용자의 꾸준하게 동일한 반응 속도
: 위의 3가지 중에서 첫번째는 위의 예처럼 즉시 호출 함수를 이용하는 방법이다. 처음에 로딩하면서 div를 로딩해두는 것이다. 그 다음 3번째는 맨 처음 구현했던 방법이다. 매번 div를 DOM 트리에서 가져오기 때문에 지속적으로 조금은 느린 사용자 반응속도가 일어나게 되는 것이다. 그럼 2번째는 어떠한 경우인지 살펴보자.
* 덧
: 자바스크립트의 퍼포먼스를 저해하는 가장 큰 요소는 'DOM을 탐색하여 접근'하는 것이다. getElementById이든 jquery의 $이든 상관없다. 따라서 위처럼 한번 접근하고 나서 다시 접근을 자주 할 것 같을 때 변수에 저장해두고 접근을 하는 것이 퍼포먼스를 향상 시킬 수 있는 큰 방법이다.
* 자기를 덮어쓰는 함수 (self-defining function)
: 이 방법은 사용자들에게나 라이브러리를 이용하는 개발자들에게는 전혀 나타나지 않고 다른 점을 못느끼겠지만 이 자체를 개발하는 개발자라면 매우 뿌듯함(?)을 느낄 수 있는, 만약에 이전에 이러한 설계를 한번도 보지 못했다면 아주 기가막힌 설계 디자인이다. 간단하게 요약하자면 바로 초기화를 호출 단계에서 하고 자기 자신을 그 초기화된 정보들을 포함하는 closure가 있는 함수로 덮어씌우는 것이다. 위의 예를 다시 한번 사용해보자.
<button onclick="javascript:setPending3();">Toggle Pending</button>
<div id="pending3">Pending3</div>
<script>
var setPending3 = function () {
var pendingInterval = false, div = document.getElementById("pending3"); // #1
function startPending() { // #2
if (div.innerHTML.length > 13) {
div.innerHTML = "Pending";
}
div.innerHTML += ".";
};
setPending3 = function () { // #3
if (!pendingInterval) {
pendingInterval = setInterval(startPending, 500);
} else {
clearInterval(pendingInterval);
pendingInterval = false;
}
};
setPending3();
};
</script>
Pending3
: 이번은 2번째 예와 비슷하지만 다른 점이 있다면 setPending3는 처음에 호출되었던 함수를 그 안에서 다시 덮어씌워서 다른 함수 #3으로 만들어버린다는 것이다. #3에서는 맨 처음의 setPending3 함수의 #1과 #2에서 가지고 있었던 변수와 함수들에 대하여 접근할 수 있는 closure가 생성되어 유지된다. 다른 점이 있다면 2번째는 소스가 로딩됨과 동시에 호출이 되어 초기화 작업이 이루어졌다면, 이번에는 함수 호출이 일어날 때, 즉 사용자가 처음으로 버튼을 클릭해서 처음으로 호출이 될 때 초기화를 하게 된다. 이게 뭐가 좋은지 잘 모를수도 있지만, 매번 호출될 때마다 초기화가 되었는지 if-else를 해볼 필요없이 그냥 함수 자체를 다시 선언해주는 것이라고 생각하면 이후에 호출될 때마다 성능상으로 충분한 메리트가 있다고 생각할 수 있다. 이렇게 사용자가 처음에 클릭했을 때 초기화하는 것은 아주 사소한 차이지만 프로그램이 커졌을 경우나 DOM을 자바스크립트로 대량으로 다루게 될 때에는 UX가 초기화가 로딩 때 일어나느냐 처음으로 눌렀을 때냐 등의 차이에 따라 꽤나 크게 다가올지도 모른다. 즉, 서로 다른 기능의 특징에 따라 서로 다른 초기화 방법을 사용하면 된다. 이것은 각자 생각하는 가치관에 따라 다르기 때문에 대충 한번 간단하게 어떻게 구현하느냐에 따른 기준을 적어보자면 아래와 같다.
사용자가 페이지에 접속하자마자 자주 사용하는 기능, 이 페이지에 들어와서 반드시 한번은 사용하게 되는 기능이라면 로드하면서 초기화
사용자가 페이지에 들어와서 한참 후에 사용하겠지만 한번 쓰고나서 자주(혹은 이따금씩) 이용하게 되는 기능은 처음 호출 때 초기화
사용자가 기능을 건드리지 않고 나갈 가능성이 크고 자주 이용하지도 않는 기능은 초기화 단계 없이 그냥 그때그때 사용
: 이것이 자바스크립트 초기화에 대한 기본 이해가 될 것이다. 그렇다면 이번에는 closure를 응용하는 한가지 예를 살펴보자.
* 오버로딩
: 객체 지향 개발자라면 아주 반가운 단어일 것이다. 자바스크립트에서는 유동적으로 인자의 수를 받아들이기 때문에사실 오버로딩을 지원하지 않는다. 하지만 arguments와 closure를 이용한다면 이러한 오버로딩 개념도 나름 구현할 수 있게 된다. 간단하게 작성해보면 아래와 같다.
function overload(object, functionName, fn, fnMap) {
var previousFunction = object[functionName];
object[functionName] = function () {
if (fn.length === arguments.length) {
if (fnMap && fnMap.length === arguments.length) {
for (var i = 0; i < arguments.length; i++) {
if (fnMap[i] === typeof arguments[i]) {
return previousFunction.apply(this, arguments);
}
}
return fn.apply(this, arguments);
}
return previousFunction.apply(this, arguments);
} else if (typeof previousFunction === "function") {
return previousFunction.apply(this, arguments);
};
};
}
: 복잡한듯 하지만 함수를 호출하게 되면 객체에 저장되어있는 함수를 previousFunction으로 closure에 저장해두고, 인자의 갯수와 인자의 형을 비교해서 모든게 일치하다면 인자로 넘어왔던 fn을 호출하고, 아니라면 이전에 설정되었던 다른 함수 previousFunction을 호출하게 되는 것이다. 간단한 활용 예를 들면 아래와 같다.
var MyFile = {};
overload(MyFile.prototype, init, function () {
console.log("init as empty file");
});
overload(MyFile.prototype, init, function (fileName) {
console.log("init with file name");
}, ['string']);
overload(MyFile.prototype, init, function (file) {
console.log("init with file object");
}, ['object']);
var file = new MyFile();
file.init();
file.init("myfile.txt");
file.init(fileObject);
: 이러한 활용 예를 들 수 있겠다. 위의 예는 아주 간단하게 오버로딩을 구현한 기법이고, 에러 체크라던가 기본 호출 함수 설정 등 구미에 맞게 바꿔서 구현하면 될 것이다.
* closure 단점
: 이렇게 편리한 closure라도 만능은 아니다. 엄연히 단점이 있기 때문에 항상 사용하기 보다는 정말로 필요할 때, 구현에 있어서 급진적으로 개발이 편해질 때 사용하면 좋을 것이다. 이러한 closure의 단점은 크게 2가지이라고 볼 수 있고 부수적인 단점이 한가지 더 있다. 일단 큰 단점 2개는 다음과 같다.
메모리를 소모한다.
Scope 생성에 따른 퍼포먼스 손해가 있다.
: 이들 2가지는 어떻게 극복할수 없는 단점들이다. Closure를 정말로 필요한 곳에 요긴하게 사용하는 수 밖에 없다. 특히, 메모리의 소모는 리턴하거나 timer, 콜백 등으로 등록했던 함수들이 메모리에 계속 남아있게 되면 해당하는 closure도 같이 메모리에 계속 남아있게 되는 것이기 때문에, 지속적으로 루프를 돌면서 closure 생성하는 것은 지양해야할 설계가 될지도 모른다. 최신 버전에서는 해결되었지만, 구 버전의 IE 같은 경우에는 DOM의 콜백함수로 등록을 하고 콜백함수의 해제 없이 바로 DOM을 삭제해버리면 메모리 누수가 생기는 단점도 있었던 점만 봐도 closure의 메모리 누수와 누적에 대한 고민을 해야한다는 것을 깨달을 수 있다. Closure는 또한 하나의 새로운 Scope를 생성하여 내부의 함수에 링크를 시키기 때문에 이에 따른 퍼포먼스 손해도 감수해야한다. 잦은 함수 호출이 퍼포먼스상 안 좋듯 만약 굳이 함수나 closure를 사용하지 않아도 되는 간단한 일이라면 굳이 함수로 분류를 하지 않아도 될 것이다.
: 그렇다면 위의 핵심적인 단점들 이외의 부수적인 단점은 무엇일까? 다른 언어의 개발 경험이 많았던 사람이라면 조금은 느꼈을지도 모른다. 바로 이해하기가 어렵다는 것이다. Closure는 개발자 본인이 사용할 때에는 나비처럼 날고 벌처럼 쏘는 핵심 기능으로 자라나지만 다른 사람들이 보면 이것이 무엇인지, 어디서 closure가 생성되었고 거기에는 어떠한 정보가 있는지 불분명하게 되는 경우가 많기 때문에 협업을 하게 될 때에는 명확한 주석과 문서화가 필요로 있어야할 것이다. 그리고 무엇보다도 다른 언어 개발자들은 closure가 돌아가는 방식을 이해하지 못하는 경우가 많기 때문에 그들에게 설명을 해줘야할 시간을 투자해야 한다는 점과 이 개념을 같은 웹개발자라도 제대로 이해하고 있지 않는 사람이 정말 많다는 것에 놀라 마음의 충격을 받는다는 점이다.
* 단점에도 불구하고..
: 이러한 단점들은 정말 어떻게 다른 방법으로 극복이 불가능한 단점들이다. 하지만 자바스크립트에서 closure를 빼면 그것은 진정한 자바스크립트가 아니다. 정말로 단순한 '스크립트 언어'에 머물던 5년전 closure를 배제한 개발 방식이야말로 자바스크립트에 잠재되어있는 무한한 가능성을 없애는, 정말로 너무나 놀라운 언어를 그냥 '스트립트언어'로, 그냥 보조적인 언어로 만들어 버리는 것이다. 위에서 단점을 쓴 것은 단점이 많아서라던가 치명적이어서가 아니라 장점이 훨씬 더 많지만 적어도 어떠한 단점들이 있는지 알고 사용해야 더욱더 잘 사용할 수 있기 때문이다. 이 closure를 마음껏 쓰다가 다른 언어를 사용하게 되면 closure가 없다는 것이 엄청 아쉬울 때가 많아질 만큼 closure는 자바스크립트의 핵심이자 특징이라고 볼 수 있다.
* 정리
- Closure는 function 안에 function이 있을 때 생성된다.
- Closure는 함수가 정의된 scope 이외의 곳에서 사용될 때 private 저장소처럼 활용 가능하다. 이러한 경우가 발생하는 대표적인 경우는 아래와 같다.
내부의 function을 리턴하여 다른 곳에서 사용 (바로 호출하던지 인자로 넘겨주는 등)
setTimeout, setInterval 등과 같이 비동기적으로 호출 되는 경우
이벤트 콜백 함수로 활용 되는 경우 (addEventListener 라던가, xmlhttprequest.onreadystatechange 등)
* C나 자바를 접하던 사람들이 처음으로 자바스크립트를 접하면 혼란스러워하는 것이 바로 scope와 this의 상이함일 것이다. 처음에 접할 때에는 객체지향 언어에서는 이해할 수 없는 동작들을 하고 있기 때문에 이것이 뭔가 싶다가도 자바스크립트가 이상하다고 스스로 판정을 내리게 된다. 하지만 이것들은 자바스크립트의 원리만 이해하면 아주아주 쉽고, 오히려 객체지향 언어보다 놀라운 유연함에 감탄을 하게 될 것이고, 자바스크립트를 하다가 다시 C나 자바를 하게 되면, '자바스크립트라면 쉽게 해결할 수 있는데..'라며 자바스크립트를 아쉬워하게 될 것이다. 그럼 이번에는 일단 자바스크립트의 가장 '기본'인 scope와 closure에 대해서 알아보자.
: Scope라 하면, 현재 접근 가능한 변수들이 결정되는 방법이다. 영어의 뜻을 가져와서 설명해보면, 현재 자신의 위치에서 볼 수 있는 변수들을 결정하는 방법인 것이다. 자신의 scope 안에 있다면 접근이 가능하여 변수를 읽거나 쓸 수 있는 것이고, scope 밖에라면 해당하는 변수는 접근이 불가능한 것이다. 간단하게 생각한다면 너무나 쉬운 내용이지만 왜 기존의 '능수능란한' 프로그래머들도 쉽게 이해하지 못할 함정에 빠지게 되는 것일까?
: 아래는 정확하게 Scope 때문에 일어난 일은 아니지만, C와 자바의 프로그래머들이 어떻게 해결해야할지 가장 이해하지 못하는 상황이다. <div>가 0,1,2가 있고, 각 div에 클릭 이벤트를 넣어서 0,1,2를 출력하는 아주 간단한 프로그램을 짜고자 한다. div를 쉽게 늘릴 수 있게 해주기 위해 능숙하게 for loop을 이용해서 이벤트를 부여해줘야겠다고 생각한다. 그래서 아래와 같이 짰다.
<div id="div0">Click me! DIV 0</div>
<div id="div1">Click me! DIV 1</div>
<div id="div2">Click me! DIV 2</div>
<script>
var i, len = 3;
for (i = 0; i < len; i++) { //#1
document.getElementById("div" + i).addEventListener("click", function () { //#2
alert("You clicked div #" + i); //#3
}, false);
}
</script>
: 그 구현 결과는 아래와 같다. DIV 0, DIV 1, DIV 2를 눌러보자.
Click me! DIV 0
Click me! DIV 1
Click me! DIV 2
: 위와 같이 능숙하게 짰건만 구현하고나서 위의 DIV들을 각각 눌러보면 이상하게 "You clicked div #3"만 나온다. 왜 이렇게 나오는 것일까? 문제는, scope가 어떻게 결정되느냐가 아니라, scope가 생성되는 방법과 scope가 유지되는 방법인 것이다. click 이벤트에 대하여 콜백 함수를 작성하여 alert를 시켜주는 함수의 scope는 콜백함수로 선언될 때 #2에서 생성되며, 그 scope는 #1의 변수들에 대하여 접근 가능하기 때문에 해당하는 scope를 유지하게 된다. 나중에 div위에 click 이벤트가 발생해서 실제로 #3이 호출 될때 i는 여전히 #1에 대한 scope가 그대로 살아있어서 클릭된 시점의 i 값을 가져오게 된다. 간단하게 그림으로 설명하면 아래와 같다.
: 위의 for루프를 돌 때에는 scope가 생성되지 않고 i는 기본적으로 global scope에 존재하게 되고, addEventListener에 함수를 첨부할 때 익명 함수가 선언이 될 때 scope가 생성되어 참고를 하게 된다. 그리고 2번째, 3번째 루프를 돌면서 div2의 클릭 이벤트의 콜백함수를 설정하고 나면 scope는 아래와 같이 된다.
: 각 div의 click 이벤트에 부착되었던 콜백 함수들을 모두다 같은 scope의 변수 i를 보게 되었고, 3번째 루프를 다 돌고나서 마지막에 i++이 되고나면 최종적으로 위의 함수들은 공통적으로 global scope에 있는 i=3의 i를 보게 되는 것이다. 그리고 나중에 클릭 이벤트가 일어나서 클릭을 하게 된다면 모두다 똑같이 "You clicked div #3"을 출력하게 된다.
: 이러한 현상은 자바스크립트에서 scope가 함수로 인해 생성되고 함수가 호출될 때에도 계속 지속되는 특성에 의해 생긴 문제이다. 자바스크립트에서 이는 자주 발생하는 문제로 이 개념만 쉽게 이해하고 있다면 기본은 끝냈다고 생각해도 된다. 그럼 위의 문제를 해결하기 위해 먼저 scope가 생성되는 방법에 대해 살펴보자.
* scope의 생성
: scope의 생성은 특정 구문이 실행될 때나 객체들이 생성될 때 새롭게 scope가 하나 생성하게 된다. 그 구문들은 다음과 같다.
function
with
catch
: 이들이 scope를 생성하게 되는 방법은 각각 다르지만, 중요한 것은 이런 객체들이 생성될 때에만 scope가 생성되고 {} 의 블럭이 생성된다고해서 scope이 하나 생성되는 것이 아니다. for 루프를 적용해보면 바로 차이를 느낄 수 있을 것이다. 아래의 소스코드는 실생활에서 사용할 일이 없지만 scope를 이해하는데 약간은 도움이 될 것이다.
* 목적: 수를 0~9까지 더하다가 총합이 16를 넘는 숫자를 구하고 싶다.
for(var i = 0; i < 10; i++) {
var total = (total || 0) + i; // #1
var last = i; // #2
if (total > 16) {
break;
}
}
alert(total + " , " + last); // #3
: 위의 소스코드에서 #1은 자바스크립트에서 자주 사용하는 표현에 한정됐지만, #2의 last는 C나 자바에서도 자주 사용하는 표현일 것이다. 특정 객체의 index를 찾는 등을 할 때 자주 이용하는데 for 루프 안에 var last로 선언되어있다. C나 자바에서라면 #3에서 당연히 에러가 나야겠지만, 자바스크립트에서는 잘 돌아간다. 이것은 블럭{}을 사용할 때에는 scope이 생성되지 않는다는 뜻이다. 반면 function의 안에 있는 값들은 접근이 불가능하다.
function foo() {
var b = "access me";
}
typeof b === 'undefined';
: 이렇게 scope가 생기는 것은 다른 언어와 같기 때문에 당연한 것이다. 하지만 with와 catch는 조금 다르다. function은 블럭{} 안에 있는 모든 내용이 새로운 내부 scope에 포함되지만, with와 catch는 괄호() 안에 인자로 받는 변수들만 새로운 내부 scope에 포함되어 이후에 따르는 블럭{}에서만 접근이 가능하다.
try {
throw new exception("fake exception");
} catch (err) {
var test = "can you see me";
console.log(err instanceof ReferenceError === true);
}
console.log(test === "can you see me");
console.log(typeof err === 'undefined');
: 위와 같은 경우 외부에서 test는 일반 블럭처럼 접근이 가능하지만 catch의 인자로 들어온 err에는 접근을 할 수가 없다. with도 마찬가지이다.
with ({test: "You can't see me"}) {
var notScope = "but you can see me";
console.log("Inside: " + (test === "You can't see me"));
}
console.log(typeof test === 'undefined');
console.log(notScope === "but you can see me");
: function과는 또 다른 독특한 scope를 가지고 있다. 그렇다면 이것만 가지고 위의 클릭 이벤트에서 일어나는 오류를 해결할 수 있을까?
: 잠시 다른 이야기지만, 자바스크립트를 오래한 사람들은 머리에 수없이 박히게 들어왔을, 왠만해서는 정말 어쩔 수 없는 경우가 아니라면사용하지 말아야할 2가지 함수가 있다.
eval
with
: 자바스크립트의 활성화에 가장 큰 기여를 한 Douglas Crockford가 말한 명언이 있다.
"eval is evil"
: 이미 eval을 사용하는 것은 JSON.parse() 기능이 나오고난 이후에 퍼포먼스상, 보안상 단점만 있는 기능이 되어버렸다. 그리고 그와 동급으로 with는 처음부터 없는 듯이 사는 것이 좋을 때도 있다고 언급하고 있다. 이미 자바스크립트를 많이 해왔던 사람들 중에서도 with가 무엇을 하는 구문인지 모르는 사람들도 많겠지만 위의 클릭 이벤트 문제에서는 이렇게 천대받던 with도 뭔가 한가지 역할을 찾을 수 있게 된다. 만약 with를 쓴다면 이것이 with문을 거의 유일하게 효율적으로 이용할 수 있는 방법이 아닐까 싶다.
<div id="divWith0">Click me! DIV 0</div>
<div id="divWith1">Click me! DIV 1</div>
<div id="divWith2">Click me! DIV 2</div>
<script>
var i, len = 3;
for (i = 0; i < len; i++) {
with ({num: i}) {
document.getElementById("divWith" + num).addEventListener("click", function () {
alert("You clicked div #" + num);
}, false);
}
}
</script>
: 위의 구현 결과는 아래와 같다.
Click me! DIV 0
Click me! DIV 1
Click me! DIV 2
: with를 하나 추가해줬을 뿐인데 뭐가 달라진 것일까? 이건 단순히 scope가 하나 생겼다고해서 이해되는 현상이 아니고 자바스크립트의 비동기적인 콜백 함수의 특성과 scope의 지속성이 합쳐진 결과이다. 이렇게 with를 이용해서 해결할 수도 있지만 with는 변수 사용에 있어서 혼란을 가져오고 있기 때문에 그래도 사용을 비추천한다. 다른 해결 방법은 아래에서 설명하고 그래도 with로 문제가 해결은 되었기 때문에 일단 위의 소스에서 with가 어떻게 문제를 해결하는지 살펴보자.
: 먼저 with는 괄호() 안에 새로운 scope를 만들게 된다. 그리고 num의 값에 i의 값을 부여하고 click 이벤트 콜백 함수를 선언한다. 이제 click 이벤트 콜백 함수는 with의 num을 보며 고정된 num의 값 0을 보게 되는 것이다. 이어서 다음 루프에서 scope가 생성되는 것을 다시 살펴보면 아래와 같다. 이전에 div0의 click 이벤트 콜백 함수가 보는 scope는 유지되고, 새롭게 with의 scope가 생성되면서 num을 i의 값인 1로 초기화를 시킨다. div1의 click 이벤트의 콜백함수는 이 scope의 num을 이용하게 되는 것이고, 3번째 루프에서도 똑같은 방식으로 div2의 click 이벤트 콜백도 만들어지게 된다. 결국 오른쪽 그림과 같이 scope가 형성되어 div에서 click 이벤트가 일어나 콜백 함수를 호출 할 때에는 맞게 값이 출력하게 된다.
: 위와 같이 scope가 생성되는 것 뿐만 아니라 함께 scope에서 변수값을 지속시키고 유지 시키는 특성이 이러한 문제를 해결할 수 있게 도와준 것이다. 그럼 scope의 지속성에 대해서 조금 더 공부해보자.
* scope의 지속성
: 사실 scope의 생성되는 방식이 기존의 언어와 가장 다른 점은 아니다. 하지만 scope가 지속되는 것은 다른 언어와는 다른 자바스크립트만의 강점 중 하나이다. 이러한 지속성이 자바스크립트에서 필요한 이유는 바로 자바스크립트에서 새로운 scope가 생성되는 '함수'를 변수에 넣을수도 있고, 다른 함수의 인자로 넘겨줄수도 있으며, 함수의 return 값으로도 활용할 수 있기 때문에 필요했던 개념이다. 즉, 지금 함수가 선언된 곳이 아닌 전혀 다른 곳에서도 함수가 호출될 수 있기 때문에, 그 함수의 scope는 지속될 필요가 있었던 것이다. 그럼 간단하게 그 지속성을 이해하기 위해 위의 클릭 이벤트 문제를 또 다른 방식으로 해결해보겠다.
<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>
: 이번에는 굳이 자바스크립트 언어가 아니더래도 이해하기 쉬울 것이다. 하지만 비동기적으로 호출되는 자바스크립트의 특성에서는 중요하게 생각해야할 개념이다. 일단 위의 구현 결과를 아래에서 살펴보자. 각 DIV를 누르면 누른 번호가 맞게 출력된다.
Click me! DIV 0
Click me! DIV 1
Click me! DIV 2
: 위의 with와는 똑같은 개념으로 scope가 생성되고 지속되기 때문에 그림은 비슷하게 나온다. 하지만 다른점이 있다면, with는 with의 내부에서 with의 특성을 따라가게 되고 scope가 완전히 분리된것이 아닌 global scope와 반쯤 섞여있는 형태를 취하고 있는 반면, function으로 구현한 경우 정말로 별도의 scope를 생성하게 된다.
: 소스상 with를 쓰는 것이 간편하기 때문에 이렇게 간단한 경우에'만' 사용할 것이고, 그 외에 조금이라도 복잡해지거나 다른 사람들과 협업을 한다면 function으로 분리할 것을 추천한다. 왜냐하면 자바스크립트를 자주 사용하는 사람이더래도 with가 들어가게 되면 그 용법에 대해서 다시 한번 고민을 하고 검색까지 해봐야할지도 모르기 때문에 모두가 이해할 수 있는 쉬운 용법이 있으니 협업에서는 그것을 활용하는 것이 맞는 것이다. 하지만 위와 같이 함수를 따로 뽑아내는것도 귀찮고 일이다. 그럼 이것을 위의 with처럼 간단하게 처리하고 싶을 때에는 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>
var i, len = 3;
for (i = 0; i < len; i++) {
document.getElementById("divScope" + i).addEventListener("click", (function (index) { // #1
return function () { // #2
alert("You clicked div #" + index);
};
}(i)), false); //#3
}
</script>
: 위의 소스가 이해된다면 자바스크립트를 어느 정도 봐온 사람일 것이다. 일단 with랑 비슷하게 생각하면 되지만, with가 아닌 새로운 function을 통해 새로운 scope를 생성하는 것이다. 이러한 기법은 자바스크립트에서 아주 많이 사용되고 있고, 응용 범위도 아주 넓어 익혀두면 유용한 기법이다. 일단 어떻게 돌아가는 것인지 간단하게 설명을 한다면, #1에 선언된 익명 함수는 인자를 index를 받는것인데, 이 인자의 값은 #3에 있는 (i)의 값이 index로 들어오게 된다. 이는 함수가 변수임을 다시 생각하고 보면 쉽다. 즉 아래와 비슷하게 생각하면 된다.
var func = function (index) { /* 생략 */}; //#4
var returnValue = func(i); //#5
returnValue = (function (index){ /* 생략 */}(i)); //#6
: 위의 #4와 #5를 하나로 합치게 되면 #6이 되는 것이다. 이것을 위의 #1~#3까지 표현한것이라고 보면 된다. 이것에 대한 것은 기본적으로 이해하고 넘어가길 바란다. 자바스크립트 라이브러리의 가장 기본적인 활용 패턴 중 하나이기 때문이고, 이 개념을 이해하고 응용할 줄 아는 것은 자바스크립트 프로그래머의 기본이다.
: 이제 내부에서 #2는 현재 호출된 함수의 return값으로 익명 함수를 하나 또 return한다. 이건 또 무슨 뜻인지 처음에 보면 헷갈리겠지만, 위의 #6번에서 returnValue의 안에 함수가 들어가는 것으로 이해하면 된다. 즉, 익명 함수의 리턴값으로 함수가 반환되고 그 반환된 함수가 addEventListener의 2번째 인자로서 들어가게 되는 것이다. 이 개념 또한 아주 기본적인 개념이니까 이해하고 넘어가길 바란다.
: 이렇게 간단하게 closure를 이용하는 방법이 있는 반면, closure자체는 엄청난 응용이 가능하기 때문에 closure에 대한 내용은 나중에 closure의 활용 방법에 대해서 자세하게 다시 한번 더 다룰 것이다.
* 정리
- scope는 다음의 명령어들을 호출할 때 새로 생성하게 되고, 일반적인 for, switch 등의 블럭{}에 의해서 구분되지 않는다.
function
with
catch
- scope는 비동기 함수가 호출될 때까지 계속해서 지속되어 참고된다. 이를 새로운 scope를 생성함으로써 비동기적으로 호출 될 때의 scope를 조율할 수 있다.
* 덧
: 위에서 언급했지만 여기서 잠깐 다룬 scope와 closure는 자바스크립트에서 가장 기본이 되는 내용이다. 이 내용을 모르고는 자바스크립트를 자바스크립트답게 사용할 수 없기 때문에 위의 간단한 예들이 어떻게 돌아가는 것인지 천천히 이해하고 넘어가는 것이 좋다. 많은 강좌들에서는 문법들을 다루고 closure를 가장 뒤에서 소개하고 있지만, 이 내용은 진정 자바스크립트의 가장 기초가 되는 개념이고 이것을 이해해야 다양한 자바스크립트만의 기법들이 활용될 수 있기 때문에 기본이라고 말한 것이다.
: 그럼 다음에는 this가 결정되는 방법과 function에 대해서 공부해보도록 하자. 자바스크립트를 자바스크립트답게 만드는 것이 바로 이 function이므로 이에 대해서도 가장 먼저, 사실 scope보다도 먼저, 공부를 해야할 필요가 있다.
이전글에서 소개한 function에 대한 글의 연속으로 이번글에서는 function 이 가진 여러가지 특성들에 대하여 알아보려고 한다. 이번 글에서 소개하는 function 의 특성은 Scope, Scope Chain 그리고 arguments 객체 이다. 이들은 Execution Context라는 개념에서 함수객체가 만들어지면서 생성되는 것들로서 이글 이후에 소개될 Execution Context에 대한 이해를 위해 필요로하는 녀석들이다. 그러므로 이번글에서 소개되는 내용을 통해 Scope, Scope Chain, arguments 에 대한 기본적인 이해에 도움이 되었으면 한다.
함수(function)의 특징
함수는 전역변수와 지역변수를 활용할 수 있다.
함수는 자신의 statement 안에서 변수를 선언하여 지역변수로 사용할 수 있다.
이때 선언된 지역변수는 해당 함수안에서만 접근가능한private variable 이 된다. 즉 함수 코드블럭을 벗어나게 되면 해당 변수는 메모리에서 사라지게 된다.
하지만 global 영역에 선언된 전역변수는 자바스크립트 코드 어디에서든 사용할 수 있기에 *함수안에서도 전역변수를 이용할 수 있다 *
var x= 1; // global 영역에 선언된 전역변수functionfoo(){
return x; // 전역변수 x를 return
}console.log(foo());
> 1
console.log(x); // 어디서든 참조가능한 전역변수
> 1
함수에서 전역변수와 지역변수사용함에 있어 재미있는 현상이 하나 있다. 바로 중첩된 이름의 변수 사용이다. 자바스크립트는 global 영역에 선언된 변수와 동일한 이름의 지역변수를 사용할수 있다. 이로 인해 디버깅 과정중에 많은 혼란을 초래하기도 한다.
var x='hello'; // 전역변수 x 에 hello 할당.functionfoo() {var x='world'; // 지역변수 x 에 world할당.return x;
}
console.log(foo());
>world // 함수실행에 따라 지역변수 x에 할당된 world 가 출력된다.
console.log(x);
>hello // 전역변수 x에 할당된 hello 가 출력된다.
전역변수와 지역변수를 동일한 이름으로 사용했을경우 지역변수에서 다른 값을 할당하여도 전역변수에는 아무런 변화가 없다.
즉 전역변수와 지역변수를 동일한 이름으로 사용하여도 각각 다른 변수객체가 만들어진다는 것을 알수 있다. 이 현상에 대해 정확히 이해하기 위해서는 Scope 와 Scope Chain 이란 녀석들에 대해 이해하고 있어야 한다.
함수객체의 Scope & Scope Chain
Scope & Scope Chain
ECMA-262 3 Edition 에서는 함수객체는 [[scope]] 라는 프로퍼티를 가지며 [[scope]]는 함수객체가 생성되는 시점과 관련된 Object Reference 정보를 가지고 있으며 이 Object Reference 정보들을 Scope Chain 이라고 한다고 정의한다. 즉 scope는 함수객체가 접근가능한 Valiable Object의 유효범위이며 이 Valiable Object들의 집함을 Scope Chain 이라고 한다.
함수객체는 생성과정에서 함수구문 내에서 선언된 지역변수들을 Valiable Object 라는 객체에 저장한다. 그리고 전역객체(Global Object)의 Valiable Object 와 연결된다. 전역객체의 Valiable Object는 전역객체 자신이다. 이때 이 Valiable Object들에 대한 연결들을 Scope Chain 으로 관리하고 [[scope]]를 통해 Scope Chain을 참조하여 함수객체가 가지는 유효범위를 설정하게 되는 것이다.
var x = 1;
functionfoo(){
var y = 10;
return a+x;
}console.log(z);
>ReferenceError;
위 예제코드의 foo() 함수객체가 생성될 때의 [[Scope]]와 Scope Chain 에 대한 연결은 아래 다이어그램을 보면 쉽게 이해할수 있다.
변수의 검색은 이 Scope Chain 에서만 탐색 된다. **Scope Chain의 하위에서부터 상위로 등록된 변수가 있는지 찾아가며 가장 처음 탐색되는 scope에 등록된 변수를 이용*한다. 즉 *Scope Chain 에의해 탐색되지 않는 변수는 undefined **이며 그런 이유로 예제의 console.log(z); 는 foo함수가 가진 유효범위에서 찾을 수 없는 변수 z 에 대한 접근을 하기 때문에 ReferenceError 가 되는 것이다. 처음의 의문으로 돌아가서 전역변수 x와 지역변수 x가 동일한 이름으로 사용이 가능한 이유는 이처럼 각각 x에 접근하기 위해 참조하는 Valiable Object가 다르기 때문인 것이다.
3. 변수선언에 사용되는 var 키워드
자바스크립트에서 변수선언은 반드시 var 키워드를 사용하여야 한다는 착각을 하고 있는 입문자들 여럿보았다. 하지만 이는 잘못된 생각이다. var 키워드의 사용은 해당 변수의 scope를 설정하는 역활만을 한다. var 키워드를 사용한 변수선언은 현재 Scope에 등록되며 var 없는 변수 선언은 Global Scopet에 등록되는 것이다. 이런 이유로 함수안에서 var 없는 변수를 사용하면 함수외부에서도 사용가능 하다.
x=1; //전역변수로 등록된다. functionsum() {
y=2; //전역변수로 등록된다.
var z = 3; //var 키워드 사용으로 현재 scope에 등록된다. return x+y+z;
}
console.log(sum());
> 6console.log(x+y);
> 3//x,y 는 전역변수로 등록되기 때문에 정상 수행된다. console.log(x+y+z);
> ReferenceError // z는 함수실행이 종료되면 메모리에서 비워진다.
위 예제를 보면 알수 있듯이 y 변수는 함수내부에 선언되었지만 var 없이 선언 되었기 때문에 전역객체에 등록되는 전역변수가 된다. var 를 사용한 z는 현재 스코프 즉 현재 실행되는 코드 블럭의 주인읜 객체의 Valiable Object 에 등록되기 때문에 함수가 호출되어 실행되고 있는 시점에서의 런타임 주체인 sum 함수의 Valiable Object에 등록되었다가 함수 실행이 종료되면 메모리에서 비워진다.
하지만 이런 코드패턴은 그다지 좋은 방법이 아니다. var 없는 변수는 전역객체에 등록이 됨으로 항시 메모리를 차지하고 있을뿐 아니라 변수에 값이 할당되는 것은 런타임에서 일어나는 일이기에 해당 변수는 함수객체가 생성되기 이전 즉 함수호출되기 이전에는 global scope 에 등록되지 않는다.
또 한가지 var 없는 변수선언을 지양하는 이유는 복잡하고 길어진 코드내에서 var 없이 사용된 변수는 어디에서 어떻게 바뀌는지 추적하기 힘들기 때문이기도 하다. var 없이 선언된 변수는 함수내에서 수정된 값이 전역 scope에 직접적으로 영향을 끼치게 됨으로 본인도 모르는 사이 엄청난 버그를 양산해 낼수 있기 때문이다.
함수는 0개 이상의 매개변수(parameter)를 가질 수 있다.
매개변수(Parameter) 란?
매개변수란 함수호출시 함수객체로 넘겨질 인자값들을 담기 위해 함수명세에서 선언된 변수들을 뜻한다.
함수는 내부에서 선언된 지역변수와 global 영역에 선언된 전역변수를 사용하는 방법말고 변수를 활용하는 한가지 방법이 더 있다. 바로 함수호출시 인자로 전달받은 값을 저장할 변수를 미리 함수명세에 선언해 두는 것이다.
이런 매개변수는 ,(comma)를 기준으로 기분하여 선언하면 한개 이상의 매개변수도 사용할 수 있으며 해당 함수의 scope에 등록되어 함수내부 어디에서든 별다른 선언없이 함수호출시 넘어온 인자값을 할당받아 바로 사용할 수 있다.
위 예제에서 알수 있듯이 명세에 선언된 매개변수보다 많은 매개변수가 전달될 경우 넘어온 순서대로 매개변수로 할당하고 남은 인자값은 버려진다. 재미있는 사실은 넘어온 값이 매개변수보다 적을 경우에 실행 된다는 것이다. 흔히 함수호출시 선언된 매개변수에 대한 인자를 넘기지 않아 생기는 undefined 를 syntax error 라고 생각한다.
하지만 이는 함수객체 생성과정에서 매개변수로 선언된 변수를 Valiable Object에 추가하면서 기본적으로 undefined 를 할당하며 생긴 결과이지 syntax error 가 아니다.
이렇게 함수호출시 전달된 인자값들은 함수객체가 가진 arguments 객체에 저장된다.
arguments 객체
arguments 객체
자바스크립트의 모든 함수객체 scope는 arguments 라는 프로퍼티를 가지고 있다. 이 arguments는 Function에 넘겨진 모든 인자에 대한 정보가 담겨 있는 arguments Object 라는 특별한 객체를 가리키고 있다.
정의에서 말했듯이 함수객체의 Valiable Object의 프로퍼티로 추가되는 arguments는 arguments Object를 가리킨다. 이 arguments Object 는 넘어온 인자의 순서에 따른 인덱스를 가진 유사배열객체이다. 유사배열객체라는 것은 매우 중요한 특성이다.
arguments Object에 저장된 인자값들에 대한 접근은 배열과 동일한 [] 를 사용하지만 **arguments Object는 Array Prototype Object를 상속하는 객체가 아니다.**그러므로 arguments Object 객체에는 push(), pop(), slice()등이 메소드를 가 존재하지 않는다.
functionsum(x,y,z) {
var total =arguments[0] +arguments[1]+arguments[2];
return total;
}
console.log(sum(1,2,3));
> 6
위 코드의 scope에 대한 다이어그램을 보도록 하자
다이어그램에서 보면 알수 있듯 arguments Object는 넘어온 인자값을 저장하며** .length와 .callee 라는 프로퍼티를 가지고 있다.** .callee 프로퍼티는 현재 실행되고 있는 function 을 가리키는 프로퍼티 이다. sum() 함수객체의 .length 프로퍼티와 arguments 객체가 가진 .length 프로퍼티는 서로 다른 의미를 갖는다. 함수객체의 .length 는 함수명세에 선언된 매개변수의 수를 arguments 의 .length는 함수호출시 넘어온 인자값의 수를 나타내는 프로퍼티들이다. 함수객체와 arguments 객체가 가진 .length 프로퍼티를 이용하면 함수명세에 선언된 매개변수와 동일한 수의 인자값이 넘어왔는지 체크할수 있다.
이번글에서 알아본 내용들은 후에 있을 Closure와 Execution Context 에 대한 설명에서 나오는 용어들의 풀이정도라고 생각하면 될것이다.
즉 이번 글에서 해당 용어들에 대한 정의를 제대로 이해하지 못한다면 앞으로 연재될 글을 제대로 이해하지 못할수도 있다. 그럼 언제나 똑같은 멘트이지만 이글을 통해서도 해당 내용들에 대하여 자세히 이해할 수 없다면 당연히 다른 레퍼런스를 참조해서 이들에 대한 정확한 이해를 해야할것이다.
이는 비단 다음연재될 글들뿐만이 아닌 자바스크립트 Core 에 대한 이해에 필수적인 내용이기 때문에 반드시 이해하고 있길 바란다.
프로그래밍 언어를 처음 배울때 가장 먼저 다루는 부분이 바로 그 언어의 데이터타입 이다. 대부분의 기초 프로그래밍 서적들도 첫장에서 데이터 타입에 대해 설명을 하고 있으며 데이터 타입을 숙지 하지 않고서 해당 언어를 다룬다는 것은 "스펠링을 모르면서 작문을 하겠다는 것과 같다" 라고 생각한다. 그런 이유로 자바스크립트 기초의 가장 첫번째 포스틩이 될 이 글에서는 자바스크립트에서 사용되는 데이터 타입에는 어떤 것들이 있으며 그 특징은 무엇인지 다루어 보고자 한다.
자바스크립트에서 사용되는 데이터 타입
자바스크립트에는 Number, String, Boolean, Function, Object, Null, undefined,Array 등의 데이터 타입이 존재한다. 하지만 Array,Function,Date,RegExp 와 같은 데이터는 엄밀히 따지면 Object이다. 자바스크립트를 객제기반 언어라고 하는 이유이기도 하다. 실직적으로 자바스크립트에서 사용되는 대부분의 데이터 타입은 객체로 존재하며 그에 따른 사용또한 객체기반이 될수 밖에 없다. 그래서 자바스크립트 기술 문서들은 다음과 같이 데이터 타입을 분류 한다.
수 (Number)
문자열 (String)
부울,불린 (Boolean)
객체 (Object)
함수 (Function)
배열 (Array)
날짜 (Date)
정규식 (RegExp)
널 (Null)
정의되지않음 (Undefined)
Number
숫자를 표현하거나 산술 연산을 하는데 사용되는 데이터 타입이다. 기본적으로 +, -, *, / 등의 산술연산이 가능하며 Math 라는 내장객체를 이용하여 수학함수를 이용한 결과를 얻을 수도 있다. 명세에 따르면 자바스크립트의 Number는 "64비트 형식 IEEE 754 값" 으로 정의 된다. 이 때문에 자바스크립트에서 간혹 의도하지 않은 어이없는 결과로 인하여 개발자를 열받게 하기도 한다
console.log(0.1+0.2);
> 0.30000000000000004// ????????? 이런 미친 결과가 나오니 주의해야 한다.
Number Casting :: parseInt(), parseFloat()
자바스크립트는 변수선언시 데이터 타입을 명시하지 않는다. 그래서 숫자형 데이터가 아닌 문자열 데이터로 숫자를 표현하는 일도 많다. 그런 이유로 자바스크립트 코딩을 하다보면 데이터형 변환(casting)을 해야하는 경우가 종종 발생한다. 자바스크립트의 캐스팅을 이용한 데이터형 변환에 자주 사용되는 내장객체 중 parseInt()를 이용하면 문자열을 정수로, parseFloat()를 사용하면 실수로 치환 할수 있다.
console.log(parseInt("010", 10));
> 10
parseInt() 의 두번째 인자는 치환하고자하는 문자열이 몇진수의 숫자를 표현한 문자열인가를 나타낸다 . 위 예제는 "010" 을 10진수 표현의 문자열이라는 의미다. 이와 같은 원리로 두번째 인자를 2로 변경 하여 2진수를 나타낸 문자열이라고 한다면 그 결과는 위 예제와 다른 값이 출력된다.
console.log(parseInt("010", 2));
> 3// 010 을 2진수로 판단하여 10진수 정수로 변환한 값을 출력해준다.
재미있는 사실 하나는 위 예제들에서 두번째 인자를 전달하지 않으면 원하는 결과와 전혀 다른 값이 출력된다.
console.log(parseInt("010"));
> 8// ??? 뜬금없이 8 이 출력된다.
이런 어이없는 결과가 나오는 이유는 간단하다. parseInt() 는 두번째 인자인 진법을 지정하지 않으면 0x로 시작하면 16진수, 0으로 시작하면 8진수로 인식하여 그 결과를 출력하기 때문에 "010"을 8진수로 인식하여 8이라는 결과를 출력한 것이다. 그러니 정확한 결과값을 원한다면 두번째 인자를 반드시 전달해주는 것이 좋다. 또한가지 주의 해야 할 것은 parseInt()는 소수점을 과감하게 버림한다. 그러니 소수점을 표현하고자 한다면 parseFloat()를 사용하여야 한다.
parseFloat()를 사용하면 parseInt()에서 나왔던 두번째 인자 미전달로 인한 황당한 결과의 초래도 막을 수 있다.
위에서 설명한 문자열에서 숫자형으로 데이터형변환시 대상이 되는 문자열이 숫자를 표현한 문자열표현이 아닌경우 자바스크립트는 Not a Number의 약자인 NaN 을 리턴 하며 말그대로 해당 하는 값이 숫자가 아니란 뜻이다. 이 NaN은 자바스크립트를 다루면서 반드시 인지하고 있어야 하는 결과 중 하나 이기도 하다.
console.log(parseInt("hello world", 10));
> NaN
자바스크립트에서 해당 데이터가 NaN 인지 검사해주는 isNaN() 이라는 내장 객체가 존재하는데 이 isNaN() 객체는 상당히 많은 활용도를 가지는 내장객체이다. isNan() 은 NaN 인지 여부를 검사하는 함수임으로 NaN 일때 true 를 반환한다.
Number의 무한대의 표현식 :: Infinity , -Infinity 과 isFinite()
자바스크립트의 특별한 표현식중 하나가 바로 이 무한대를 나타내는 Infinity 이다. 0보다 큰 양의 무한대는 Infinity, 0보다 적은 음의 무한대는 -Infinity로 표현 되는데 이는 산술연산의 결과 혹은 데이터 값이 무한대값을 가지는 경우 자바스크립트가 출력하는 결과값이다.
String은 문자열을 표현하는데 사용되는 데이터 타입이다. 자바스크립트의 문자열은 16비트 유니코드 문자들의 연결구조 이기도 하다. 즉 문자열이라 함은 문자 하나하나가 연결되어 하나의 표현을 이루는 데이터를 말하는 것이다.
String property :: length
이글 초반에 언급했듯이 자바스크립트의 대부분의 데이터타입은 객체로 존재하고 있다. 문자열의 typeof는 당연히 String 이다. 하지만 문자열또한 객체로서 활용가능한 property와 method를 가지고 있는 특이한 형태의 데이터 타입이다. 그러한 이유로 php 에서 문자열의 길이를 구하기위한 strlen() 함수와 같이 별도의 내장함수를 사용하지 않고도 문자열이 가지고 있는 property인 length 만으로도 그 길이를 알수 있다.
문자열은 객체에 의존적으로서 객체의 수행을 담당한다는 메소드(함수와 메소드의 차이는 객체를 다룰때 자세히 정의 하겠다.) 를 가지고 있다. 이들 메소드들은 문자열을 여러가지 형태로 변환하거나 문자열 정의에서 밝힌 문자 하나하나의 연결중 특정 위치의 값을 가져오는데 활용할 수 있다.
console.log("hello world".charAt(0));
> h // charAt() 를 활용하여 문자열의 특정 자리에 위치한 문자를 반환.````
console.log("hello world".replace("hello","goodbye"));
> goodbye world // replace() 를 활용하여 문자열 치환을 수행.
console.log("hello world".toUpperCase());
> HELLO WORLD //toUpperCase()를 이용하여 문자열을 대문자로 치환.
console.log("1+2+3+4+5".split("+"));
> ["1","2","3","4","5"] // split() 를 활용하여 "+" 를 기준으로 문자열을 나누어 배열로 반환한다.
Null (값 없음) 과 undefined
자바스크립트는 값이 없음을 나타내는 null 과 초기화(선언) 되지 않았거나 값이 할당되지 않았음을 나타내는 undefined라는 특별한 데이터 타입이 존재한다. 자바스크립트를 활용함에 있어서 null 과 undefined의 차이를 정확히 이해하지 않는다면 훗날 큰 곤욕을 치를 지도 모른다. **null 은 자바스크립트 개발자가 의도적으로 비어있는 값을 부여한 것이고 undefined 는 **애당초 어떤한 값도 할당되지 않은 것이다. 자바스크립트는 변수의 선언과 초기화를 동시에 하지 않아도 된다. 이는 다른 언어에서도 마찬가지로 활용되지만 자바스크립트에서는 특이하게 선언만 하고 초기화 되지 않은 변수는 초기화되지 않았거나 값이 할당되지 않았음을 표현하는 undefined 라는 값을 할당받는다.
var a; // 선언만 되고 초기화 되지 않은 변수;
console.log(a);
> undefined// 초기화가 이루어지지 않았음으로 java-script 엔진이 undefined를 강제적로 할당한다.
하지만 유념해야하는 것이 있다. 위 예제의 결과는 undefined 지만 아래의 예제를 보면 null과 undefined가 굉장히 햇갈리게 된다.
var a;
console.log(typeof a);
> undefined// 초기화가 이루어지지 않았음으로 당연히 undefined
console.log(a==undefined);
> true// 당연히 a==undefined 는 true를 반환 한다.
console.log(a==null);
> true//typeof 는 undefined 이지만 null 인지 검증하면 true를 반환 한다.
console.log(null==undefined);
> true//null==undefined 검증은 true를 반환한다.
위 예제를 보면 null과 undefined는 그닥 별반 차이가 없어 보인다. 검증에 따른 결과가 둘다 true 이기 때문이다. 하지만 선언조차 하지 않은 변수를 활용한다면 이야기가 틀려진다.
console.log(typeof a);
> undefined// 선언되지 않은 변수를 사용함에 따라 자바스크립트 엔진이 강제적으로 undefined를 할당한다.
console.log(a==undefined);
> // error. 선언되지 않은 변수이기에 undefined의 데이터타입이지만 사용할 수는 없는 문법 오류이다.
그럼 정확하게 null과 undefined를 구별하기 위해서는 어떻게 해야하느냐? 간단하다 **null은 변수가 참조하는 객체가 없음(null)을 나타내고, **undefined는 그 변수가 참조하는 객체를 아직 지정하지 않았음(not initialized)을 뜻한다. 따라서 undefined 값을 가지는 변수는 할당을 통해 값을 가지며 이 값(객체)을 해제할때 null 이 되는 것이다.
var a;
console.log(a);
> undefined // 아직 값이 할당되지 않은 undefined
a = "hello world";
console.log(a);
> hello world // a는 "hello world"의 문자열 객체를 할당 받았음으로 null,undefined가 아닌 hello world 이다.
a=null;
console.log(a);
> null // a가 참조하는 hello world 라는 문자열 객체를 해제하였기에 undefined가 아닌 null 이다.
Boolean (부울,불린)
자바스크립트도 부울 혹은 불린으로 불리우는 이 데이터 타입은 true,false값을 가지는 논리 데이터 타입이다 . 자바스크립트뿐만 아니라 대다수의 프로그래밍 언어에서 가장 많이 사용되는 이 Boolean 데이터 타입은 Boolean() 함수를 이용하여 검증을 수행할수 있다.
흔히 가장 많이 활용되는 if(a) 라는 형태의 제어문은 if(Boolean(a)) 의 약식 표현인 것이다.
갈무리
이번 포스팅에서는 자바스크립트에서 사용되는 데이터 타입에 대해 다루었다. 하지만 글을 차분히 잘 읽은 사람이라면 이미 깨닿고 있을 Object에 대해서는 이번장에서 다루지 않았다. 그 이유는 Object에 대해 논하려면 이글의 스크롤바는 밑도 끝도 없는 해저 깊숙히 박혀 있어야 할듯 하기에 데이터 타입에 대한 글에서 독립시켜 별도로 다룰 예정이기 때문이다.
어쨋든간에 이번장에서는 자바스크립트를 다루면서 반듯이 이해하고 있어야할 기본중에 기본인 데이터 타입에 대해 다루었으며 이런 기초들 하나하나가 곧 front-end 개발자로서 성장해 나가는 밑거름이며 앞으로 자바스크립트를 다루면서 애매한 상황에서 햇갈리지 않고 자신이 원하는 프로그램코드를 작성하는데 큰 도움이 될것이다. 그럼 이번장은 여기서 마무리하고 다음 포스팅이 될 Object에 대해 정리는 훼이크고 사실 지금 사무실에서 혼자 뻘짓중이기때문에 밤이 늦은 관계로 이젠 슬슬 집에 들어가야겠다.
이번 글은 자바스크립트의 핵심이라고 할수 있는 객체(Object)에 대하여 다뤄 보고자 한다. 이전 글에서도 언급 했듯이 자바스크립트는 객체기반의 스크립트 언어이며 자바스크립트를 이루고 있는거이 모든것들이 객체로 존재한다. 그렇기 때문에 자바스크립트의 기초를 다지면서 객체에 대한 제대로된 이해가 없이 "나는 hellow world"를 찍어내는 것이 목표다" 라고 하는 것은 배추 없이 김장을 담그겠다며 큰소리 치는 소리다. 객체는 그만큼 중요한 요소이기 때문에 한번에 이거다 라고 설명하기에는 무리다. 그래서 이 객체라는 녀석은 이번 글에서 뿐만아니라 앞으로 몇장에 걸쳐 나누어 다룰 예정이고 이번 글은 그 첫 시작을 알리는 글이다.
객체(Object)의 기본정의
자바스크립트에서 말하는 객체는 어떻게 보면 자바스크립트 그 자체라고 해도 무방할것이다. 계속 귀에 못이 밖히도록 반복하고 있듯이 자바스크립트 자체가 객체기반으로 돌아가는 스크립트 언어이기 때문이다. 객체지향언어를 다루어본 사람이라면 객체의 기본개념이 데이터(주채)와 그 데이터에 관련되는 동작(절차,방법,기능)을 모두 포함하고 있는 개념 이란 기본적인 내용은 익히 알고 있을 것이다. 한마디로 객체는 자신의 정보를 가지고 있는 독립적인 주체이다. 우리 주위에 보이는 컴퓨터,책상,의자,마우스,키보드 등이 모두 자신만의 특성을 가진 객체인 샘이다. 그럼 자바스크립트에서의 객체에 대해 알아보기 전에 먼저 객체가 가지는 특성에 대해 알아보도록 하겠다. 물론 이를 정확히 알고 있다고 자부한다면 skip 해도 무방하다.
프로퍼티(Property) & 메소드(Method)
객체에 대해 다루게 될때 프로퍼티와 메소드에 대한 이해가 없으면 객체를 이해할수 없다. 객체란 것은 결국 껍대기를 이루는 말이고 실제 객체를 완성하는 구성요소들은 이 프로퍼티와 메소드이기 때문이다. 즉 우리가 객체라고 부르는 것은 컴퓨터 케이스다 실제 컴퓨터를 구성하고 있는 것은 메모리,보드,파워서플라이,디스크등등을 안에 포함하고 있기에 우리는 그것을 쓸모있는 객체로서 컴퓨터 케이스를 가리키며 컴퓨터라고 부를수 있는 것이다. 그래서 자바스크립트의 객체를 다루기 전에 가장 먼저 프로퍼티와 메소드에 대해 알아보겠다.
프로퍼티(property)
ECMA5에서 프로퍼티는 객체의 일부로 이름과 값 사이 연결을 의미 라고 정의 한다. 한마디로 프로퍼티는 객체의 속성을 나타내는 접근 가능한 이름과 활용 가능한 값을 가지는 특별한 형태이다. 이는 객체를 활용함에 있어서 매우 유용한 녀석이다. 특정객체가 가지고 있는 정보를 품고 있기에 **그 객체가 가진 정보에 직접적으로 접근할 수 있게 해주기 때문이다. 물론 proto 와 같이 프로그램적으로 접근 불가능한 프로퍼티들도 존재한다. 이들에 대해서는 후에 자세히 다루도록 하겠다. 어찌되었건 프로퍼티란 기본적으로 이름과 값을 가지고 객체의 정보를 담고 있는 녀석들이다. 이 프로퍼티의 접근 연산자는 '.' 이다. 이 연산자를 통해 프로퍼티를 추가 할수도 있고 프로퍼티에 접근 할수도 있다.
var foo={}; // foo 객체 생성.
foo.a=1; // . 연산자를 이용하여 a 라는 이름의 프로퍼티를 생성하면서 1이라는 값을 할당.varsum= foo.a+10; // . 연산자를 이용하여 foo 객체의 a 프로퍼티에 접근하여 값을 활용가능하다.
console.log(sum);
>11
여기서 문제를 하나 내보도록 하겠다. 자바스크립트에서 프로퍼티에 아무런 값도 할당하지 않으면 어떻게 될가?
자바스크립트에서 사용하는 변수는 값을 할당하지 않고 선언만 할경우 자바스크립트 엔진이 강제적으로 undefined를 할당한다. 하지만 프로퍼티는 값이 활당되지 않으면 존재할 필요가 없는 녀석이다. 객체의 정보를 담고 있어야할 요소가 그 어떤 정보도 할당받지 않았다면 객체로서는 이 프로퍼티는 쓸모없는 녀석이기 때문이다. 그렇기 대문에 프로퍼티를 추가하면서 값을 할당하지 않으면 syntax error 이다. 프로퍼티를 추가 할때 이점을 반드시 주의 하여야 한다.
또한가지 추가적으로 반드시 기억해야할 것은 자바스크립트의 프로퍼티는 undefined나 null 을 할당한다고 삭제 되지 않는다. 프로퍼티의 삭제는 delete 라는 keyword를 사용하여야 한다.
var foo= new Object();
foo.name='foo';
console.log(foo.name);
> foo
foo.name=null;
console.log(foo.name);
> null
delete foo.name;
console.log(foo.name);
> undefined
메소드(method)
메소드는 객체가 가지고 있는 동작이다. 이 동작이란 의미에서 함수와 메소드를 구분하지 못하는 분들도 많다. 기본적으로 함수와 메소드가 서로 일련의 동작을 실행한다는 점에서 동일하기 때문이다. 하지만 메소드와 함수는 엄연하게 서로 다른 녀석이다. 메소드와 함수의 차이가 무엇인지 궁금한 분을 위해 잠깐 그 차이를 설명하고 넘어가겠다
메소드는 위에서 설명하였듯이 객체가 가지고 있는 동작이다. 메소드를 수행하기 위해서는 객체를 통해서 해당 메소드를 수행하여야 한다. 즉 그 동작을 수행하는 주체는 객체이며 그 동작을 수행하기 위해서는 객체에게 그 동작을 수행하라고 지시해야 한다. 함수는 그 동작을 수행하기 위해 객체에게 어떤을 동작을 수행하라고 명령하지 않아도 된다. 그이유는 함수자체가 그 동작을 정의한 함수객체이기 때문에 자기 자신을 수행하는 것이다. 함수객체라는 것에 대해서는 이후에 자세히 설명하도록 하겠다. 어찌 되었건 메소드는 객체를 움직이는 동작이며 그 동작을 수행하기 위해서 객체의 정보를 담고있는 프로퍼티를 사용할수 있다.
var foo={};
foo.a=1;
foo.b=2;
foo.sum=function() {console.log(foo.a+foo.b);};
foo.sum();
> 3
자바스크립트에서 사용되는 메서드는 다른 언어와 다른 재미있는 특징을 가지고 있다. Java와 같은 class 기반 객체생성 모델의 언어를 다루던 사람이라면** 메소드는 단지 meta data 일뿐이며 객체 에 속한 동작에 대한 선언일 뿐일 것이다. 하지만 자바스크립트는 다르다. 자바스크립트에서의 메소드는 각각 개별 객체로 존재 한다. 그 사용법은 동일하지만 객체에 속한 메타 데이터를 사용하는 것이 아닌 그 객체로 부터 파생되어 확장된 새로운 객체를 사용하는 것이다.
자바스크립트의 객체(Object) 구성
이제 객체란 녀석에 대해 기본적인 개념을 이해 했다면 자바스크립트에서의 객체란 무엇인지 알아보도록 하겠다. ECMA Script 에서 객체는 크게 3가지로 구분하고 있다. 1. Built-in Object 2. Native Object 3. Host Object
1. Built-in Object(자바스크립트 내장객체)
Built-in Object 에는 Global,Object, String, Number, Boolean, Date, Array, Math, RegExp, Error 등 많은 내장객체들이 있다. 이들은 자바스크립트 엔진이 구동되는 시점에서 바로 제공되며 자바스크립트코드 어디에서든 사용이 가능하다.
2. Native Object(브라우져 내장 객체)
Native Object 역시 자바스크립트가 구동되는 시점에서 바로 사용이 가능한 객체 들이다. 하지만 이들은 자바스크립트 엔진이 구성하는 기본객체라고 하기 보단 브라우져 즉 자바스크립트 엔진을 구동하는 녀석들에서 빌드되는 객체 들이다. 자바스크립트 프로그램을 작성하면서 사용하는 브라우져객체모델(BOM)과 문서객체모델(DOM)들이 이에 속한다. 이 객체들은 Built-in Object 가 구성된후에 구성된다. 이 Native Object역시 내장객체이다. Built-in Object와 동일하게 자바스크립트 구동시점부터 바로 사용가능 하다. 하지만 이들을 굳이 Native Object 라 분류한 이유는 브라우져마다 이 Native Object 를 사용함에 있어서 그 구성을 달리하는 경우가 있기 때문이다.
3. Host Object(사용자 정의 객체)
Host Object 는 말그대로 사용자가 생성한 객체 들이다. constructor 혹은 객체리터럴을 통해 사용자가 객체를 정의하고 확장시킨 녀석들이기 때문에 Built-in Object 와 Native Object가 구성된 이후에 구성되어 진다.
자바스크립트의 객체(Object) 생성
자바스크립트에서 객체를 생성하는 방법은 크게 2가지로 나누어 진다. constructor 를 이용하여 생성하는 방식과 객체 리터럴을 이용하는 것이다. 객체리터럴을 이용한 방식은JSON(Java-Script Object Notation) 을 이용하여 표현 한다.
/* constructor 를 사용하여 객체를 생성하는 예 */var foo =new Object(); //
foo.name='foo';
console.log(foo.name);
> foo
/* JSON 방식을 사용하여 객체 리터럴 */var foo = {
name : 'foo'
};
console.log(foo.name);
> foo
이 둘은 객체를 생성하는 방법이라는 관점에서는 동일하지만 객체의 사용이라는 방식에서는 차이가 있다. JSON 방식을 이용하면 객체리터럴이기에 단일 객체로만 활용 된다. 하지만 constructor 를 이용하면 동일한 구성을 가진 객체를 여러개 만들어 낼수 있다.
자바스크립트의 객체(Object) 참고사항.
자바스크립트에서 생성되는 객체들에게는 재미있는 사실 몇 가지가 포함되어 있다. 자바스크립트에서 생성하는 모든 객체는 Object 객체에서 파생되어 나온 객체들이란 것과 이들은 암묵적으로Array 객체를 상속한다는 것이다.
Obejct 객체
Object 객체는 Built-in 객체로서 최상위레벨의 객체((Top-Level-Object) 이다. 즉 모든 객체는 이 Object 객체에서 파생되어 나온 확장형태인 것이다. Object 객체라는 최상위 객체를 껍대기로 파생되는 객체들은 Object 객체가 가지고 있는 기본적인 구성요소를 상속받게 된다. 사실 자바스크립트의 모든 객체가 자유롭게 확장될수 있는 이유도 Object 객체가 가진 특별한 구성요소 때문이다. Object 객체가 가진 특별한 구성요소들이란 바로 constructor, prototype 이라는 프로퍼티와 hasOwnProperty(), toString(), isPrototypeOf() 라는 메소드들이다. 이들에 대해서는 바로 다음 글에서 자세히 다룰예정이니 지금은 이런것이 있다 정도만 알고 있어도 충분하다. 자바스크립트의 모든 객체가 Object 에서 파생되었다는 것은 객체를 생성할때 사용된 생성자 함수의 prototype을 저장하는 __proto__ 를 통해 알수 있으며 객체들이 기본적으로 가지는 메소드들을 보면서 확인 할 수 있다.
var foo = {name:'foo'};
console.log(foo);
Obejct(객체)의 배열 상속
아마 지금부터 나오는 내용은 너무나도 큰 충격으로 다가올수도 있다. 자바스크립트의 객체는 곧 배열이다. 지금까지 객체에 대해 감 나와라 콩 나와라 설명해놓고 이제와서 객체가 곧 배열이다 라는 개소리를 하는 나 자신도 충격적이다. 하지만 이는 명백한 사실이다. 이를 증명할수 있는 방법은 많다. 가장 간단한 방법은 객체의 메소드는 객체 안에 존재하지 않는다. 메소드를 소유한 parent 객체는 객체에 포함된 메소드들의 참조값만을 가지고 있다. 즉 메소드들 자체도 각각 개별 객체로서 존재한다. 이 내용은 prototype을 다루면서 더 자세히 설명하겠다. 또한가지 방법은 Object Notation 이다. JSON의 key, value 방식의 객체 리터럴 구조는 프로그래밍 언어를 공부한 사람은 누구나 아는 배열 구조와 동일하다. 특히 php 와 같은 스크립트 언어의 배열을 봐왔던 사람들은 더더욱 그럴것이다. 즉 JSON의 표현방식은 [] 라는 Array 표현의 다른 방식일 뿐이다. 실제로 객체의 프로퍼티 생성이나 참조에서 . 연산자가 아닌 []를 사용 할수도 있다.
var foo = {name:'foo'};
console.log(foo['name']);
> foo
즉 객체는 자기 자신을 이루는 요소들을 저장한 배열들의 집한체이며 이들은 chain 이라는 연결 구조를 가지고 있다.
갈무리
이번 글에서는 Object 에 대한 간단한 이해를 돕기 위해 잡다한 내용을 많이 담아봤다. 이 내용들은 앞으로 연재될 prototype,function, excution context,this 등의 내용을 이해하는데 필수적인 기본배경지식이 될것이다. 그러니 이 글을 읽고도 Object 에 대해 이해가 잘 안간다면 더많은 레퍼런스를 참고해서라도 반드시 기본개념을 정확히 이해하고 있어야 한다. 그리고 다음 글에 대한 썰을 살짝 플어보자면 위에서 잠간 언급했듯이 prototype에 대한 글이 될것이다. 그러니 위 내용에서 prototype 이 뭐지 하는 의문을 가지고 있지 않아도 된다. 그냥 그런게 있다 정도만 알고 있으면 다음 글에서 자세히 다루도록 하겠다.
Function Declarations(함수선언) vs Function Expressions(함수표현)
소개
이번 글은 function이란 주제를 다뤄볼가 한다. javascript에서 function 은 매우 중요한 녀석이다. 특히 객체를 다루면서 이 function은 절대 빠질수도 빠져서도 안되며 이에 대한 이해가 없다면 javascript 객체에 대한 이해를 제대로 하지 못할 것이다. 그런 이유로 이 글은 Object에 대해 다루기전에 다뤘다면 더 좋았을 거라 생각하지만 사실 자바스크립트에는 연관된 내용이 너무 많은 탓이라며 스스로를 위로하고 있다. 하지만 너무 늦기전에 function에 대한 썰을 풀어놔야 앞으로 연재될 Execution Context와 this,closure 를 이해 할 수 있기에 이번 글을 통해 자바스크립트에서의 function에 사용에 대해 알아보도록 하겠다.
Function at javascript
자바스크립트에서의 function 이란?
독립적으로 분리된 로직으로서 프로그램 수준에서 미리 정의되어 있거나 사용자정의에 의해 만들어진 실행단위를 일컫는 말이다. 자바스크립트의 function은 Fisrt-Class-Object 로서 변수나 데이터 구조 안에 담을 수 있으며 인자로 전달할 수 있고 반환 값으로도 사용할 수 있으며 , 런타임에 생성할 수 도 있다.
이처럼 자바스크립트에서 function은 Fisrt-Class-Object 이면서 모든 함수객체의 프로토타입인 Function Prototype Object의 생성자 함수가 가리키는 class 개념이기도 하다. function 또한 일반 객체처럼 취급될 수 있다. 그래서 익명함수를 비동기 함수의 callback 과 같은 형식으로 넘길 수 있는 것이다.
이런 함수에 대해 알아보기 전에 반드시 집고 넘어가야하는 것이 있다. 바로 함수선언(Function Declarations) 과 함수표현(Function Expressions)에 대한 구분이다.
함수선언(Function Declarations) 이란?
함수선언은 Function Statement 라고도 하며 말그대로 함수 문장이란 뜻이다. 이는 곧 실행가능한 코드블럭이 아니며 함수의 정의를 나타내는 문장으로 해석되며 따라서 코드해석에 따른 수행결과가 존재하지 않는다는 것을 의미한다.
Statement 라는 개념을 잘 집고 넘어가야 한다. 함수 선언문이 Statement 라고 하는 말은 정의에서 밝힌것 처럼 코드블럭 자체는 실행가능 코드가 아니라는 것이다. 즉 해당 코드블럭을 콘솔에서 실행하여도 어떠한 결과도 return 되지 않는다. 그러한 이유로 함수선언문을 Class와 동일한 개념으로 이해해도 무방하다.
이러한 statement 는 console 에서 실행을 시켜도 아무런 결과를 반환하지 않는다.
함수표현(Function Expressions) 이란?
함수표현은 Function Literal 이다 이는 실행 가능한 코드로 해석 되어지거나 변수나 데이터구조에 할당되어지고 있음을 의미한다. 즉 해당 코드블럭이 실행코드로서 해석되어지며 동시에 코드실행에 따른 결과값을 가지거나 특정 변수에 할당된 값으로 존재한다.
이처럼 함수표현은 함수리터럴로 특정변수에 할당되거나 즉시 실행가능한 코드 블럭으로서 존재하는 함수를 의미하는 것이다. 함수표현이 실행코드로서 해석되기 위해서는 ()를 이용하여 함수를 감싸 주어야 한다. 이를 자기호출함수(self invoking function) 라고도 한다. 자기호출함수는 재귀함수와는 다른 개념이다. 재귀함수는 함수 안에서 자신을 호출하는 형식이지만 자기호출함수는 해석과 동시에 실행되는 코드블럭을 말한다.
위 코드에서named function expression는 매우 특이하다. foo 라는 변수에 이름있는 함수를 할당하고 있다. 흔히 알고 있는 함수리터럴과는 좀 거리가 있다. 하지만 이 named function expression에는 한가지 특징이 있다. 바로 해당 함수의 이름은 함수밖에서는 사용할수 없다는 것이다.
var foo = functionA(){
A(); // 실행가능
}A();// Syntax Error
즉 재귀호출외에는 그다지 쓸대가 없다. 하지만 이런 표현식이 가능하다는 것은 알고 있다고 나쁠것은 없지 않은가? 함수표현은 자기호출함수을 이용하여 콘솔에서 실행결과를 받을 수 있다.
이들 함수선언과 함수표현은 함수호출 방법에서는 큰 차이가 없다. 하지만 *호이스팅(hoisting) 관점에서는 큰 차이 *를 보이게 된다.
호이스팅(hoisting) 이란
인터프린터가 자바스크립트 코드를 해석함에 있어서 Global 영역의 선언된 코드블럭을 최상의 Scope로 끌어올리는 것을 호이스팅이라 한다.
즉 Global 영역에 선언된 변수 또는 함수는 자바사크립트 엔진이 가장 먼저 해석하게 된다. 단 할당구문은 런타임과정 이루어지기 때문에 hosting 되지 않는다.
이 정의가 다소 어려울수도 있다. 하지만 단순하게 선언문은 항시 자바스크립트 엔진 구동시 가장 최우선으로 해석한다고 이해하면 쉬울 것이다. 즉 인터프린터가 자바스크립트의 코드를 해석하면서 코드작성 순서에 상관없이 global 영역에 선언되어 있는 변수나 함수의 선언문들을 먼저 수집하여 전역객체의 속성으로 등록시켜 둔다는 것이다. 그렇기 때문에 전역변수와 전역함수는 자바스크립트 코드의 어떠한 위치에서도 실행이 가능한 것이다.
하지만 같은 이유로 너무 많은 선언문이 남발되어 있는 자바스크립트 코드는 실행코드의 해석시점이 뒤로 밀리게 됨으로서 자바스크립트 실행코드의 구동시점이 길어지는 좋지않는 결과를 가져오기도 한다.
이 hoisting에서 중요한 부분은 statement 는 hoisting 되지만 할당은 hoisting 되지 않는 다는 것이다. 즉 {} 안의 내용은 포함하지만 = 연사자를 사용한 값은 hoisting 되지 않는다.
위 코드에서 알수 있듯이 함수선언은 foo(); 라는 실행코드를 해석하기 이전에 foo 함수에 대한 선언을 호이스팅하여 global 객체에 등록시키기 때문에 오류 없이 수행된다. 하지만 함수표현은 변수에 함수리터럴을 할당하는 구조이기 때문에 Hoisting 되지 않으며 Syntax Error 를 발생시킨다.
이러한 Hoisting은 자바스크립트 코드를 사람이 해석하는데 많은 혼란을 주게 된다. 그런 이유로 자바스크립트 코드를 작성할대 선언문은 항상 최상위에서 작성하는 것을 권고하고 있다.
그럼 지금까지 이해한 내용을 가지고 아래의 퀴즈들을 풀어보도록 하자. 아래의 퀴즈들을 보고 바로 답이 나온다면 더이상 이글을 읽을 필요는 없을 것이다.
Hoisting Question
//Question 1:functionfoo(){functionbar() {console.log('hello');
}
return bar();
functionbar() {console.log('world');
}
}
foo();
//Question 2:functionfoo(){
var bar= function() {console.log('hello');
};
return bar();
var bar = function() {console.log('world');
};
}
foo();
자바스크립트 엔진이 구동되는 시점에 global 영역에 선언된 foo 함수를 hoisting 한다. 이때 foo의 statement 인 {} 블럭안의 내용 또한 같이 따라 올라간다. 런타임에서 해석되는 실행코드보다 선언문이 먼저 해석되기 때문에 return bar(); 라는 실행문이 해석되기 전에 function bar()로 선언된 함수선언문이 먼저 해석 되어 지고 두번째로 선언된 bar 함수 역시 return 구문보다 먼저 해석되어 진다. 그렇기 때문에 퀴즈 1번에서 실행코드를 만나기 전까지 자바스크립트가 해석한 foo함수는 아래와 같다.
functionfoo(){
var bar= function() {console.log('hello');
};
return bar();
var bar = function() {console.log('world');
};
}
foo();
2번 퀴즈에서 역시 foo 함수는 hoisting 되어 런타임이전에 미리 해석되어 진다. 하지만 {}안에 작성된 var bar 라는 변수는 함수리터럴을 할당받고 있다. 할당은 런타임 시점에서 이루어지고 자바스크립트 엔진은 런타임 이전까지는 강제적으로 undefinde 를 할당하게 된다. 즉 hoisting 되어진 foo 함수안에 구조는 실행코드를 만나기 전까지는 아래와 같이 해석되어 진다.
2번째로 작성된 함수표현식은 실행조차 되지 않는다. 런타임에서는 자바스크립트 엔진이 해석한 순서로 실행되기 때문에 할당구문보다 먼저 return 이 실행되어 버린다. 그렇기 때문에 2번 퀴즈는 'hello' 를 출력한다. 같은 이유로 아래의 코드는 Syntax Error 를 뱉어내게 된다.
functionfoo(){return bar();
var bar= function() {console.log('hello');
};
var bar = function() {console.log('world');
};
}
foo();
갈무리
이번 글에서는 함수선언과 함수표현의 차이와 호이스팅에 대해 알아보았다. 사실 호이스팅을 사용한 코드작성은 절대 추천하고 싶지 않다. 코드 가독성이 너무나도 떨어지게 되어 코드를 해석하는데 있어 혼란을 격게 될수도 있기 때문이다. 하지만 이번 글에서 이해한 함수선언과 함수표현은 후에 연재될 Excution Context 를 이해하는데 있어 필수적인 요소중 하나다. 그러니 이번글을 읽고 이해가 잘안된다면 차분한 마음으로 다시 읽어보거나 더 싶게 풀이된 다른 레퍼런스를 찾아보길 바란다. 다음 연재인 function #2는 arguments 객체와 Constructor 가 될것이다.
자바스크립트에서 함수를 정의하는 방법은 Function 객체를 사용하는 방법과 연산자인 function을 사용하는 방법이 있습니다. 일반적으로 Function 객체를 사용한 정의 방법은 많이 사용되지는 않습니다. 연산자인 function을 이용한 함수 정의 방식은 함수선언문(function declaration)과 함수표현식(function expression)으로 나뉩니다.
우리는 그간 아래와 같이 함수를 정의하고 사용해 왔습니다.
// 함수선언식(function declaration)
function company() {
/* 실행코드 */
}
이와 같은 방식을 함수선언식(function declaration)이라고 합니다. 함수선언식으로 정의된 함수는 자바스크립트 인터프리터가 스크립트가 로딩되는 시점에 바로 초기화하고 이를 VO(variable object)에 저장합니다. 그렇기 때문에 함수 선언의 위치와는 상관없이 소스 내 어느 곳에서든지 호출이 가능합니다.
함수 정의할 때 "함수는 first-class object이므로 변수에 할당될 수 있다." 라는 전제 하에 아래와 같이 작성할 수 있습니다.
// 기명 함수표현식(named function expression)
var company = function company() {
/* 실행코드 */
};
// 익명 함수표현식(anonymous function expression)
var company = function() {
/* 실행코드 */
};
// 기명 즉시실행함수(named immediately-invoked function expression)
(function company() {
/* 실행코드 */
}());
// 익명 즉시실행함수(immediately-invoked function expression)
// Javascript 대가이신 더글라스 클락포트의 권장 표기법
(function() {
/* 실행코드 */
}());
// 익명 즉시실행함수(immediately-invoked function expression)
(function() {
/* 실행코드 */
})();
이렇게 정의한 방식을 함수표현식(function expression)이라고 합니다. 함수가 변수에 할당되었으므로 "함수는 객체이다."라는 정의가 가능합니다. 함수표현식은 함수선언식과는 달리 스크립트 로딩 시점에 VO에 함수를 저장하지 않고 runtime시에 해석되고 실행되므로 이 두가지를 구분하는 것은 중요합니다.
함수선언식으로 함수를 정의하면 사용하기에 쉽지만 대규모 애플리케이션을 개발하는 경우 인터프리터가 너무 많은 코드를 VO에 저장하므로 애플리케이션의 응답속도는 현저히 떨어질 수 있으므로 주의해야 할 필요가 있습니다. 참고로, 스크립트 파일을 모듈화하고 이를 필요한 시점에 비동기 방식으로 로딩하여 http 요청을 줄이고 응답속도와 사용자 체감속도를 향상시킬 수 있습니다.
즉시실행함수(Immediately-invoked function expression)
자바스크립트에서 가장 큰 문제점 중의 하나는 글로벌 스코프에 정의된 것은 코드 내의 어디서든지 접근이 가능하다는 것입니다. 하지만, 외부에 공유되면 안되거나 공유될 필요가 없는 속성이나 메소드가 있습니다. 또한, 다른 스크립트 파일 내에서 동일한 이름으로 명명된 변수나 함수가 있을 경우 원치 않는 결과를 가져올 수 있습니다.
익명 함수표현식으로 함수를 하나 정의하고 실행해 보겠습니다. 그리고 외부에서 함수 내의 변수에 접근해 보겠습니다.
// 함수표현식에 의한 명시적인 함수호출
var app = function() {
console.log('함수 호출'); // "함수 호출" 출력
};
app();
함수표현식은 함수를 정의하고, 변수에 함수를 저장하고 실행하는 일련의 과정을 거칩니다. 하지만, 즉시실행함수를 사용하면 이와 같은 과정을 거치지 않고 즉시 실행된다는 특징이 있습니다. 차이점이라면 단순히 함수를 괄호"()"로 랩핑한 게 전부입니다. 이런 함수를 즉시실행함수(IIFE)라 부릅니다.
이번에는 변수를 선언하고 이 변수에 즉시실행함수를 할당해 보겠습니다.
var app = (function() {
var privateVar = 'private';
return {
prop : privateVar
};
}());
console.log(app.prop); // "private" 출력
콘솔에는 "private" 라고 출력됩니다.
즉시실행함수 내에서 선언한 변수를 외부에서도 접근할 수 있음을 확인할 수 있습니다. 변수의 접근 범위가 함수 내부가 아닌 외부에서도 가능해진 것입니다. 이와 같이, 즉시실행함수는 변수의 스코프를 포함하는데 사용되며 외부에서 함수 내의 변수에 접근할 경우 이를 통제할 수 있습니다. 즉시실행함수는 글로벌 네임스페이스에 변수를 추가하지 않아도 되기 때문에 코드 충돌이 없이 구현할 수 있어 플러그인이나 라이브러리 등을 만들 때 많이 사용됩니다.
아래 두개의 코드는 기명 함수표현식과 즉시실행함수에서 파라미터를 전달하는 방법을 보여줍니다.
var buyCar = function(carName) {
// "내가 구매한 차는 sonata입니다." 출력
console.log('내가 구매한 차는 ' + carName + '입니다.');
};
buyCar('sonata');
(function(carName) {
// "내가 구매한 차는 sonata입니다." 출력
console.log('내가 구매한 차는 ' + carName + '입니다.');
}('sonata'));
위 두개의 코드 블럭은 동일한 동작을 수행합니다. 또한, 앞서의 예제처럼 괄호"()"로 랩핑한 차이 밖에 없습니다.
jQuery나 Prototype 라이브러리는 동일한 $라는 글로벌 변수를 사용합니다. 만약, 이 두개의 라이브러리를 같이 사용한다면 $ 변수에 대한 충돌이 생길 것입니다. 하지만, 즉시실행함수의 코드 블럭에서 jQuery를 위한 $ 변수를 사용하고자 한다면 아래와 같이 파라미터를 전달하는 방법으로 Prototype의 $ 변수에 대한 overwritting을 예방할 수 있습니다.
(function($) {
// 함수 스코프 내에서 $는 jQuery Object임.
console.log($);
}(jQuery));
모듈 패턴(Module Pattern)
현대의 웹 애플리케이션은 점점 더 복잡하고 고도화된 대규모 애플리케이션이나 데스크탑 애플리케이션의 모습을 닮아가는 형태(Rich Internet Application)로 진화하고 있는 추세입니다. 하나의 파일에 모든 코드를 담는 것은 불가능하고 설사 그렇게 작성되었다 하더라도 많은 문제점을 내포할 뿐만 아니라 유지보수시 골치 아픈 경험을 자주 하게될 것입니다. 그렇다면 이를 어떻게 극복할 수 있을까요? 그에 대한 답은 자바스크립트 함수의 특징을 이용한 모듈화에서 부터 찾을 수 있습니다.
Java나 C++과 같은 고급언어들은 언어 자체적으로 모듈화를 지원하는 방법을 제공하지만 자바스크립트는 언어레벨에서 캡슐화를 위한 접근제어자(private, public 등), 모듈 간의 구분을 위한 package가 명시적으로 제공되지 않습니다. 하지만, 명시적으로 제공되지 않는 이런 지원도구들을 자바스크립트 함수의 특징을 이용하여 유사하게 제공할 수 있습니다.
즉시실행함수는 우리가 작성한 코드들 뿐만 아니라 함께 사용하는 외부 라이브러리와도 충돌없이 구동하는 샌드박스(sandbox)를 제공합니다. 이 특징과 단위기능별로 작성된 코드를 분리된 개별 파일 형태로 유지한다면 앞서 언급한 모듈화를 위한 조건을 해결할 수 있습니다.
var clerk = (function() {
var name = 'Teo';
var sex = '남자';
var position = '수석 엔지니어';
// salary private
var salary = 2000;
var taxSalary = 200;
var totalBonus = 100;
var taxBonus = 10;
var payBonus = function() {
totalBonus = totalBonus - taxBonus;
return totalBonus;
};
var paySalary = function() {
return salary - taxSalary;
};
// Public 속성, 메소드
return {
name : name,
sex : sex,
position : position,
paySalary : paySalary,
payBonus : payBonus
};
}());
// name 속성은 public
console.log(clerk.name); // 'Teo' 출력
// salary 변수는 즉시실행함수 내부 변수이므로 private
console.log(clerk.salary); // undefined 출력
// paySalary 메소드는 public
console.log(clerk.paySalary()); // 1800 출력
// payBonus 메소드는 public
console.log(clerk.payBonus()); // 90 출력
console.log(clerk.payBonus()); // 80 출력
위에 작성된 즉시실행함수는 name, sex, position속성과 payBonus메소드를 가진 객체를 clerk변수에 반환합니다. 다시 말해, 즉시실행함수가 clerk변수에 저장되는 것이 아니라 즉시실행함수의 반환값이 clerk변수에 저장됩니다. name, sex, position변수를 글로벌 스코프에 추가하지 않고 단지 clerk변수만 추가되었습니다. 애플리케이션의 규모가 커지면 글로벌 변수에 대한 충돌을 고려해야 하므로 글로벌 변수 사용을 자제해야 합니다. 또한, name, sex, position속성은 clerk변수에 저장된 객체의 속성이므로 외부에서 접근이 가능(public)하지만 salary, totalBonus변수는 즉시실행함수 내의 변수이므로 외부에서 접근할 수 없습니다(private). paySalary()메소드를 호출하여 지급된 급여인 1800을 반환값을 가져올 수 있고 payBonus()메소드를 호출하여 지급된 상여금인 90을 반환할 수 있으며 payBosnus()메소드를 한번 더 호출하여 80을 반환할 수 있습니다. 여기서 주목해야 할 부분은 payBonus()메소드를 호출할 때마다 totalBonus 변수에 저장된 값이 업데이트 되지만 paySalary()메소드는 여러번 호출해도 salary 변수에 저장된 값은 업데이트 되지 않는 점입니다.
즉시실행함수를 위와 같이 사용하여 언어레벨에서 제공하지 못하는 모듈화 지원도구를 극복할 수 있으며 이렇게 작성된 코드를 분리된 파일로 구성하면 재사용성을 높일 수 있습니다.
자바스크립트 모듈 작성시 코드 순서
Javascript로 SPA를 구현할 때, 다음과 같은 순서의 코드로 모듈을 작성하게 되면 협업하는 개발자들에게 집약되고 일관된 코드를 제공하여 많은 도움이 될 것입니다. 이렇게 코드의 순서를 정하는 이유는 집약되고 일관된 코드를 제공하는데 있으므로 코드의 순서는 개발상황과 모듈용도에 맞춰가며 조정하고 추가 및 삭제될 수도 있습니다. 아래 순서는 단지 코드작성 순서의 예일뿐입니다.
모듈 스코프 내에서 사용할 변수 작성
유틸리티 메소드 작성
DOM 조작 메소드 작성
이벤트 핸들러 작성
Public 메소드 작성
// SPA 모듈 작성 순서 예시
var app = (function() {
// 1. 모듈 스코프 내에서 사용할 변수 작성
var scopeVar = {};
var utilMethod;
var manipulateDom;
var eventHandle;
var initModule;
// 2. 유틸리티 메소드 작성
utilMethod = function() {
// 실행코드
};
// 3. DOM 조작 메소드 작성
manipulateDom = function() {
// 실행코드
};
// 4. 이벤트 핸들러 작성
eventHandle = function() {
// 실행코드
};
// Public 메소드 작성
initModule = function() {
// 실행코드
};
return {
init : initModule
};
}());
아래 코드는 라이브러리를 모듈화하는 코딩기법을 정리해 봤습니다. jQuery, Backbone, underscore, requireJs 등 많은 자바스크립트 라이브러리나 프레임워크가 아래와 같은 코딩기법을 사용하고 있습니다.
/**
* Library 모듈화를 위한 코딩기법 1
* call 함수 이용
*/
(function() {
'use strict';
var root = this;
var version = '1.0';
var Module1;
if(typeof exports !== 'undefined') {
Module1 = exports;
} else {
Module1 = root.Module1 = {};
}
Module1.getVersion = function() {
return version;
}
}).call(this);
console.log(Module1.getVersion());
/**
* Library 모듈화를 위한 코딩기법 2
* 글로벌 객체를 파라미터로 전달
*/
(function(global) {
var root = global;
var version = '1.0';
var Module2;
if(typeof exports !== 'undefined') {
Module2 = exports;
} else {
Module2 = root.Module2 = {};
}
Module2.getVersion = function() {
return version;
}
}(this));
console.log(Module2.getVersion());
/**
* Library 모듈화를 위한 코딩기법 3
* 즉시실행함수 내부에서 글로벌 객체를 내부 변수에 할당
*/
(function() {
var root = this;
var version = '1.0';
var Module3;
if(typeof exports !== 'undefined') {
Module3 = exports;
} else {
Module3 = root.Module3 = {};
}
Module3.getVersion = function() {
return version;
}
}());
console.log(Module3.getVersion());
/**
* Library 모듈화를 위한 코딩기법 4
* apply 함수 이용
*/
(function() {
var root = this;
var version = '1.0';
var Module4;
if(typeof exports !== 'undefined') {
Module4 = exports;
} else {
Module4 = root.Module4 = {};
}
Module4.getVersion = function() {
return version;
}
}).apply(this) ;
console.log(Module4.getVersion());
/**
* Library 모듈화를 위한 코딩기법 5
* 기명 즉시실행함수 이용
*/
var Module5 = (function() {
var root = this;
var version = '1.0';
var Module;
if(typeof exports !== 'undefined') {
Module = exports;
} else {
Module = root.Module = {};
}
Module.getVersion = function() {
return version;
}
return Module;
}());
console.log(Module5.getVersion());
맺음말
현대의 웹 애플리케이션은 시대와 환경의 요구로 인해 대규모 형태로 개발되고 있으며 데스크탑 애플리케이션을 닮아가는 추세이다. 자바스크립트로 이런 웹 애플리케이션을 개발하기 위해서는 자바스크립트 함수에 대한 올바른 이해가 뒷바침되어야 합니다.
특히, 즉시실행함수를 이용한 변수에 대한 스코프, 글로벌 네임스페이스 오염 방지, 언어레벨에서 명시적으로 지원하지 않는 캡슐화 도구를 모듈패턴으로 극복하는 방법 그리고 다른 라이브러리와 충돌없이 우리의 소스를 유지하는 방법은 꼭 이해해야 합니다.
다음에는 클로져(Closure)에 대한 내용이나 scope chain, prototype chain에 대해 올려볼 계획입니다.
이번 글에서 다룰 내용은 자바스크립트의 프로토타입 상속(prototypal inheritance) 이라는 확장과 객체의 재사용을 가능하게 해주며 class 기반으로 인스턴스를 생성하지 않는 자바스크립트에서 객체지향적인 개발 개념을 가질수 있게 해주는 Prototype에 대한 내용이다. 하지만 일반적으로 자바스크립트에 대한 정공파가 아니면 제대로 이해하고 있지 않거나 이해 하기 힘든 부분이기도 하다. 그이유는 Prototype Object 와 Prototype Link을 제대로 구분하여 인지 하지 못하고 있기 때문일 것이다. 이번글을 통해서 자바스크립트에서 사용하는 프로토타입이란 녀석의 개념을 확실히 집고 넘어가 보도록 하겠다.
프로토타입기반 프로그래밍 이해하기
프로토타입 기반 프로그래밍 이란?
객체의 원형인 프로토타입을 이용하여 새로운 객체를 만들어내는 프로그래밍 기법이다. 이렇게 만들어진 객체 역시 자기자신의 프로토타입을 갖는다. 이 새로운 객체의 원형을 이용하면 또 다른 새로운 객체를 만들어 낼수도 있으며 이런 구조로 객체를 확장하는 방식을 프로토타입 기반 프로그래밍이라고 한다.
이 프로토타입 기반 프로그래밍은 Class 기반 OOP 언어를 다루던 사람에게는 이해가 잘 안가는 부분 일수도 있다. Class 기반의 언어에서는 Class 안에 기술된 내용을 기반으로 인스턴스를 생성하여 객체를 사용한다. 하지만 자바스크립트는 Class 가 존재하지 않는다. 그래서 자바스크립트에서는 객체의 원형인 프로토타입을 이용한 클로닝(Cloning: 복사)과 객체특성을 확장해 나가는 방식을 통해 새로운 객체를 생성해 낸다. 자바스크립트 응용에서 다룰 프로그래밍 패턴과 연관된 재밌는 사실중 하나는 자바스크립트의 프로토타입 객체의 확장은 옵져버패턴을 따른다. 지금으로서는 그다지 중요하게 알고 있을 필요는 없다. 하지만 이 연재를 계속 지켜본다면 후에 굉장히 신선한 내용으로 다가올 것이다.
자바스크립트에서 사용되는 프로토타입
var foo = {name : "foo"}; // foo 라는 객체를 생성.foo.prototype.a='hello world';
console.log(foo.a);
지금 자신이 알고 있는 자바스크립트 프로토타입에 대한 지식만으로 위 코드의 결과를 추측해 보길 바란다. 어떤 결과를 예상했는지 모르겠지만 위 결과는 당연히 syntax error 이다. 적어도 이글을 읽고 있는 사람중 몇몇은 예상했던 결과와 코드 실행결과를 비교하면서 왜 그런지 이해하지 못할 것이라 생각한다.
그 이유는 극히 단순하다. 자바스크립트에서 말하는 프로토타입이란 말에 원천적인 의미와 실제 자바스크립트를 사용하면서 눈으로 보는 프로토타입이란 녀석과의 차이를 이해하지 못하기 때문이다.
자바스크립트에서 사용되는 프로토타입이란 용어는 크게 두가지로 나눈다. Prototype Property가 가리키고 있는Prototype Object 와 자기 자신을 만들어낸 객체의 원형을 의미하는 Prototype Link 이다. 자바스크립트 관련 레퍼런스는 대부분 프로토타입에 대한 정의는 객체의 원형을 의미하는 Prototype Link 로 정의하지만 Prototype Property를 이용한 객체 확장에 대해서만 다루기 때문에 이 둘의 차이를 자세히 거론하지 않는다. 그런이유로 자바스크립트의 프로토타입을 공부하는 사람들은 Prototype Property가 자바스크립트에서 말하는 프로토타입이라고 이해하고 이를 사용한다. 하지만 이둘은 엄연히 틀린 의미이기 때문에 제대로 이해하고 있어야 한다.
자바스크립트의 프로토타입(Prototype) 란?
자바스크립트의 모든 객체는 자신을 생성한 객체 원형에 대한 숨겨진 연결을 갖는다. 이때 자기 자신을 생성하기 위해 사용된 객체원형을 프로토타입이란 한다. 자바스크립트의 모든 객체는 Object 객체의 프로토타입을 기반으로 확장 되었기때문에 이 연결의 끝은 Object 객체의 프로토타입 Object 다.
자명한 사실이다. 프로토타입이란 의미자체가 원형,원본이란 말이지 않은가. 자바스크립트에서 말하는 프로토타입 역시 문법적인 의미에서 벗어나지 않는다. 즉 어떠한 객체가 만들어지기 위해 그 객체의 모태가 되는 녀석을 프로토타입이라고 한다. Java나 php에서 말하는 class가 바로 자바스크립트의 프로토타입과 동일한 의미일 것이다.
functionA() {};
var A = new A();
console.log(A);
위 예제는 new Operator 를 사용하여 A라는 객체 생성자를 이용한 새로운 객체를 생성하여 B라는 변수에 담는 코드이다. 그렇게 만들어진 객체를 console.log롤 찍어보면 아래와 같은 내용을 확인 할수 있다.
위 내용에서 주의깊게 봐야할 것이 있다. 바로 __proto__과 constructor 이다. __proto__ 바로 이 녀석이 A라는 객체를 만들어내기 위해 사용된 객체원형에 대한 숨겨진 연결이다. 해당객체의 프로토타입은 A라는 함수객체이며 이 객체의 생성자 역시 function A() 함수라는 것이다. 즉 new Operator를 통해 만들어진 객체는 function A() 를 자신의 프로토타입으로 사용하여 만들어졌다는 말이다.
하지만 역시 먼가 꺼림직하다 "내가 알고있는 프로토타입은 저게 아닌데?" 라고 생각하고 있을 것이다. 그 이유는 바로 당신이 알고 있는 프로토타입은 자신을 만들어낸 객체원형을 의미하는 것이 아니기 때문이다. 대부분의 자바스크립트 기초가 부족한 사람들이 말하는 프로토타입은 애석하게도 prototype 프로퍼티다.
자바스크립트의 프로토타입(Prototype) 프로퍼티 란?
모든 함수 객체의 Constructor는 prototype 이란 프로퍼티를 가지고 있다. 이 prototype 프로퍼티는 객체가 생성될 당시 만들어지는 객체 자신의 원형이될 prototype 객체를 가리킨다. 즉 자신을 만든 원형이 아닌 자신을 통해 만들어질 객체들이 원형으로 사용할 객체를 말한다. prototype object는 default로 empty Object 를 가리킨다.
이 말이 매우 어렵게 들릴수도 있다. 하지만 정확히 이해한다면 그리 어려운 말이 아니다. 위에서 분명히 프로토타입은 자기 자신을 생성하게 한 자신의 원형 객체라고 정의했다. 그럼 그 원형객체란 프로토타입은 function A() 함수객체 그 차체일가? 전혀 그렇지 않다.
자바스크립트의 모든 객체는 생성과 동시에 자기자신이 생성될 당시의 정보를 취한 Prototype Object 라는 새로운 객체를 Cloning 하여 만들어낸다. 프로토타입이 객체를 만들어내기위한 원형이라면 이 Prototype Object 는 자기 자신의 분신이며 자신을 원형으로 만들어질 다른 객체가 참조할 프로토타입이 된다. 즉 객체 자신을 이용할 다른 객체들이 프로토타입으로 사용할 객체가 Prototype Object 인 것이다. 즉 위에서 언급한 __proto__라는 prototype 에 대한 link는 상위에서 물려받은 객체의 프로토타입에 대한 정보이며 prototype 프로퍼티는 자신을 원형으로 만들어질 새로운 객체들 즉 하위로 물려줄 연결에 대한 속성이다.
functionfoo(){}varfoo = newfoo();
위 예제코드를 통해 만들어지는 Prototype Link 와 Prototype Property가 가리키는 Prototype Object의 Real Link 에 대한 관계도는 다음과 같다.
위 관계도에서 알수 있듯 모든 객체의 확장은 객체가 소유한 prototype Object를 통해 이루어지며 이 연결의 끝은 이전 글에서 소개한 Object 객체의 prototype Object 가 된다. 일반적으로 자바스크립트 코드를 통해 다루는 prototype이 객체가 소유한 자기자신의prototype Object 이기 때문에 대부분의 자바스크립트 입문자는 프로토타입이란 것을 prototype property라고 생각하게 되는 오류를 범하게 되는 것이다.
자 이제 자바스크립트 프로토타입에 대한 이해가 부족한 사람 혹은 자바스크립트 입문자들이 가장 햇갈려하는 부분에 대하여 알아보도록 하겠다.
functionfoo(x) {this.x = x;
};
var A = new foo('hello');
console.log(A.x);
> hello
console.log(A.prototype.x)
> syntax error
엇! 소리가 나올 듯하다. 하지만 이것은 당연한 결과이다. **prototype 프로퍼티는 Constructor가 가지는 프로퍼티** 이다. 그리고 *함수객체만 이 프로퍼티를 가지고 있다고 했다. * A객체는 함수객체가 아니다. foo 라는 원형을 이용하여 함수객체를 통해 만들어진 Object 객체에 확장된 단일 객체일 뿐이다. 즉 A는 prototype 프로퍼티를 소유하고 있지 않기에 A.prototype.x가 syntax error 인 것이다. 즉 프로토타입을 이해하려면 foo.prototype.x 는 OK, A.prototype.x 는 error 라는 사실을 반드시 이해하고 기억하고 있어야 한다.
지금까지 내용을 잘 이해하고 있다면 예제1의 this.x 는 A의 프로퍼티 x 이고, 예제2 는 A의 prototype Object 에 연결된 x 라는 것을 이해 할 수 있을것이다. 그럼 왜 굳이 prototype 을 사용하여야 하는가? 이 문제는 객체를 어떻게 사용 할 것인가에 따라 다르게 된다.
//#예제 1.
var A = function() {this.x = function() {console.log('hello');
};
};
A.x=function() {console.log('world');
};
var B = new A();
var C = new A();
B.x();
> hello
C.x();
> hello
//#예제 2.
var A = function() { };A.x=function() {console.log('hello');
};
A.prototype.x = function() {console.log('world');
};
var B = new A();
var C = new A();
B.x();
> world
C.x();
> world
예제1, 예제2 에서 B,C 를 생성하기 위한 객체 원형 프로토타입은 A 이다. 하지만 여기서 반드시 집고 넘어가야하는 사실은 B,C는 A 를 프로토타입으로 사용하기위해서 A의 prototype Object를 사용한다는 것이다. 그리고 이 Prototype Object는 A 가 생성될 당시의 정보만을 가지기 때문에 예제1에서 A의 Prototype Object가 알고 있는 x 는 function () {console.log('hello');} 가 된다. 즉 A.x 를 아무리 수정하여도 A의 Prototype Object는 변경되지 않기 때문에 A 를 프로토타입으로 생성되는 B,C는 function () {console.log('hello');} 만 참조하는 것이다.
예제2 에서의 결과가 world 가 되는 이유도 같은 이유다. A.prototype 은 A의 Prototype Object를 참조하는 녀석이기 때문에 A.prototype.x 를 정의한다는 것은 A의 Prototype Object를 직접 이용하게 되는 것이고 그에 따라서 A의 Prototype Object를 프로토타입으로 이용하여 만들어지는 B,C 가 알고 있는 x 는 function () {console.log('world');} 가 되는 것이다.
예제2의 객체 상속 모델을 그림으로 표현하면 아래와 같이 나온다.
자바스크립트의 프로토타입 체인(Prototype Chain)
객체의 생성 과정에서 모태가 되는 프로토타입과의 연결고리가 이어져 상속관계를 통하여 상위 프로토타입으로 연속해서 이어지는 관계를 프로토타입 체인이라고 한다. 이 연결은 __proto__ 를 따라 올라가게 된다.
즉 프로토타입 체인이란 위에서 봤던 프로토타입을 상속해서 만들어지는 객체들관의 연관관계를 의미한다. 그림에서 __proto__ 프로퍼티들간 이어진 점선을 타고 가다보면 최종적으로 Object 객체의 prototype Object에 다다르는 것을 알수 있다. 그렇기 때문에 자바스크립트의 모든 객체는 Object 객체에서부터 파생되어 나온 자식들이라고 하는 것이다.
이러한 프로토타입 체인은 하위 객체에서 상위객체의 프로퍼티와 메소드를 상속받는다. 그리고 동일한 이름의 프로퍼티와 메소드를 재정의 하지 않는 이상 상위에서 정의한 내용을 그대로 물려받는다. 하지만 여기에는 엄청난 꼼수가 숨어있다. 사실 꼼수라는 표현이 좀 애매하긴 하지만 위 그림을 잘 보면 B와 C 는 A prototype Object를 프로토타입으로 만들어졌음에도 불구하고 X 라는 프로퍼티가 존재하지 않는다. 사실 "물려 받는다" 라는 말 자체가 꼼수인 것이다. 즉** 하위 객체는 상위 객체의 속성과 메소드를 상속 받는 것이 아니라 그것을 공유**하고 있는 것이다.
var A = function() { };A.prototype.x = function() {console.log('hello');
};
var B = new A();
var C = new A();
B.x();
> hello
C.x();
> hello
A.prototype.x = function() {console.log('world');
};
B.x();
> world
C.x();
> world
위 예제에서 A의 Prototype Object의 x 메소드를 재정의 하였을 때 B,C객체도 그 영향을 받는다는 것을 알 수 있다. 그 이유는 프로토타입 체인에 의한 공유 때문이다. 아래의 그림을 잘 봐보기 바란다.
이전에 소개했던 그림과 비슷하다. 그러니 프로토타입 상속에 대한 구조 이해는 가능하리라 믿는다. 여기서 유심히 봐야할것은 A Prototype Object 가 소유한 Constructor 다. A Prototype Object의 Constructor는 A 객체의 생성자 함수이다. 즉 이를 통해 만들어지는 객체들은 A 객체가 생성될 당시 소유하고 있지 않은 x 라는 메소드를 가질수 없다. x는 A.prototype 를 이용하여 A Prototype Object에 추가 되어진 메소드다. 하지만 이 x 메소드는 프로토타입 체인에 의한 공유 메소드다. A의 Prototype Object 가 소유한 x 라는 메소드는 A Prototype Object를 프로토타입으로 만들어진 모든 객체에서 사용할 수 있다. 이는 프로토타입 기반 상속에서 매우 중요한 특성을 말하고 있다.
위 예제에서 사용한 x 라는 메소드를 하위객체에서 찾지 못하면 상위객체에서 해당 메소드를 탐색한다. 이러 방법으로 프로토타입 체인을 따라 최상위 객체까지 도달할때까지 해당 메소드를 찾지 못한다면 undefined 이 되는 것이다.
하지만 A 객체가 생성당시 x 라는 메소드 혹은 의미있는 프로퍼티에 대한 정의가 포함된 내용을 가지고 있다면 이들은 공유가 아닌 상속된다.
var A= function() {};
var B = new A();
A.prototype.x='hello';
console.log(B);
var A= function() {this.x='hello';};
var B = new A();
console.log(B);
이처럼 공유와 상속의 구분을 정확히 이해하고 사용하는 것은 매우 중요하다. 이들을 햇갈리게 되면 후에 프로토타입 재정의에 따른 원하지 않는 참혹한 결과를 가져다 줄수도 있기 때문이다.
갈무리
프로토타입에 대한 제대로된 이해를 한다는 것은 결국 자바스크립트의 객체에 대한 이해를 하는 것과 동일하다고 생각한다. 이 글을 쓰면서도 이 주제를 어떻게 풀어야할지 많은 고민을 했었다. 그만큼 개념적으로 다가서기가 좀 까다로운 부분이기도 했다. 하지만 내가 아는 범위내에서는 모두 풀어냈다고 보고 이글을 보고도 이해가 잘안된다면 더 많은 레퍼런스를 참고하기 바란다. 참고할만한 레퍼런스는 이글 끝에 추가 하도록 하겠다. 이로서 미루고 미루고 미뤄왔던 프로토타입에 대한 글을 마무리하고 다음 주제는 function 객체 외 다수의 객체들에 대하여 알아볼것이라는 떡밥을 던지며 마무리 한다.
JavaScript는 클래스라는 개념이 없습니다. 그래서 기존의 객체를 복사하여(cloning) 새로운 객체를 생성하는 프로토타입 기반의 언어입니다. 프로토타입 기반 언어는 객체 원형인 프로토타입을 이용하여 새로운 객체를 만들어냅니다. 이렇게 생성된 객체 역시 또 다른 객체의 원형이 될 수 있습니다. 프로토타입은 객체를 확장하고 객체 지향적인 프로그래밍을 할 수 있게 해줍니다. 프로토타입은 크게 두 가지로 해석됩니다. 프로토타입 객체를 참조하는 prototype 속성과 객체 멤버인 proto 속성이 참조하는 숨은 링크가 있습니다. 이 둘의 차이점을 이해하기 위해서는 JavaScript 함수와 객체의 내부적인 구조를 이해 해야합니다. 이번 글에서는 JavaScript의 함수와 객체 내부 구조부터 시작하여 프로토타입에 대해 알아보겠습니다.
1. 함수와 객체의 내부 구조
JavaScript에서는 함수를 정의하고, 파싱단계에 들어가면, 내부적으로 수행되는 작업이 있습니다. 함수 멤버로 prototype 속성이 있습니다. 이 속성은 다른 곳에 생성된 함수이름의 프로토타입 객체를 참조합니다. 프로토타입 객체의 멤버인 constructor 속성은 함수를 참조하는 내부구조를 가집니다. 아래의 그림 1과 같이 표현합니다.
function Person(){}
[소스 1]
속성이 하나도 없는 Person이라는 함수가 정의되고, 파싱단계에 들어가면, Person 함수 Prototype 속성은 프로토타입 객체를 참조합니다. 프로토타입 객체 멤버인 constructor 속성은 Person 함수를 참조하는 구조를 가집니다. 여기서 알아야 하는 부분은 Person 함수의 prototype 속성이 참조하는 프로토타입 객체는 new라는 연산자와 Person 함수를 통해 생성된 모든 객체의 원형이 되는 객체입니다. 생성된 모든 객체가 참조한다는 것을 기억해야 합니다. 아래의 그림 2와 같이 표현합니다.
function Person(){}
var joon = new Person();
var jisoo = new Person();
[소스 2]
JavaScript에서는 기본 데이터 타입인 boolean, number, string, 그리고 특별한 값인 null, undefined 빼고는 모두 객체입니다. 사용자가 정의한 함수도 객체이고, new라는 연산자를 통해 생성된 것도 객체입니다. 객체 안에는 proto(비표준) 속성이 있습니다. 이 속성은 객체가 만들어지기 위해 사용된 원형인 프로토타입 객체를 숨은 링크로 참조하는 역할을 합니다.
2. 프로토타입 객체란?
함수를 정의하면 다른 곳에 생성되는 프로토타입 객체는 자신이 다른 객체의 원형이 되는 객체입니다. 모든 객체는 프로토타입 객체에 접근할 수 있습니다. 프로토타입 객체도 동적으로 런타임에 멤버를 추가할 수 있습니다. 같은 원형을 복사로 생성된 모든 객체는 추가된 멤버를 사용할 수 있습니다.
function Person(){}
var joon = new Person();
var jisoo = new Person();
Person.prototype.getType = function (){
return "인간";
};
console.log(joon.getType()); // 인간
console.log(jisoo.getType()); // 인간
[소스 3]
위 소스 3 6라인은 함수 안의 prototype 속성을 이용하여 멤버를 추가하였습니다. 프로토타입 객체에 getType()이라는 함수를 추가하면 멤버를 추가하기 전에 생성된 객체에서도 추가된 멤버를 사용할 수 있습니다. 같은 프로토타입을 이용하여 생성된 joon과 jisoo 객체는 getType()을 사용할 수 있습니다.
여기서 알아두어야 할 것은 프로토타입 객체에 멤버를 추가, 수정, 삭제할 때는 함수 안의 prototype 속성을 사용해야 합니다. 하지만 프로토타입 멤버를 읽을 때는 함수 안의 prototype 속성 또는 객체 이름으로 접근합니다.
joon.getType = function (){
return "사람";
};
console.log(joon.getType()); // 사람
console.log(jisoo.getType()); // 인간
jisoo.age = 25;
console.log(joon.age); // undefined
console.log(jisoo.age); // 25
[소스 4]
위 소스 4 1라인은 joon 객체를 이용하여 getType() 리턴 값을 사람으로 수정하였습니다. 그리고 joon과 jisoo에서 각각 getType()을 호출하면 joon 객체를 이용하여 호출한 결과는 사람으로 출력되고, jisoo로 호출한 결과는 인간이 출력됩니다. 생성된 객체를 이용하여 프로토타입 객체의 멤버를 수정하면 프로토타입 객체에 있는 멤버를 수정하는 것이 아니라 자신의 객체에 멤버를 추가하는 것입니다. joon 객체를 사용하여 getType()을 호출하면 프로토타입 객체의 getType()을 호출한 것이 아닙니다. joon 객체에 추가된 getType()을 호출한 것입니다. 프로토타입 객체의 멤버를 수정할 경우는 멤버 추가와 같이 함수의 prototype 속성을 이용하여 수정합니다.
Person.prototype.getType = function (){
return "사람";
};
console.log(jisoo.getType()); // 사람
[소스 5]
소스 5를 보게 되면 함수의 prototype 속성을 이용하여 getType() 리턴 값을 사람으로 수정합니다. 그리고 jisoo 객체를 이용하여 호출한 결과 사람이 나옵니다.
결론을 내리면, 프로토타입 객체는 새로운 객체가 생성되기 위한 원형이 되는 객체입니다. 같은 원형으로 생성된 객체가 공통으로 참조하는 공간입니다. 프로토타입 객체의 멤버를 읽는 경우에는 객체 또는 함수의 prototype 속성을 통해 접근할 수 있습니다. 하지만 추가, 수정, 삭제는 함수의 prototype 속성을 통해 접근해야 합니다.
3. 프로토타입이란?
JavaScript에서 기본 데이터 타입을 제외한 모든 것이 객체입니다. 객체가 만들어지기 위해서는 자신을 만드는 데 사용된 원형인 프로토타입 객체를 이용하여 객체를 만듭니다. 이때 만들어진 객체 안에 __proto__ (비표준) 속성이 자신을 만들어낸 원형을 의미하는 프로토타입 객체를 참조하는 숨겨진 링크가 있습니다. 이 숨겨진 링크를 프로토타입이라고 정의합니다.
function Person(){}
var joon = new Person();
[소스 6]
위 그림 6 joon 객체의 멤버인 __proto__ (비표준) 속성이 프로토타입 객체를 가리키는 숨은 링크가 프로토타입이라고 합니다. 프로토타입을 크게 두 가지로 해석된다 했습니다. 함수의 멤버인 prototype 속성은 프로토타입 객체를 참조하는 속성입니다. 그리고 함수와 new 연산자가 만나 생성한 객체의 프로토타입 객체를 지정해주는 역할을 합니다. 객체 안의 __proto__(비표준) 속성은 자신을 만들어낸 원형인 프로토타입 객체를 참조하는 숨겨진 링크로써 프로토타입을 의미합니다.
JavaScript에서는 숨겨진 링크가 있어 프로토타입 객체 멤버에 접근할 수 있습니다. 그래서 이 프로토타입 링크를 사용자가 정의한 객체에 링크가 참조되도록 설정하면 코드의 재사용과 객체 지향적인 프로그래밍을 할 수 있습니다.
4. 코드의 재사용
코드의 재사용 하면 떠오르는 단어는 바로 상속입니다. 클래스라는 개념이 있는 Java에서는 중복된 코드를 상속받아 코드 재활용을 할 수 있습니다. 하지만 JavaScript에서는 클래스가 없는, 프로토타입 기반 언어입니다. 그래서 프로토타입을 이용하여 코드 재사용을 할 수 있습니다.
이 방법에도 크게 두 가지로 분류할 수 있습니다. classical 방식과 prototypal 방식이 있습니다. classical 방식은 new 연산자를 통해 생성한 객체를 사용하여 코드를 재사용 하는 방법입니다. 마치 Java에서 객체를 생성하는 방법과 유사하여 classical 방식이라고 합니다. prototypal 방식은 리터럴 또는 Object.create()를 이용하여 객체를 생성하고 확장해 가는 방식입니다. 두 가지 방법 중 JavaScript에서는 prototypal 방식을 더 선호합니다. 그 이유는 classical 방식보다 간결하게 구현할 수 있기 때문입니다. 밑의 예제 1 ~ 4번까지는 classical 방식의 코드 재사용 방법이고, 5번은 prototypal 방식인 Object.create()를 사용하여 코드의 재사용을 보여줍니다.
(1) 기본 방법
부모에 해당하는 함수를 이용하여 객체를 생성합니다. 자식에 해당하는 함수의 prototype 속성을 부모 함수를 이용하여 생성한 객체를 참조하는 방법입니다.
function Person(name) {
this.name = name || "혁준";
}
Person.prototype.getName = function(){
return this.name;
};
function Korean(name){}
Korean.prototype = new Person();
var kor1 = new Korean();
console.log(kor1.getName()); // 혁준
var kor2 = new Korean("지수");
console.log(kor2.getName()); // 혁준
[소스 7]
위 소스 7을 보면 부모에 해당하는 함수는 Person입니다. 10라인에서 자식 함수인 Korean 함수 안의 prototype 속성을 부모 함수로 생성된 객체로 바꿨습니다. 이제 Korean 함수와 new 연산자를 이용하여 생성된 kor 객체의 __proto__속성이 부모 함수를 이용하여 생성된 객체를 참조합니다. 이 객체가 Korean 함수를 이용하여 생성된 모든 객체의 프로토타입 객체가 됩니다. kor1에는 name과 getName() 이라는 속성이 없지만, 부모에 해당하는 프로토타입객체에 name이 있습니다. 이 프로토타입객체의 부모에 getName()을 가지고 있어 kor1에서 사용할 수 있습니다. 이 방법에도 단점이 있습니다. 부모 객체의 속성과 부모 객체의 프로토타입 속성을 모두 물려받게 됩니다. 대부분의 경우 객체 자신의 속성은 특정 인스턴스에 한정되어 재사용할 수 없어 필요가 없습니다. 또한, 자식 객체를 생성할 때 인자를 넘겨도 부모 객체를 생성할 때 인자를 넘겨주지 못합니다. 그림 7 소스 하단 두 번째 줄에서 kor2 객체를 생성할 때 Korean 함수의 인자로 지수라고 주었습니다. 객체를 생성한 후 getName()을 호출하면 지수라고 출력될 거 같지만, 부모 생성자에 인자를 넘겨주지 않았기 때문에 name에는 default 값인 혁준이 들어있습니다. 객체를 생성할 때마다 부모의 함수를 호출할 수도 있습니다. 하지만 매우 비효율적입니다. 그래서 다음 방법은 이 방법의 문제점을 해결하는 방법을 알아보겠습니다.
(2) 생성자 빌려 쓰기
이 방법은 기본 방법의 문제점인 자식 함수에서 받은 인자를 부모 함수로 인자를 전달하지 못했던 부분을 해결합니다. 부모 함수의 this에 자식 객체를 바인딩하는 방식입니다.
function Person(name) {
this.name = name || "혁준";
}
Person.prototype.getName = function(){
return this.name;
};
function Korean(name){
Person.apply(this, arguments);
}
var kor1 = new Korean("지수");
console.log(kor1.name); // 지수
[소스 8]
위 소스 8 10라인을 보면 Korean 함수 내부에서 apply 함수를 이용합니다. 부모객체인 Person 함수 영역의 this를 Korean 함수 안의 this로 바인딩합니다. 이것은 부모의 속성을 자식 함수 안에 모두 복사합니다. 객체를 생성하고, name을 출력합니다. 객체를 생성할 때 넘겨준 인자를 출력하는 것을 볼 수 있습니다. 기본 방법에서는 부모객체의 멤버를 참조를 통해 물려받았습니다. 하지만 생성자 빌려 쓰기는 부모객체 멤버를 복사하여 자신의 것으로 만들어 버린다는 차이점이 있습니다. 하지만 이 방법은 부모객체의 this로 된 멤버들만 물려받게 되는 단점이 있습니다. 그래서 부모객체의 프로토타입 객체의 멤버들을 물려받지 못합니다. 위 그림 8 그림을 보시면 kor1 객체에서 부모객체의 프로토타입 객체에 대한 링크가 없다는 것을 볼 수 있습니다.
(3) 생성자 빌려 쓰고 프로토타입 지정해주기
이 방법은 방법 1과 방법 2 문제점들을 보완하면서 Java에서 예상할 수 있는 동작 방식과 유사합니다.
function Person(name) {
this.name = name || "혁준"; }
Person.prototype.getName = function(){
return this.name;
};
function Korean(name){
Person.apply(this, arguments);
}
Korean.prototype = new Person();
var kor1 = new Korean("지수");
console.log(kor1.getName()); // 지수
[소스 9]
위 소스 9 9라인에서 부모 함수 this를 자식 함수 this로 바인딩합니다. 11라인에서 자식 함수 prototype 속성을 부모 함수를 사용하여 생성된 객체로 지정했습니다. 부모객체 속성에 대한 참조를 가지는 것이 아닌 복사본을 통해 내 것으로 만듭니다. 동시에 부모객체의 프로토타입 객체에 대한 링크도 참조됩니다. 부모객체의 프로토타입 객체 멤버도 사용할 수 있습니다. 그림 7과 비교했을 때 kor1 객체에 name 멤버가 없는 반면 그림 9에서는 name 멤버를 가지고 있는 것을 확인할 수 있습니다. 그림 8과 비교했을 때는 프로토타입 링크가 부모 함수로 생성한 객체에 대해 참조도 하고 있습니다. 그리고 부모 객체의 프로토타입 객체도 링크로 연결된 것을 볼 수 있습니다. 이 방법에도 문제점이 있습니다. 부모 생성자를 두 번 호출합니다. 생성자 빌려 쓰기 방법과 달리 getName()은 제대로 상속되었지만, name에 대해서는 kor1 객체와 부모 함수를 이용하여 생성한 객체에도 있는 것을 볼 수 있습니다.
(4) 프로토타입공유
이번 방법은 부모 생성자를 한 번도 호출하지 않으면서 프로토타입 객체를 공유하는 방법입니다.
function Person(name) {
this.name = name || "혁준";
}
Person.prototype.getName = function(){
return this.name;
};
function Korean(name){
this.name = name;
}
Korean.prototype = Person.prototype;
var kor1 = new Korean("지수");
console.log(kor1.getName()); // 지수
[소스 10]
위 소스 10 12라인에서 자식 함수의 prototype 속성을 부모 함수의 prototype 속성이 참조하는 객체로 설정했습니다. 자식 함수를 통해 생성된 객체는 부모 함수를 통해 생성된 객체를 거치지 않고 부모 함수의 프로토타입 객체를 부모로 지정하여 객체를 생성합니다. 부모 함수의 내용을 상속받지 못하므로 상속받으려는 부분을 부모 함수의 프로토타입 객체에 작성해야 사용자가 원하는 결과를 얻게 됩니다. 그림 9와 비교했을 때 중간에 부모 함수로 생성한 객체가 없고 부모 함수의 프로토타입 객체로 링크가 참조되는 것을 볼 수 있습니다.
(5) prototypal한 방식의 재사용
이 방법은 Object.create()를 사용하여 객체를 생성과 동시에 프로토타입객체를 지정합니다. 이 함수는 첫 번째 매개변수는 부모객체로 사용할 객체를 넘겨주고, 두 번째 매개변수는 선택적 매개변수로써 반환되는 자식객체의 속성에 추가되는 부분입니다. 이 함수를 사용함으로 써 객체 생성과 동시에 부모객체를 지정하여 코드의 재활용을 간단하게 구현할 수 있습니다.
var person = {
type : "인간",
getType : function(){
return this.type;
},
getName : function(){
return this.name;
}
};
var joon = Object.create(person);
joon.name = "혁준";
console.log(joon.getType()); // 인간
console.log(joon.getName()); // 혁준
[소스 11]
위 소스 1라인에서 부모 객체에 해당하는 person을 객체 리터럴 방식으로 생성했습니다. 그리고 11라인에서 자식 객체 joon은 Object.create() 함수를 이용하여 첫 번째 매개변수로 person을 넘겨받아 joon 객체를 생성하였습니다. 한 줄로 객체를 생성함과 동시에 부모객체의 속성도 모두 물려받았습니다. 위의 1 ~ 4번에 해당하는 classical 방식보다 간단하면서 여러 가지 상황을 생각할 필요도 없습니다. JavaScript에서는 new 연산자와 함수를 통해 생성한 객체를 사용하는 classical 방식보다 prototypal 방식을 더 선호합니다.
5. 마치며
지금까지 JavaScript 프로토타입에 대해 정리했습니다. 처음 프로토타입을 공부할 땐, 자바 OOP 관점에서 접근하여 혼란스러웠습니다. 하지만 함수의 내부구조부터 차근차근 접근하였더니 쉽게 이해할 수 있었습니다. 그리고 코드의 재사용 방식에 대해 공부하였던 것도 JavaScript 언어 자체를 이해하는 데 많은 도움이 되었습니다. 이 글이 JavaScript를 처음 접하거나 프로토타입에 대해 공부하는 분들에게 많은 도움이 되었으면 합니다.