 |  |
|
난이도 : 초급 Edward J Pring, Senior Software Engineer, IBM
2007 년 3 월 06 일
Part 1에서는 Finite State Machine을 사용하여 단순한 웹 위젯에 페이드인/페이드아웃(fade in/out) 작동 같은 복잡한 작동을 디자인하는 방법을 설명했습니다. 이번 글에서는 JavaScript로 그러한 작동을 구현하는 방법과 Associative Array와 함수 클로저(closure)를 활용하는 방법을 설명합니다. 결과 코드는 간결하고, 로직은 투명하며, 그리고 애니메이션은 부하가 많은 프로세서에서도 완벽하게 작동할 것입니다.
Part 3에서는 대중적인 웹 브라우저에서 구현 작업을 할 때 발생하는 실질적인 문제를 다룰 예정입니다.
Finite State Machine은 네트워크 어댑터와 컴파일러 같은 이벤트 중심 프로그램의 복잡한 작동을 디자인 및 구현하는 구성 원리이다. 이제, 프로그램화 할 수 있는 웹 브라우저들이 차세대 애플리케이션 환경에 대하여 이벤트 중심 환경이라는 새로운 장을 열었다. 브라우저 기반의 애플리케이션들은 Ajax에 의해서 대중화 되었고, 점점 더 복잡해지고 있다. 이와 같은 상황에서 'Finite State Machine'의 원리와 구조를 활용하면 큰 도움이 된다.
Part 1에서는 대중적인 웹 브라우저에서 내장된 구현 보다 정교한 작동을 갖고 있는 웹 페이지용 툴팁 위젯을 설명했다. FadingTooltip 위젯은 커서가 HTML 엘리먼트 위에서 잠시 머문 후에 희미하게 나타나고 툴팁을 잠시 디스플레이 한 후에 서서히 사라진다. 이 툴팁은 서서히 나타났다 사라지듯이 커서가 움직이는 방향을 따라가고, HTML 엘리먼트에서 커서가 떠날 때 방향을 바꾸거나 뒤로 움직인다. 이렇게 작동하기 위해서는 FadingTooltip 위젯이 다양한 이벤트에 반응해야 하고, 어떤 경우에는 특정 이벤트에 대한 반응은 이전 이벤트에 기반할 수도 있다.
개발자들은 Finite State Machine 패턴을 사용하여 이와 같이 이벤트 중심 프로그램들을 구성한다. 아래의 그림 1은 이러한 디자인 원리를 적용하여 원하는 작동을 정의하는 상태 테이블을 도식화 한 것이다.
그림 1. FadingTooltip 위젯용 상태 테이블
이 상태 테이블의 행과 칼럼에는 위젯이 반응할 이벤트 이름과 위젯이 이벤트들 사이에서 기다릴 상태 이름들이 붙어있다. 테이블의 각 셀은 특정 상태에서 특정 이벤트가 발생할 때 위젯이 취할 액션들을 지정한다. 테이블 셀은 액션이 취해진 후에 위젯이 이동할 다음 상태를 지정하거나 위젯이 같은 상태로 머문다는 것을 지정한다. 빈 테이블 셀은 해당 상태에서는 발생하지 않는 이벤트를 나타낸다. 또한 Part 1에서는 이벤트들 사이에서 위젯이 기억해야 할 상태 변수 리스트를 컴파일 하여 다른 셀에서 관련 액션들을 실행할 수 있도록 하였다.
 | | Part 3에서는 대중적인 브라우저에서 이러한 구현들을 테스트하고, 현실적인 문제들을 다루도록 하겠다. |
