어제 "프로그래밍 jQuery" (인사이트. 원제 jQuery in Action)를 완독했습니다. 개인 프로젝트로 시작한 웹 사이트 구축에 필요할까 해서요. 이 책을 읽으면서 존 레식의 "프로 자바스크립트 테크닉"을 띄엄띄엄 보기도 했는데, 자바스크립트라는 언어와 자바스크립트 라이브러리에 대해 새로운 것을 깨달은 느낌입니다.
"프로그래밍 jQuery"라는 책의 최대 강점은 굉장히 알기쉽게 쓰여져 있고, 예제가 잘 구축되어 있다는 점입니다. 예제 파일들이 하나로 패키징되어 있는데, 덕분에 예제를 참고하고 공부하기가 쉽습니다. 프로그래밍을 배우는 사람들이 쉽고 잘 짜여져 있는 예제에 목마르다는 점을 감안한다면, 아주 대단한 장점이라고 할 수 있겠습니다. 거기다 설명까지 잘 되어 있으니, 금상첨화라고도 할 수 있겠죠.
그런데 이 책이 갖는 그런 장점은 jQuery라는 라이브러리가 갖는 장점 때문에 생기는 것이기도 합니다. jQuery 라이브러리는 일관된(consistent), 잘 조직되고(well-organized) 잘 짜여진(well-written) 라이브러리거든요. 사용법도 간단해서 (복잡하게 사용하는 것도 물론 가능하겠지만 말입니다) 프로그래머가 신경쓸 일이 대폭 줄어듭니다.
jQuery가 오랫만에 만나는 잘 짜인 라이브러리라고 한다면, 이 책은 그야말로 오랫만에 읽어보는, 모든 면에서 균형이 잘 잡힌 책입니다. 번역까지도요.
JavaScript는 클래스라는 개념을 제공하지 않기 때문에, 객체를 만들때 클래스의 이름을 줄 수가 없습니다. 따라서 객체를 만들때 클래스의 이름이 아니라, '함수'의 이름을 주어야 합니다.[각주:1]
다음의 코드를 봅시다.
function Foo(name) { this.name = name; }
var obj = new Foo("obj 1");
이 코드에서 new 키워드의 의미는, 객체를 하나 만들되 그 객체에 대한 생성자로 그 다음에 오는 함수 이름을 사용하라는 뜻입니다. 따라서 함수 Foo는 생성자처럼 쓰일 수도 있고, 일반 함수처럼 쓰일 수도 있습니다. 다만 일반 함수로 사용하게 되면 위의 경우 this가 컨텍스트 오브젝트를 가리키게 되므로 좀 곤란하겠죠.
따라서 위와 같이 실행하고 obj.constructor == Foo 인지를 검사해보면 true가 됩니다. constructor 프로퍼티는 해당 변수(즉, 객체)가 만들어질 때 초기화를 담당한 함수를 참조합니다.
그럼 생성자는 그렇다 치고, 객체의 멤버 함수는 어떻게 정의해야 하나요? 앞서 function도 '객체'라고 했음을 상기합시다. 모든 function 객체는, prototype이라는 프라퍼티를 가집니다. 이 프라퍼티에 속한 프라퍼티는 해당 함수를 생성자로 사용해 만들어진 모든 객체에 자동적으로 추가됩니다. JavaScript에서는 어떤 프라퍼티에 하위 프라퍼티들을 동적으로 아무때나 추가할 수 있기 때문에, prototype 프라퍼티를 사용하면 멤버 함수들을 손쉽게 추가할 수 있습니다.
function Foo() { this.name = "baby"; this.value = "cry"; }
사실 여기까지만 하면 이해도 그다지 어렵지 않은 편입니다만... ㅎㅎ 더글러스 크록포드라는 몹쓸(?) 양반이 써놓은 기사를 읽어보면, 문제가 좀 더 복잡해집니다. http://javascript.crockford.com/private.html 이 기사는 JavaScript 객체에서 private 멤버 변수와 private 메소드를 어떻게 정의하고 사용할 수 있는지를 다루고 있습니다.
위의 글에 적힌 내용을 이해하려면, JavaScript에서의 유효범위(scope)의 개념을 숙고해 볼 필요가 있습니다. JavaScript에서 유효범위는 함수 내부에서 선언되었느냐, 아니면 함수 밖에서 선언되었느냐로 갈립니다. '선언'의 기준은 var 키워드가 사용되었느냐 사용되지 않았느냐로 갈리구요.
가령 다음과 같이 선언된 변수가 있다고 합시다.
var foo = 3;
함수 안에서 선언되지 않았으므로, 전역 변수입니다. 다음의 경우는 어떤가요?
function Foo() { var bar = 3; }
함수 안에서 선언되었으므로 bar는 지역 변수입니다. 키워드 var가 사용되지 않았다면 전역 변수가 되었을 겁니다. 그럼 이 지역 변수는 Foo() 안에서만 사용 가능한 변수가 되겠군요. 그런데, 아마 다들 아시겠습니다만 JavaScript에는 클로저(closure)라는 개념이 있습니다.
What this means is that an inner function always has access to the vars and parameters of its outer function, even after the outer function has returned.
이 클로저라는 개념 덕에, 함수의 지역 변수를 마치 private 객체 변수인 것 처럼 사용할 수 있습니다. 함수 안에서 정의되는 함수는 외부 함수가 종료되더라도 외부 함수 안에서 정의된 변수나 외부 함수에 전달된 인자들을 그대로 사용할 수 있기 때문입니다.
크록포드 선생님이 작성한 다음 예제를 한번 보시죠.
function Container(param) {
function dec() {
if (secret > 0) {
secret -= 1;
return true;
} else {
return false;
}
}
this.member = param;
var secret = 3;
var that = this;
this.service = function () {
if (dec()) {
return that.member;
} else {
return null;
}
};
}
desc라는 함수를 안쪽에 하나 정의했습니다. 이 함수의 유효 범위는 역시 Container() 안으로 제한됩니다. 그러므로 외부에서는 사용할 수 없는 private 함수가 됩니다. 이 함수는 secret이라는 지역 변수를 사용하도록 구현되어 있습니다. desc()는 함수 안에서 정의된 함수이므로, 클로저가 갖는 성질을 고스란히 갖습니다. 따라서 이 함수는 Container()가 종료된 뒤에도 그 함수가 사용하던 지역 변수들을 그대로 사용할 수 있습니다. '클로저를 통해 지역 변수의 lifetime을 연장하여 private 변수로 기능하도록 만들었다'고 이해하면 되겠습니다.
그런데 desc는 그 유효범위가 Container() 안이기 떄문에, 다른 함수에서 이용하려면 public scope의 또다른 함수를 Container 안에 정의해 주어야 합니다. 위의 예제에서는 this.service에 할당한 function 객체가 그에 해당합니다. 클로저를 통해 private 변수와 메소드를 정의하고, 역시 또 클로저를 통해 그 메소드를 호출하는 릴레이 함수를 정의해주는 셈입니다. 새로 정의된 이 릴레이 함수 덕에, desc()의 lifetime 또한 연장될 수 있습니다. 크록포드는 이런 릴레이 함수를 privileged 메소드라고 부르고 있습니다. 이 메소드는 다음과 같이 호출하면 됩니다.
myContainer.service()
이렇게 호출하면 처음 세 번까지는 'abc'가 반환되고, 그 다음부터는 null이 반환됩니다. 한가지 알아두면 좋은 것은, privileged 메소드는 일반 public method와 달리 컴파일 시점이 아닌 실행 시간에 객체에 추가된다는 점입니다.
function addGenerator( num ) { return function( toAdd ) { return num + toAdd; } }
var addFive = addGeneraator( 5 );
alert( addFive(4) == 9 );
함수가 객체이기 떄문에 함수 안에서 리턴하는 것도 가능하고, 함수를 인자로 받는 것도 가능합니다. 위의 코드는 그런 특성을 사용하고 있습니다.
함수가 객체라는 것은, 함수의 코드가 '데이터'라는 것, 그리고 그 '데이터'를 조작하기 위한 메소드가 해당 객체 내에 바인딩 되어 있을 것이라는 점을 시사합니다. 가령 다음 코드를 보면... (역시 존 레식의 "프로 자바스크립트 테크닉"에서 빌려왔습니다.)
function changeColor( color ) { this.style.color = color; }
...
var main = document.getElementById( 'main' );
changeColor.call( main, "black" );
여기서 call은 changeColor를 호출하되 그 함수가 바인딩될 객체(즉, this가 가리키는 객체. 다른 말로 하면 컨텍스트)를 main으로 하여 호출해 줍니다. C++식으로 이야기하자면 이 call 이라는 함수는 changeColor 객체의 멤버 함수(즉, 메소드)가 되는 셈입니다. 이런 특성이 JavaScript라는 프로그래밍 언어가 갖는 언어적 단순성이 단순히 단순함에 머무르지 않고 다양한 방식으로 풍부해질 수 있도록 만듭니다.
함수와 객체를 구별하는 것이 의미가 없게 하는 이런 특성은 비단 JavaScript 뿐 아니라 Erlang같은 함수형 프로그래밍 언에서도 쉽게 찾아볼 수 있습니다.
가령 얼랑 셸에서 다음과 같이 했다고 해 봅시다.
1> Double = fun(X) -> 2 * X end.
이렇게 하면 X를 인자로 받아 그 값을 두 배 하는 함수가 만들어지고, 그 함수를 Double이라는 이름으로 사용할 수 있게 됩니다. 이렇게 만들어진 함수를 lists:map(F, L)에 넘기면, 리스트에 담긴 모든 원소의 값을 2 배한 새로운 리스트를 만들수 있습니다.
2> L = [1, 2, 3, 4]. 3> list:map( Double, L ). [2, 4, 6, 8].
만일 위의 addGenerator같은 역할을 하는 function을 Erlang으로 만들고자 한다면, 다음과 같이 하면 될 것입니다.
4> AddGenerator = fun(Num) -> ( fun(X) -> NUm + X end ) end.
이렇게까지 하고 나면 이제
5> AddFive = AddGenerator(5).
하고 나서
6> AddFive(4). 9.
처럼 동일한 결과를 얻을 수 있게 되죠.
JavaScript는 '객체지향적인 프로그래밍 언어'이고 '객체'라는 개념을 이처럼 '함수'에까지 확장하고 있기 때문에 C++같은 좀 오래된 객체지향 언어보다는 좀 더 공격적인 객체지향 프로그래밍을 할 수가 있습니다.
댓글을 달아 주세요
저도 rails 개발 때문에 prototype을 주로 쓰지만, jQuery를 더 좋아합니다. 번역도 잘되었다고하니
2008/10/07 13:33 [ ADDR : EDIT/ DEL : REPLY ]한권 사서 봐야겠군요. 소개 감사합니다.
별말씀을... 열공하세요~
2008/10/07 14:05 [ ADDR : EDIT/ DEL ]