|
Part 1에서 JavaScript가 Finite State Machine의 실행 환경으로서 JavaScript가 적합하다는 것을 증명했고, 디자인 단계와 관련된 몇 가지 기능들을 설명했다. 이 글에서는, 여러분이 디자인 한 것들을 JavaScript로 변환하고, 이 언어의 고급 기능들을 활용하고, 구현 시 약간의 트릭을 가미하는 방법을 설명하겠다.
디자인을 JavaScript로 변환하기
Part 1에서 Finite State Machine을 디자인 했기 때문에 이제 JavaScript로 FadingTooltip 위젯을 구현해 보자. 이제부터는 쉬운 디자인 관념화 단계에서 현실적인 실행 환경을 지나야 한다.
모두들 Netscape Navigator, Microsoft® Internet Explorer®, Opera, Mozilla Firefox 등 대중적인 브라우저를 사용하고 있을 것이다. 이렇게 제한된 실행 환경은 두통을 유발한다. 다양한 브라우저의 마우스와 타이머 이벤트를 JavaScript 프로그램과 연결하는 문제가 여러분을 힘들게 할 것이다. 함수 클로저(function closures)라고 하는 JavaScript의 고급 특성이 여러분의 구원자가 될 것이다. 또한 associative arrays라는 또 하나의 JavaScript 고급 기능을 사용하여 상태 테이블을 코드로 변환할 수 있다. HTML 엘리먼트를 사용하여 툴팁을 생성 및 스타일링 하고, 여기에 텍스트와 이미지를 채우고, 커서 주변에 배치하여 나타났다가 사라지는 효과를 주면서 커서의 움직임을 따라가도록 만들 것이다.
하지만 먼저 객체 지향 개발에 대한 근본적인 사상 안에 존재하는 객체가 필요하다. 즉, ‘여러분이 구현하고자 하는 모든 것을 포함한 객체’가 필요한 것이다. 이것부터 시작해 보자.
모든 것을 포함하고 있는 객체
FadingTooltip 위젯은 웹 디자이너들이 단순히 HTML 페이지로 JavaScript 코드를 컷&페이스트(cut&paste)하는 것 보다 복잡한 프로그래밍 노력을 필요로 한다. 소프트웨어 엔지니어들은 위젯의 변수와 메소드를 객체로 그룹핑 하는 것에 익숙하다. JavaScript 객체 모델은 Java™와 C++ 프로그래밍에게는 약간 이상해 보이지만 말이다. JavaScript 객체는 여러분의 필요에 매우 적합하다. 변수와 메소드를 객체로 그룹핑 할 수 있고, 각 툴팁용 데이터의 인스턴스를 생성한다. 객체 인스턴스는 공통 코드를 공유하고 독립적으로 실행된다.
JavaScript에서, 객체의 컨스트럭터(constructor는 단순한 함수이다. 이 함수의 이름이 객체의 이름이다. 여러분의 위젯은 어떤 HTML 엘리먼트가 연결되고, 툴팁에 어떤 콘텐트가 디스플레이 될 것인지를 알아서 이들을 컨스트럭터에 대한 인자로서 지정하고 객체에 저장해야 한다. (또한 툴팁의 작동과 모양과 관련한 매개변수들을 설정해야 하고, 이것에 대한 인자도 지정해야 한다. 이 부분은 이 글 후반에 사용할 것이다.) 변수들은 타입이 없기 때문에(untyped), 객체 컨스트럭터는 Listing 1의 코드로 시작한다.
Listing 1. FadingTooltip 객체 컨스트럭터용 JavaScript 코드
function FadingTooltip(htmlElement, tooltipContent, parameters) {
this.htmlElement = htmlElement; // save pointer to HTML element whose mouse events
// are hooked to this object
this.tooltipContent = tooltipContent; // save text and HTML tags for the tooltip's
// HTML Division element
...
|
JavaScript에서, 변수 또는 메소드가 될 수 있는 프로퍼티(properties) 객체를 객체에 추가할 수 있고, 나중에 여기에 값을 할당할 수 있다. 컨스트럭터는 this.htmlElement와 this.tooltipContent 프로퍼티에 대해 수행할 것이기 때문이다.
JavaScript에서, 프로토타입(prototype) 객체는 객체의 새로운 인스턴스를 만드는 템플릿이다. 이것은 객체의 초기 프로퍼티와 초기 값을 정의한다. Part 1의 상태 변수로 객체 프로토타입을 시작할 수 있다. (Listing 2)
Listing 2. FadingTooltip 객체 프로토타입용 JavaScript 코드
FadingTooltip.prototype = {
currentState: null, // current state of finite state machine (one of the state
// names in the table below)
currentTimer: null, // returned by setTimeout, non-null if timer is running
currentTicker: null, // returned by setInterval, non-null if ticker is running
currentOpacity: 0.0, // current opacity of tooltip, between 0.0 and 1.0
tooltipDivision: null, // pointer to HTML division element when tooltip is visible
lastCursorX: 0, // cursor x-position at most recent mouse event
lastCursorY: 0, // cursor y-position at most recent mouse event
...
|
객체 프로토타입은 상태 테이블, 액션, 매개변수들 같은 Finite State Machine과 관련한 거의 모든 것을 정의하기에 좋은 장소이다. 커서 이벤트를 연결하는 것으로 객체 컨스트럭터를 완료하고, 이 글의 나머지 부분은 객체 프로토타입에 대해 설명하도록 하겠다.
커서 이벤트 연결하기
Part 1의 디자인 단계 섹션에서 설명했듯이, 커서가 HTML 엘리먼트로 와서, 그 안에서 움직이고, HTML 엘리먼트에서 떠날 때, 브라우저는 이벤트를 JavaScript로 보낼 수 있다. 이러한 이벤트들에는 이벤트 유형과 페이지 내의 커서의 현재 위치 같은 유용한 정보가 포함된다. 브라우저는 미리 등록되었던 함수들을 호출하여 이벤트를 전달한다. 이러한 함수들이 등록되는 상세한 방법과 인자들이 전달되는 방법은 브라우저 마다 다양하다. Finite State Machine이 커서 이벤트들을 모든 대중적인 브라우저에서 연결할 수 있도록 하려면 다양한 이벤트 모델을 구현해야 한다. 다행히도, 각 이벤트 모델용 코드는 매우 간결하다 할 수 있다만, 아무리 간결한 코드라도 내재된 복잡성은 있다.
Mozilla Firefox, Opera, 최신 버전의 Netscape Navigator는 World Wide Web Consortium (W3C)에서 제안한 표준화 된 이벤트 모델을 지원한다. 이것은 이벤트 함수를 쉽게 등록(비등록)하고 다중 등록된 함수들간 연결 고리들은 브라우저에서 핸들되기 때문에 가장 많이 선택되고 있다. HTML 엘리먼트의 addEventListener 메소드를 호출하여 커서 이벤트를 연결하면서, 이벤트가 HTML 엘리먼트에서 발생할 때 호출될 이벤트 유형과 함수를 전달한다. (Listing 3)
Listing 3. 커서 이벤트를 연결하는 JavaScript
function FadingTooltip(htmlElement, tooltipContent, parameters) {
...
htmlElement.fadingTooltip = this;
if (htmlElement.addEventListener) { // for FF and NS and Opera
htmlElement.addEventListener(
'mouseover',
function(event) { this.fadingTooltip.handleEvent(event); },
false);
htmlElement.addEventListener(
'mousemove',
function(event) { this.fadingTooltip.handleEvent(event); },
false);
htmlElement.addEventListener(
'mouseout',
function(event) { this.fadingTooltip.handleEvent(event); },
false);
}
...
|
addEventListener 호출의 두 번째 인자는 익명 함수(anonymous functions,)인데, 이것은 이름을 갖고 있지 않다. JavaScript의 다른 함수들 내의 함수를 정의하는 첫 번째 기회이지만, 마지막은 아니기 때문에 지금 사용하는 것이 좋다. JavaScript 코드의 아무데나 function 함수를 사용하여 익명 함수를 정의한다. 이것은 함수에 대한 포인터를 리턴하는데, 다른 레퍼런스 값처럼 사용될 수 있다. FadingTooltip 위젯에서 함수 포인터를 인자로서 다른 함수들에 전달하고, null 여부를 테스트 하고, 변수를 할당하고, 객체 메소드로서 정의할 수 있다.
addEventListener 메소드로 전달된 익명 함수들은 많은 것을 수행하지 않는 것처럼 보인다. 커서 이벤트가 발생하면, 브라우저는 이들을 호출하면서, FadingTooltip 객체의 handleEvent 메소드로 전달할 곳에 event 객체를 전달한다. 브라우저의 이벤트 객체에는 이벤트 유형과 커서 위치가 포함되기 때문에 handleEvent 메소드는 위젯이 반응해야 할 모든 커서 이벤트들을 핸들 할 수 있다.
이러한 단순한 익명 함수들은 중요하면서도 섬세한 태스크를 수행한다. W3C 이벤트 모델에서, HTML 엘리먼트의 addEventListener 메소드로 등록된 함수들은 그 엘리먼트의 메소드가 되기 때문에, 브라우저가 이들을 호출하면 빌트인 this 변수가 그 HTML 엘리먼트를 가리킨다. 하지만, handleEvent 메소드는 상태 값을 포함하고 있는 FadingTooltip 객체에 대한 포인터를 필요로 한다. 이를 수행하는 한 가지 방법은 fadingTooltip 프로퍼티를 FadingTooltip 객체를 가리키는 HTML 엘리먼트에 추가하고, 객체의 handleEvent 메소드를 호출하는 것이다. 이로써 handleEvent 메소드가 실행될 때 this가 FadingTooltip 객체를 가리킬 수 있다.
Internet Explorer에서 커서 이벤트 연결하기
Microsoft Internet Explorer는 현재 W3C 이벤트 모델을 지원하지 않지만 고유의 비슷한 이벤트 모델을 제공한다. 차이점은 다음과 같다.
- 이벤트 유형이 약간 다르다.
- 등록된 함수들이 HTML 엘리먼트의 메소드가 되지 않는다.
- 이벤트 객체들이 글로벌 윈도우 객체에 남겨진다.
HTML 엘리먼트의 attachEvent 메소드를 호출함으로써 이벤트를 연결하면서 약간 다른 이벤트 유형과 함수들을 전달할 수 있다. (Listing 4)
이때 함수 정의로 변수들을 인클로즈(enclose)하기 위해 함수 클로저를 사용한다.
Listing 4. Internet Explorer에서 커서 이벤트를 연결하는 JavaScript 코드
function FadingTooltip(htmlElement, tooltipContent, parameters) {
...
else if (htmlElement.attachEvent) { // for MSIE
htmlElement.attachEvent(
'onmouseover',
function() { htmlElement.fadingTooltip.handleEvent(window.event); } );
htmlElement.attachEvent(
'onmousemove',
function() { htmlElement.fadingTooltip.handleEvent(window.event); } );
htmlElement.attachEvent(
'onmouseout',
function() { htmlElement.fadingTooltip.handleEvent(window.event); } );
}
...
|
HTML 엘리먼트의 attachEvent 메소드로 등록된 함수들은 그 엘리먼트의 메소드가 되지 않는다. 커서 이벤트가 발생하면 브라우저는 이들을 호출할 것이지만 빌트인 this 변수는 HTML 엘리먼트가 아니라 글로벌 window 객체를 가리키기 때문에 함수는 HTML 엘리먼트에 저장된 포인터에서 FadingTooltip 객체를 배치할 수 없다.
다행히도, 익명 함수 정의는 객체 컨스트럭터의 htmlElement 인자의 어휘 범위 내에 있다. 익명 함수 정의에서 htmlElement 변수를 사용함으로써 이것을 그러한 함수들로 인클로즈(eclose)할 수 있다. 이를 함수 클로저(function closure)라고 한다. 함수가 또 다른 함수 내에서 정의될 때 내부 함수는 외부 함수의 로컬 변수들을 사용하고 JavaScript는 그러한 변수들을 내부 함수의 정의로 정의한다. 나중에 외부 함수가 리턴된 후에 내부 함수가 호출될 때 외부 함수의 로컬 변수들을 여전히 사용할 수 있다.
여기에서, JavaScript는 컨스트럭터가 리턴한 후에 htmlElement 변수의 값을 보유하기 때문에 다른 브라우저에서 호출될 때 익명 함수에서 사용될 수 있다. 이로써 HTML 엘리먼트를 배치할 수 있고 브라우저의 도움 없이 FadingTooltip 객체에 대한 포인터를 따라갈 수 있다.
함수 클로저는 JavaScript 언어의 기능이기 때문에 W3C 이벤트 모델을 사용하는 브라우저에서 잘 작동한다. 빌트인 this 변수를 사용하는 대신, 컨스트럭터의 htmlElement 인자의 값을 이전 섹션에서 정의된 익명 함수로 인클로즈 하는데 사용할 수 있다.
구 브라우저에서 커서 이벤트 연결하기
W3C나 Internet Explorer 이벤트 모델을 지원하지 않는 구 브라우저의 경우, 초기 버전의 Netscape Navigator에서 제공된 원래의 이벤트 모델을 사용하여 이벤트를 연결해야 한다. 이것은 모든 대중적인 브라우저에서 지원되며 웹 페이지 애니메이션 관련 웹 디자이너들 사이에서 사용되지만 다중 이벤트 핸들러를 통해 연결되지 않기 때문에 복잡한 애플리케이션은 구현할 수 없다. 고유의 이벤트 함수 정의에서 이전에 정의된 이벤트 함수에 대한 포인터들을 인클로즈 하고 handleEvent 메소드를 호출한 후에 이들을 호출한다. (Listing 5)
Listing 5. 구 브라우저에서 커서 이벤트를 연결하는 JavaScript 코드
function FadingTooltip(htmlElement, tooltipContent, parameters) {
...
else { // for older browsers
var self = this;
var previousOnmouseover = htmlElement.onmouseover;
htmlElement.onmouseover = function(event) {
self.handleEvent(event ? event : window.event);
if (previousOnmouseover) {
htmlElement.previousHandler = previousOnmouseover;
htmlElement.previousHandler(event ? event : window.event);
}
};
... and similarly for 'onmousemove' and 'onmouseout' ...
}
}
|
이러한 방식은 불완전하다. 다른 위젯이 이미 등록했던 같은 이벤트에 등록할 수 있고 이들을 연결할 수 있지만, 연결 포인터들이 이들에 액세스 할 수 없기 때문에 다른 이벤트 함수들을 등록 해제 할 수 없다.
다양성을 위해서, 이 코드는 FadingTooltip 객체를 가리키는 컨스트럭터의 this 변수들을 self라고 하는 로컬 변수로 복사하고 self 포인터를 사용하여 익명 함수 정의의 FadingTooltip 객체를 배치한다. 이는 익명 함수 정의에 FadingTooltip 객체에 대한 포인터를 인클로즈 하기 때문에 HTML 엘리먼트에 대한 포인터를 제공하는 브라우저에 의존하지 않고, HTML 엘리먼트의 FadingTooltip 객체에 포인터를 저장할 필요 없이 브라우저가 이들을 호출할 때 직접 배치할 수 있다.
W3C와 Microsoft 이벤트 모델에서 정의된 익명 함수로 FadingTooltip 객체에 대한 포인터를 인클로즈 할 수 있다. 이로써 객체들에 대한 포인터를 HTML 엘리먼트로 저장할 필요가 없고, 모든 이벤트 모델에 HTML 엘리먼트를 배치하는데 같은 기술을 사용할 수 있다. 소스 코드의 컨스트럭터도 이와 같은 방식으로 코딩된다.
대중적인 브라우저들에 커서 이벤트를 연결하는 것으로 객체 컨스트럭터를 완료했고 객체 프로토타입에 대해 알아보자.
타이머 설정과 타이머 이벤트 연결하기
FadingTooltip 컨스트럭터를 완료했고 프로토타입에 대해 알아보자. JavaScript에서 객체 프로토타입에는 메소드와 변수가 포함될 수 있다. 메소드는 함수를 가리키는 단순한 변수이다. 타이머를 시작 및 취소하는 범용 메소드로 시작해 보자.
Part 1의 디자인 단계 섹션에서 JavaScript가 두 가지 유형의 타이머, 1회성(one-shot) 타임아웃과 반복 timetick을 제공하고, Finite State Machine은 이 두 가지 모두를 필요로 한다고 설명했다. setTimeout 또는 setInterval 함수를 호출하고 시간 값(밀리초)과 timeout이 종료되거나 timetick이 발생할 때 호출되는 함수를 전달한다. 이들은 나중에 타이머를 취소하는 clearTimeout 또는 clearInterval로 전달할 수 있는 불투명(opaque) 레퍼런스를 리턴한다.
브라우저는 timeout 값이 경과하거나 반복적으로 timetick 인터벌이 발생할 때 setTimeout과 setInterval 함수에 대한 인자로서 전달되었던 타이머 이벤트 함수들을 호출할 것이다. 이러한 timeout과 timetick 함수들은 객체의 메소드가 된다. 브라우저가 이들을 호출하면 this 변수는 글로벌 윈도우 객체를 가리킨다. 브라우저는 타이머 이벤트에 대한 정보를 이러한 함수들로 전달하지 않는다.
커서 이벤트로 많은 훈련을 했다면, 타이머 이벤트들을 연결하기는 쉽다. 타이머를 설정하면 상태 변수들을 포함하고 있는 FadingTooltip 객체를 가리키는 빌트인 this 변수를 setTimeout과 setInterval 함수 호출의 범위 내에 있는 self라는 로컬 변수로 복사한다. self 변수를 사용하는 익명 함수를 정의하고 setTimeout과 setInterval 함수를 인자로서 전달한다. 이것은 함수 정의로 self 변수를 인클로즈 하기 때문에 브라우저가 함수를 호출할 때도 사용할 수 있다. (Listing 6)
Listing 6. 타이머를 설정하고 타이머 이벤트를 연결하는 JavaScript 코드
FadingTooltip.prototype = {
...
startTimer: function(timeout) {
var self = this;
this.currentTimer =
setTimeout( function() { self.handleEvent( { type: 'timeout' } ); },
timeout);
},
startTicker: function(interval) {
var self = this;
this.currentTicker =
setInterval( function() { self.handleEvent( { type: 'timetick' } ); },
interval);
},
...
|
타이머 이벤트 함수는 커서 이벤트 함수 보다 더 많은 일을 하는 것은 아니다. timeout 또는 timetick 같은 이벤트 유형만 포함하는 타이머 이벤트 객체를 만들고 이를 커서 이벤트를 핸들하는 같은 handleEvent 메소드로 전달한다.
액션/트랜지션 테이블 만들기
JavaScript에서, 객체 프로토타입에는 어레이(배열:array)과 기타 객체, 변수와 메소드 같은 데이터 구조들이 포함된다. 일반 엘리먼트는 정수로 색인되는 반면, Associative Array의 엘리먼트는 숫자 대신 이름으로 인덱싱 된다. JavaScript에서, Associative Array와 객체들은 같은 데이터에 액세스 하는 다른 신택스일 뿐이다. 객체 프로퍼티는 Associative Array 엘리먼트로서 액세스 될 수 있다. (Listing 7)
Listing 7. Associative Array 엘리먼트로서 객체 프로퍼티에 액세스 하는 JavaScript 코드
if ( htmlElement.fadingTooltip == htmlElement["fadingTooltip"] ) ... // always true
|
함수의 이차원 Associative Array로서 상태 테이블 테이블을 구현함으로써 이를 활용할 수 있다. 어레이의 인덱스로서 상태 이름과 이벤트 이름을 직접 사용하게 될 것이다. 이 어레이의 비어있지 않은 셀들은 유틸리티 메소드(타이머를 시작 또는 취소)를 호출함으로써 이벤트에 대한 액션을 취하는 익명 함수들을 가리키고, 다음 상태로 리턴한다. handleEvent 메소드의 코어는 Listing 8 같은 어레이 신택스를 사용하여 액션/트랜지션 함수들을 호출한다.
Listing 8. Associative Array에 저장된 익명 함수를 호출하는 JavaScript 코드
var nextState = this.actionTransitionFunctions[this.currentState][event.type](event);
|
handleEvent 메소드는 Associative Array로서 actionTransitionFunctions 테이블에 액세스 하면서, 현재 상태와 이벤트 유형을 사용하고 호출할 함수를 선택한다. 이벤트 객체를 인자로서 함수로 전달한다. 이 함수는 필요한 모든 액션을 취할 것이고 다음 상태의 이름을 리턴한다.
Associative Array는 객체이기 때문에, handleEvent 메소드가 어레이 신택스를 사용하여 여기에 액세스 하더라도, 객체 신택스를 사용하여 actionTransitionFunctions 테이블을 정의할 수 있다. 예를 들어, Inactive 상태에서 유일하게 기대되는 이벤트는 mouseover이기 때문에, 이 상황을 핸들하기 위한 함수를 정의할 수 있다. (Listing 9)
Listing 9. 객체 프로퍼티로서 익명 함수를 저장하는 JavaScript 코드
FadingTooltip.prototype = {
...
initialState: 'Inactive',
actionTransitionFunctions: {
Inactive: {
mouseover: function(event) {
this.cancelTimer();
this.saveCursorPosition(event);
this.startTimer(this.pauseTime*1000);
return 'Pause';
}
},
...
|
FadingTooltip 객체용 프로토타입에는 actionTransitionFunctions 프로퍼티가 포함되고, 이것의 값은 또 다른 객체이다. Inactive라는 프로퍼티가 포함되고 이것의 값은 또 하나의 객체이다. 여기에는 mouseover라는 단 하나의 프로퍼티가 포함되는데 해당 프로퍼티의 값은 함수이다. Inactive 상태에서 mouseover 이벤트가 발생하면 handleEvent 메소드는 이 함수를 호출할 것이다. Event라는 인자를 기다리고, 세 개의 유틸리티 함수들을 호출함으로써 세 개의 액션을 취하고 다음 상태의 이름으로서 Pause를 리턴한다. 이 액션에는 커서 위치를 저장하는 것도 포함되는데, 어떤 브라우저는 마우스 이벤트 객체들에 저장하고, 타이머를 시작하며, 이것의 타임아웃 값은 pauseTime (초 단위로 지정됨. startTimer 메소드에 의해 요청될 때 밀리초로 변환됨.)
위젯은 Pause 상태에서 세 개의 다른 이벤트에 반응해야 할 것이다. mousemove,
mouseout, timeout 이벤트들이 바로 그것이다. 이러한 이벤트 유형에 대한 프로퍼티를 갖고 있는 actionTransitionFunctions 테이블에 Pause 객체를 정의할 것이다. (그림 10)
Listing 10. Pause 상태에서 커서 이벤트에 반응하는 함수
FadingTooltip.prototype = {
...
actionTransitionFunctions: {
...
Pause: {
mousemove: function(event) {
return this.doActionTransition('Inactive', 'mouseover', event);
},
mouseout: function(event) {
this.cancelTimer();
return 'Inactive';
},
timeout: function(event) {
this.cancelTimer();
this.createTooltip();
this.startTicker(1000/this.fadeRate);
return 'FadeIn';
}
},
...
|
mousemove 이벤트가 Pause 상태에서 발생할 때, handleEvent 메소드는 doActionTransition 메소드를 호출하는 함수를 호출하고 이 event 인자를 전달하고 리턴된 무엇이든 리턴할 것이다. doActionTransition 메소드는 어레이 인덱스로서 처음 두 개의 인자들을 사용하여 actionTransitionFunctions 테이블에 액세스 하고 (handleEvent 메소드와 비슷함), 세 번째 인자를 거기서 찾은 함수에 전달한다. mouseout 이벤트가 발생하면 코드는 이전에 시작되었던 타이머를 취소하는 함수를 호출하고 이를 Inactive 상태로 전환한다.
timeout 이벤트가 발생하면 실행 중인 어떤 타이머를 캔슬하고, 초기 불투명도 0으로 툴팁을 만들고 티커(ticker)를 시작하고 FadeIn 상태로 전환한다.
actionTransitionFunctions 테이블의 또 다른 함수 예제로서, FadeIn 상태에서 timetick 이벤트를 핸들하는 함수를 정의하겠다. (Listing 11)
Listing 11. FadeIn 상태에서 타이머 이벤트에 반응하는 함수
FadingTooltip.prototype = {
...
actionTransitionFunctions: {
...
FadeIn: {
...
timetick: function(event) {
this.fadeTooltip(+this.tooltipOpacity/(this.fadeinTime*this.fadeRate));
if (this.currentOpacity>=this.tooltipOpacity) {
this.cancelTicker();
this.startTimer(this.displayTime*1000);
return 'Display';
}
return this.CurrentState;
}
},
....
|
FadeIn 상태에서 timetick 이벤트가 발생하면 handleEvent 메소드는 툴팁의 불투명도를 약간 증가시키는 함수를 호출한다. 페이드인(fade in) 동안(초 단위로 지정됨) 불투명도의 비율은 0부터 증가하고(초 단위) 최대 불투명도(0.0과 1.0 사이) 모두 매개변수들이다. 이 함수는 현재 상태를 리턴하면서, 툴팁의 불투명도가 최대 불투명도 매개변수에 다다를 때까지 Finite State Machine FadeIn 상태로 둔다. 그런 다음, 티커를 취소하고 타이머를 시작하여 툴팁을 디스플레이 하고 Display 상태로 옮겨간다.
actionTransitionFunctions 테이블의 나머지 함수들도 비슷한 방식으로 정의된다. 전체 소스 코드를 보고 이것을 그림 1과 비교해 보라.
이벤트 핸들러 구현하기
우리가 자주 언급했던 handleEvent 메소드는 구현이 평범하다. (Listing 12)
Listing 12. 이벤트 핸들러용 JavaScript 코드
FadingTooltip.prototype = {
...
handleEvent: function(event) {
var actionTransitionFunction =
this.actionTransitionFunctions[this.currentState][event.type];
if (!actionTransitionFunction)
actionTransitionFunction = this.unexpectedEvent;
var nextState = actionTransitionFunction.call(this, event);
if (!this.actionTransitionFunctions[nextState])
nextState = this.undefinedState(nextState);
this.currentState = nextState;
},
...
|
actionTransitionFunctions 테이블에 실제로 액세스 하는 방법은 이전 섹션에서 설명한 것과는 다르다. 이 메소드는 현재 상태와 Associative Array 인덱스로서 이벤트 유형을 사용하여 actionTransitionFunctions 테이블에서 호출할 함수를 선택한다. 하지만, 이 메소드는 선택된 함수에 대한 포인터를 로컬 변수로 복사하고, 이를 직접 호출하기 보다는 함수 객체의 call 메소드를 사용하여 함수를 호출한다. 함수 객체에는 변수가 할당될 수 있기 때문에 다른 값인 것처럼 이를 수행한다. 빌트인 this 변수가 함수가 실행되는 동안 FadingTooltip 객체를 가리켜야 하기 때문에 이를 반드시 수행해야 한다. 여러분이 실제로 어레이 인덱스를 사용하여 actionTransitionFunctions에서 직접 함수를 호출했다면, this 변수는 테이블을 가리킬 것이다. 이 함수의 call 메소드는 this 변수를 첫 번째 인자로 설정하고 함수를 호출하여 나머지 인자들을 전달할 것이다.
actionTransitionFunctions 테이블을 보면 상대적으로 빈약 하다는 것을 느낄 것이다. 각 상태에서 기대하는 이벤트에 대한 함수를 정의했고 다른 모든 셀들은 비워두었다. handleEvent 메소드는 unexpectedEvent 메소드를 호출함으로써 예상치 못한 이벤트를 핸들할 것이다. 또는 액션/트랜지션 함수가 유효 상태가 아닌 값을 리턴하면 undefinedState 메소드를 호출할 것이다. 이러한 메소드들은 실행 타이머를 취소하고, 하나가 생성되었다면 툴팁을 삭제하고, Finite State Machine을 초기 상태로 리턴한다. Listing 13은 하나의 메소드를 보여주고 있다. 다른 것은 거의 동일하다.
Listing 13. 예상치 못한 이벤트 핸들러
FadingTooltip.prototype = {
...
unexpectedEvent: function(event) {
this.cancelTimer();
this.cancelTicker();
this.deleteTooltip();
alert('FadingTooltip received unexpected event ' + event.type +
' in state ' + this.currentState);
return this.initialState;
},
...
|
이러한 메소드들은 에러를 설명하는 경고 다이얼로그를 디스플레이 할 것이고, 일부 사용자들은 코드 작성자에게 문제를 보고할 것이다.
툴팁 디스플레이
이제 지체 말고 툴팁을 구현해 보자.
timeout 이벤트가 Pause 상태에서 발생하면 커서 주번에 툴팁이 나타나게 하고 싶지만, 브라우저는 커서 위치를 타이머 이벤트로 전달하지 않는다. 다행히도, 브라우저는 커서 위치를 커서 이벤트로 전달하기 때문에 커서 이벤트가 발생하면 saveCursorPosition 메소드를 호출하여 상태 변수에 이것을 저장한다. (Listing 14)
Listing 14. 커서 위치 저장
FadingTooltip.prototype = {
...
saveCursorPosition: function(event) {
this.lastCursorX = event.clientX;
this.lastCursorY = event.clientY;
},
...
|
이 툴팁은 텍스트, 이미지, 마크업 등 무엇이든 포함하고 있는 HTML 엘리먼트로서 tooltipContent 인자에 있는 컨스트럭터로 전달된다. createTooltip 메소드는 Listing 15에 나와있다.
Listing 15. 툴팁을 포함하고 있는 JavaScript 코드
FadingTooltip.prototype = {
...
createTooltip: function() {
this.tooltipDivision = document.createElement('div');
this.tooltipDivision.innerHTML = this.tooltipContent;
if (this.tooltipClass) {
this.tooltipDivision.className = this.tooltipClass;
} else {
this.tooltipDivision.style.minWidth = '25px';
this.tooltipDivision.style.maxWidth = '350px';
this.tooltipDivision.style.height = 'auto';
this.tooltipDivision.style.border = 'thin solid black';
this.tooltipDivision.style.padding = '5px';
this.tooltipDivision.style.backgroundColor = 'yellow';
}
this.tooltipDivision.style.position = 'absolute';
this.tooltipDivision.style.zIndex = 101;
this.tooltipDivision.style.left = this.lastCursorX + this.tooltipOffsetX;
this.tooltipDivision.style.top = this.lastCursorY + this.tooltipOffsetY;
this.currentOpacity = this.tooltipDivision.style.opacity = 0;
document.body.appendChild(this.tooltipDivision);
},
...
|
CSS 클래스 이름이 매개변수로서 지정되면, 이것은 HTML 엘리먼트의 모습에 적용한다. 그렇지 않으면, 기본 스타일링을 사용할 수도 있다. 이 툴팁 작동의 일부 측면들은 위치와 불투명도 같은 모양에 의존하기 때문에 스타일시트에서 지정된 이러한 프로퍼티들과 관련된 무엇이든 오버라이드 할 것이다. HTML 엘리먼트는 절대 죄표를 사용하여 페이지에 배치되고 마지막에 저장된 커서 위치 근처에서 시작된다. 초기 불투명도는 0이고, 거의 투명한 상태라고 보면 된다.
timetick 이벤트가 FadeIn 또는 FadeOut 상태에서 발생할 때, fadeTooltip 메소드가 호출되어 툴팁의 불투명도를 증가 또는 줄이면서, 0과 최대 불투명도 매개변수 사이에 범위를 유지한다. (Listing 16)
Listing 16. 희미해지는 툴팁을 나타내는 JavaScript 코드
FadingTooltip.prototype = {
...
fadeTooltip: function(opacityDelta) {
this.currentOpacity += opacityDelta;
if (this.currentOpacity<0)
this.currentOpacity = 0;
if (this.currentOpacity>this.tooltipOpacity)
this.currentOpacity = this.tooltipOpacity;
this.tooltipDivision.style.opacity = this.currentOpacity;
},
...
|
액션/트랜지션 함수들 역시 툴팁을 움직이고 삭제하는 유틸리티 메소드를 필요로 한다. 구현은 단순하고 소스 파일 주석에 자세히 설명되어 있다.
본문에서 가끔 언급했지만, 구현을 완성하기 전에 매개변수들을 정의해야 한다. 이들은 객체 프로토타입의 프로퍼티이지만 상태 변수와는 달리 디폴트 값을 갖고 있다. (Listing 17)
Listing 17. 객체 프로토타입에 매개변수 정의하기
FadingTooltip.prototype = {
...
tooltipClass: null, // name of a CSS style to apply to the tooltip, or
// 'null' for default style
tooltipOpacity: 0.8, // maximum opacity of tooltip, between 0.0 and 1.0
// (after fade-in, before fade-out)
tooltipOffsetX: 10, // horizontal offset from cursor to upper-left
// corner of tooltip
tooltipOffsetY: 10, // vertical offset from cursor to upper-left
// corner of tooltip
fadeRate: 24, // animation rate for fade-in and fade-out, in
// steps per second
pauseTime: 0.5, // how long the cursor must pause over HTML
// element before fade-in starts, in seconds
displayTime: 10, // how long to display tooltip (after fade-in,
// before fade-out), in seconds
fadeinTime: 1, // how long fade-in animation will take, in seconds
fadeoutTime: 3, // how long fade-out animation will take, in seconds
...
};
|
객체 컨스트럭터의 parameters 인자는 이러한 프로퍼티들의 디폴트 값을 오버라이드 할 수 있는 JavaScript Object Notation (JSON)으로 코딩된 객체이다. (Listing 18)
Listing 18. 객체 컨스트럭터에 매개변수 초기화 하기
function FadingTooltip(htmlElement, tooltipContent, parameters) {
...
for (parameter in parameters) {
if (typeof(this[parameter])!='undefined')
this[parameter] = parameters[parameter];
}
...
};
|
이 컨스트럭터는 parameters 인자에 각 프로퍼티를 검사한다. 프로퍼티가 프로토타입에 존재하면 이것의 값은 매개변수의 디폴트 값을 오버라이드 한다. 프로토타입은 객체이기 때문에, 이 또한 Associative Array도 된다. 여기에서도 객체 표기법을 사용하여 매개변수를 정의하고, 어레이 표기법을 사용하여 여기에 액세스 한다.
FadingTooltip 구현이 완성되었다. 컨스트럭터와 프로토타입에 대해서는 실제 소스 코드를 다운로드 하기 바란다.
성능
구현을 테스트하기 전에 성능에 대해 짚어보자.
브라우저는 JavaScript 프로그램들을 동기식으로 실행한다. 연결되어 있는 이벤트가 발생하면, 브라우저는 이벤트 핸들러를 호출하고 다음 이벤트를 진행하기 전에 이벤트 핸들러를 기다린다. 이벤트 핸들러가 리턴되기 전에 더 많은 이벤트가 발생하면 브라우저는 이벤트 핸들러가 리턴될 때까지 이들을 일렬로 정렬하고 순서대로, 한번에 하나씩 동기식으로 처리한다. 이벤트 핸들러가 너무 오래 걸리면 연결되지 않았던 이벤트에 대한 응답은 지연된다. 사용자는 프로그램이 느려진다고 생각하거나 브라우저가 잘 작동하지 못한다고 생각할 것이다.
이벤트 핸들러를 가능한 한 짧게 유지하는 것이 중요하지만 밀접하게 공간을 차지한 타이머 이벤트로 애니메이션을 시뮬레이트 하는 프로그램에서도 마찬가지로 중요하다. timetick 이벤트 핸들러가 티커 간격보다 더 오래 걸리면, timetick 이벤트는 브라우저의 이벤트 큐에 있는 것들에 겹쳐 놓기 때문에 프로세서가 포화되면 브라우저는 응답하지 않는다.
예를 들어, 기본 애니메이션 비율을 초 당 24단계로 설정했다면 timetick 이벤트 핸들러는 최대 40초 이내 브라우저로 리턴되기 전에 필요한 모든 것을 수행해야 한다. 현대적인 워크스테이션에서, 이것은 많은 프로세싱을 수행하기에 충분한 시간이다. 하지만 가능한 프로세서 시간을 적게 유지해야 한다. 프로세서 사용량이 별로 없다면 애니메이션은 잘 작동하고 프로그램은 프로세서가 다른 액티비티를 로딩하더라도 반응성이 좋다.
여러분의 모니터의 리프레쉬 비율이 보다 부드러운 움직임을 보일 것이라는 생각 때문에 애니메이션 비율을 초당 60 또는 85로 설정해서는 안된다. 이것은 timetick 이벤트들간 시간을 약 12 밀리초로 감소시킨다. timetick 이벤트 핸들러가 이것 보다 오래된 시간을 잡고 있거나, 프로세서간 경쟁이 있을 경우, 애니메이션은 둔해지고 브라우저는 반응성이 떨어진다.
테스트 준비 구현이 완료되었으므로, 실제 브라우저에서 코드를 테스트 할 차례이다. 이 부분은 Part 3에서 설명하도록 하겠다. 개발은 반복적인 과정이다. 디자인 또는 구현 단계로 다시 거슬러 올라갈 수도 있다.
기사의 원문보기
다운로드 하십시오
참고자료 교육
-
Ajax: A New Approach to Web Applications
-
JavaScript: The Definitive Guide
(David Flanagan, published repeatedly by O'Reilly Media between 1996 and 2006) -Book
-
Standard ECMA-262: ECMAScript Language Specification (Ecma International, 1999)
-
Document Object Model (DOM) Level 2 Events Specification (W3C, 2000)
-
Gecko DOM Reference (Mozilla)
-
HTML and DHTML Reference (Microsoft)
- Chapters 21 "Protocol Representation with Finite State Models" by Andre A. S. Danthine, and 25 "Executable Representation and Validation of SNA" by Gary D. Schultz, et. al. in Computer Network Architectures and Protocols (edited by Paul E. Green, Jr., Plenum Press, 1982)
- Chapter 3.5 "Finite Automata" in Compilers: Principles, Techniques, ad Tools (Alfred V. Aho et. al., Addison-Welsley, 1986)
- Chapter 5 "Behavioral Patterns" in Design Patterns: Elements of Reusable Object-Oriented Software (Erich Gamma et. al., Addison-Welsley, 1995)
- Chapter 13.25 "TCP State Machine" in Internetworking with TCP/IP (Douglas E. Comer, Simon and Schuster Company, 1995)
- Chapter 15 "State Machines" in the Unified Modeling Language 2.0 Superstructure Specification (Object Management Group, 2004)
-
State Chart XML (SCXML): State Machine Notation for Control Abstraction (RJ Auburn et. al., W3C)
-
한국 developerWorks 웹 개발 존: 이-비지니스와 이-커머스 웹 사이트 구현에 필요한 정보와 리소스들이 들어있습니다.
-
technology bookstore
-
developerWorks technical events and webcasts
제품 및 기술 얻기
토론
필자소개  | 
|  | Edward Pring은 New York University에서 컴퓨터 공학 석사 학위를, Mathematics from Stanford University에서 학사 학위를 받았다. IBM 연구원으로서, 운영 체계, 애플리케이션, 메인프레임용 터미널 에뮬레이터, 개인용 컴퓨터의 바이러스 보호, Digital Immune System의 네트워크 자동화, 웹 서비스의 시각화 및 성능 분석 등 다양한 IBM 제품과 기술에 기여했다. |
기사에 대한 평가
|  |