[Adobe AIR] HTMLLoader에 로드되는 HTML문서에 trace 기능 추가하기

2010/01/23 16:08

Adobe AIR에는 Webkit 브라우져 엔진이 내장되어 있어 IE나 FireFox와 같은 브라우져 기능을 만들 수 있다. 단순히 그런 기능만 제공하는 거라면 흥미가 떨어지겠지만 중요한 점은 로드되는 컨텐츠에 DOM형태로 접근할 수 있어 실제로 Element를 추가하거나 뺄 수 있고 함수까지 재정의도 가능하다. 이러한 작업을 Javascript에 대한 지식이 있다면 쉽게 할 수 있기 때문에 이것을 이용한 다양하고 재미있는 시도를 해볼 수 있다.

개발환경은 Flash Builder 라고 가정하겠다. 물론 Flash IDE에서 해도 무방하다.


AIR에서 DOM 접근의 예

AIR에서 DOM에 접근해보는 재미있는 예를 들어보겠다.

Javascript에는 alert() 함수가 존재한다. 알다시피 경고창 띄워주는 함수이다. 이 함수를 AIR에서 커스터마이징할 수 있다. 가령, 원래기능인 경고창을 띄워주지 않고 넘겨준 문자열 정보를 Flash Builder의 Console창에 출력하도록 하는 것이다. 이 기능을 구현해보자.

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=EUC-KR">
<title>Test</title>
<style type="text/css">
	body { background-color: #aaaaaa; }
</style>
</head>
<body>
	<input type="button" value="alert( 'Hello' )" onclick="alert('Hello')"> 
	<input type="button" value="alert( 'I', 'love', 'you' )" onclick="alert('I', 'love', 'you')">
</body>
</html>

위 HTML코드는 Button을 누르면 alert() 함수로 경고창을 띄워준다.

AIR에서 이 HTML 문서를 로드하는데 HTMLLoader 클래스로 할 수 있다. 이 클래스는 Webkit 엔진을 한번 감싸서 AIR개발자가 손쉽게 사용할 수 있도록 한다. HTMLoader의 load()를 이용해 HTML문서를 로드하고 alert를 아래 코드와 같이 재정의 할 수 있다.

var _html:HTMLLoader = new HTMLLoader();
_html.addEventListener( Event.HTML_DOM_INITIALIZE, __ON_HTML_DOM_INITIALIZE );
_html.load( new URLRequest("test.html") );
stage.addChild(htmlLoader);

private function __ON_HTML_DOM_INITIALIZE($e:Event):void {
	//javascript의 alert 함수를 actionscript에서 재정의했다.
	_html.window.alert = function():void {
		trace( arguments );
	}			
}

참고 1. 위 코드는 완벽한 코드는 아니다. 중요부분만 떼어내었다.
참고 2. _html.window.alert 내에 재정의 된 함수에 arguments가 뭐냐고 물어보신다면... 
Adobe문서를 참고하자. 이런것이 가능한 것은 Actionscript와 Javascript가 모두 ECMAScript 규약을 따르고 있기 때문이다.

Event.HTML_DOM_INITIALIZE 이벤트는 HTML DOM이 만들어졌음을 알려주는 이벤트로 이때부터 HTML의 DOM에 접근할 수 있다. 하지만 모든 DOM이 만들어지는 것을 의미하지 않으며 가장 기본적인 구조만 접근이 가능해진다. 완벽한 DOM구조에 접근하려면 Event.COMPLETE 이벤트를 핸들링해야한다. 

위 ActionScript 코드의 __ON_HTML_DOM_INITIALIZE() 이벤트 핸들러에서 브레이크 포인트를 찍어서  디버깅 모드로 실행해보면 다음과 같은 DOM구조를 볼 수 있다.


위처럼 내부적으로 정의된 __HTMLScriptObject 객체가 HTMLLoader의 window속성으로 참조되어 있는 것을 확인할 수 있고 alert도 여기에 포함되어 있는 것을 확인할 수 있을 것이다. 저기에 있는 HTML 내부 속성을 모두 개발자가 커스터마이징 할 수 있다는데 매우 흥미를 느끼지 않는가?

Flash Builder에서 디버깅 모드로 실행해보면 다음 처럼 윈도우가 나오고 test.html이 HTMLLoader에 로드되는 것을 볼 수 있다. 



위 버튼을 누르면 누를때마다 alert창이 뜨지않고 다음처럼  Flash Builder의 Console창에 출력된다.


굳이 Event.COMPLETE가 아니라 Event.HTML_DOM_INITIALIZE 이벤트 발생시 alert를 재정의 하는 것은 Event.COMPLETE 이전에 Javascript가 얼마든지 실행될 수 있기 때문이다. 그러므로 어느때든지 alert가 원래기능을 수행하지 못하게 하는데 가장 적절한 시점은 바로 Event.HTML_DOM_INITIALIZE 이벤트가 발생할 때이다.


사용자 정의 Trace() 만들자.

AIR의 HTMLLoader를 통해 불려지는 HTML의 디버깅은 쉽지 않다. 왜냐하면 Flash Builder나 FireFox와 같은 디버깅 툴을 사용할 수 없기 때문이다. 이 때문에 복잡하게 만들어진 JavaScript 코드와 HTML이 섞여 있는 경우라면 어떤 경우에 문제가 발생하는지 찾는것 조차 어려워진다. 

Flash Builder에서는 trace()문을 사용하면 디버깅에 크게 도움이 된다. 비록 브레이크 포인트를 찍으면서 값을 추적할 수 없지만 trace()문 하나만으로도 많은 부분 문제를 해결하는데 도움을 준다.

그럼 JavaScript내에 강제적으로 trace()함수를 정의하고 개발자가 필요할때 HTML문서상에서 이 함수를 호출하면 AIR 애플리케이션에 trace으로 들어온 인자값을 출력해주면 어떨까? alert()를 커스터마이징 했었다는 것을 충분히 이해했다면 이 부분도 그리 어려운 것은 아니다.

private function __ON_HTML_DOM_INITIALIZE($e:Event):void {
	_html.window.trace = function():void {
		trace( arguments );
	}			
}

참고 : _html.window.trace는 로드되는 HTML DOM에 trace()함수를 정의한 것이고 함수내에 trace()는 ActionScript의 trace라는 것을 인식하자.


이미 보여준 ActionScript 코드에서 alert 재정의 대신 trace를 정의하도록 만들었다. 이미 언급한 HTML문서에서 onclick="alert()" 구문대신 onclick="trace()" 로 바꿔보자.

디버깅 모드로 실행하면 Flash Builder의 Console창에 버튼을 클릭할때마다 메시지가 보이는 것을 확인할 수 있을 것이다.

이 방법은 꽤 유용하다. 웹개발자가 만든 HTML문서가 AIR의 Webkit에도 제대로 동작하지 않는 경우도 꽤 발생한다. 이런경우 어느 지점에서 문제가 되는지 알 수 없어 alert()만 의지해서 디버깅하는 것은 웹개발자에게 너무 가혹하다. AIR개발자는 웹개발자를 위해 이 정도의 기능을 최소한으로 제공해서 웹개발자의 어려움을 덜어주어야 한다고 생각한다.


몇가지 문제점 극복하기

하지만 아직까지 문제는 있다.

1. IFrame으로 로드되는 컨텐츠는 AIR에서 정의한 trace를 직접사용할 수 없다.
이런 점을 극복하기 위해서 iFrame에 로드되는 문서에서는 parent.trace() 처럼 사용하면 어느정도 해결이 된다.

2. 기존에 사용하는 HTML문서가 이미 웹상에서 서비스되고 있을때 HTML내 실수로 trace()문을 그대로 남겨두게 되는 경우 문제가 발생한다. 왜냐하면 AIR에 로드되는 경우는 강제적으로 trace()함수를 정의하지만 일반 웹브라우져에서 로드되는 경우에는 trace()정의가 없으므로 trace()함수를 호출하면 에러를 던질 것이기 때문이다. 물론 이런 경우가 발생하지 않도록 웹개발자의 꼼꼼한 테스트가 필요하지만 그게 말처럼 쉬울까?

이처럼 웹상에서 trace()가 쓰여도 웹개발자의 실수로 인해 trace()를 지우지 못해 발생하는 문제를 해결할 방법은 있다. 먼저 아래처럼 common.js 파일을 만든다.
try {
	trace; //trace문이 정의되었는가?
} catch(e) {
	trace = function() {}; //정의되어 있지 않으면 강제로 만들어준다.
}

그 다음 서비스되고 있는 모든 HTML에 common.js를 아래처럼 import한다.

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=EUC-KR">
<title>Test</title>
<script type="text/javascript" src='common.js'></script>
<style type="text/css">
	body { background-color: #aaaaaa; }
</style>
</head>
<body>
	<input type="button" value="trace( 'Hello' )" onclick="trace('Hello')"> 
	<input type="button" value="trace( 'I', 'love', 'you' )" onclick="trace('I', 'love', 'you')">
</body>
</html>

모든 HTML문서에 common.js가 있어야 한다는 점을 기억하자. 중요하게 봐야할 것은 common.js의 동작방식이다. 이 HTML문서를 AIR에서 로드되면 DOM이 생성되자마자 trace를 정의하므로 try..catch문에서 catch()문으로 가지 않는다. 하지만 일반 웹브라우져에서 로드하면 trace가 생성되지 않아 catch()문이 실행되게 된다. 그러니 웹개발자가 trace()문을 실수로 지우지 않아도 AIR에 로드되든 일반 웹브라우져에 로드되든 문제가 발생하지 않는다.


실용적으로 만들어보자. 

AIR에 로드되는 HTML문서에서 trace()문을 이용하는 방법을 알 수 있게 되었다. 이를 좀더 실용적으로 테스트 해볼 수 있도록 만들기 위해서는 trace()의 결과가 console창에만 나오는 것이 아니라 별도의 창을 띄워서 언제든지 trace()내용을 확인할 수 있도록 하는 것이 좋다. 이것은 AIR 개발자와 웹페이지 개발자가 다른 경우 AIR개발자가 웹페이지 개발자에게 해줄 수 있는 최소한의 배려이다.

여기서는 최소한의 기능만 보여주려고 한다. 아래처럼 창 2개가 있다. 좌측창은 HTML를 로드한 지금까지 예제이고, 우측창은 trace문의 결과물을 보여주는 창이다.


이것을 구현하기 위해 ActionScript 3.0으로 만들어보자. Flash Builder에서 개발한다면 Flex 프로젝트를 생성하되 Application type은 Desktop(AIR)로 하고 Main Application file 이름을 .mxml이 아닌 .as로 끝나는 확장자를 가지도록 만들어야한다. 나는 Main.as로 만들었다.

Main.as
package {
	import flash.display.NativeWindow;
	import flash.display.Sprite;
	import flash.events.Event;

	/**
	 * 메인 애플리케이션 
	 * @author jidolstar
	 */
	public class Main extends Sprite {
		public function Main() {
			//브라우져 띄우기 
			var window:BrowserWindow = new BrowserWindow();
			window.width = stage.stageWidth;
			window.height = stage.stageHeight;
			window.load( "test.html" );
			window.activate();
		}
	}
}


BrowserWindow.as


package {
	import flash.display.NativeWindow;
	import flash.desktop.NativeApplication;	
	import flash.display.NativeWindowInitOptions;
	import flash.display.NativeWindowType;
	import flash.display.StageAlign;
	import flash.display.StageScaleMode;
	import flash.events.Event;
	import flash.html.HTMLLoader;
	import flash.net.URLRequest;

	/**
	 * Trace 테스트용 브라우저 윈도우
	 * @author jidolstar
	 */
	public class BrowserWindow extends NativeWindow {
		private var _html:HTMLLoader;
		private var _trace:TraceWindow;

		/**
		 * 생성자 
		 */
		public function BrowserWindow() {
			stage.align = StageAlign.TOP_LEFT;
			stage.scaleMode = StageScaleMode.NO_SCALE;
			
			//윈도우 옵션
			var initOptions:NativeWindowInitOptions = new NativeWindowInitOptions();
			initOptions.type = NativeWindowType.NORMAL;
			super(initOptions);
			
			//HTML 로더 
			_html = new HTMLLoader();
			_html.width = stage.stageWidth;
			_html.height = stage.stageHeight;
			_html.addEventListener(Event.HTML_DOM_INITIALIZE, function ($e:Event):void {
				//DOM에 trace 함수를 정의한다. 
				_html.window.trace = function():void {
					var str:String = "";
					for( var i:int = 0; i < arguments.length; i++ ) {
						str += arguments[i] + " ";
					}
					//결과물을 창에 출력해준다.
					_trace.addString( str );
				}
			});
			stage.addChild(_html);
			
			//Trace 결과물을 보여주는 창 
			_trace = new TraceWindow();
			_trace.width = 300;
			_trace.height = 300;
			_trace.activate();
			
			//창이 닫힐때 모든 윈도우를 닫는다.
			addEventListener(Event.CLOSING, function($e:Event):void {
				var openedWindows:Array = NativeApplication.nativeApplication.openedWindows;
				for each( var window:NativeWindow in openedWindows ) {
					window.close();
				}
			} );	
			
			title = "browser";
		}
		
		public function load($url:String):void {
			_html.load( new URLRequest( $url ) );
		}
		
	}
}


TraceWindow.as

package {
	import flash.display.NativeWindow;
	import flash.display.NativeWindowInitOptions;
	import flash.display.NativeWindowType;	
	import flash.text.TextField;
	import flash.display.StageAlign;
	import flash.display.StageScaleMode;
	
	/**
	 * Trace를 출력해주는 윈도우
	 * @author jidolstar
	 */
	public class TraceWindow extends NativeWindow {
		private var _textField:TextField;
		private var _traceText:String = "";

		public function TraceWindow() {
			stage.align = StageAlign.TOP_LEFT;
			stage.scaleMode = StageScaleMode.NO_SCALE;

			var initOptions:NativeWindowInitOptions = new NativeWindowInitOptions();
			initOptions.type = NativeWindowType.NORMAL;
			super(initOptions);

			_textField = new TextField();
			_textField.width = stage.stageWidth;
			_textField.height = stage.stageHeight;
			stage.addChild(_textField);
			
			title = "trace window";
		}

		public function addString($value:String):void {
			_textField.appendText($value + "\n");
		}
	}
}


물론 이것은 ActionScript 3.0 코드이고 이미 언급한 HTML(test.html) 및 common.js 도 함께 소스에 포함해야한다.

아래는 위 코드들을 압축한 소스이다.


예제는 단순하지만 더 응용해서 컴포넌트화 시키면 좋을 것이다.


정리하며
Adobe AIR의 장점은 크로스 OS, 기존 Flash/Flex 개발자의 접근 향상 정도만 있는 것은 아니다. Adobe는 웹과 매우 친하면서도 데스크탑 영역으로까지 확장했다. 여기서 웹과 매우 친하다는 말에 주목할 필요있다. 웹브라우져에 올라온 Flash와 AIR가 별도의 설치 없이 서로 통신할 수 있고 웹브라우져에서 AIR를 실행할 수도 있다. 또한 오늘 언급한 것처럼 dom에 대한 접근이 매우 쉬운 것도 있다. 아직까지 AIR가 성능 및 기술적 한계가 분명 존재하지만 버전업을 거듭하면서 더욱 나아지고 있다. AIR의 장점을 명확히 인식한다면 AIR만의 매력을 느낄 수 있지 않을까 생각한다.

아래 링크로부터 AIR에 관련된 정보를 얻을 수 있다.

글쓴이 : 지돌스타(http://blog.jidolstar.com/650)
저작자 표시 비영리 동일 조건 변경 허락

Adobe AIR , , , , , , , , , ,

  1. 와우 ㅋㅋ 굿~

  2. 오오! air의 html처리가 생각보다 좋은 것 같아서 관심이 갑니다..
    늘 좋은 정보 감사합니다.

  3. 아직 몇가지 한계는 있지만 분명 큰 장점이 있는 것은 분명하다고 생각합니다. 웹킷을 채용했다는 것 자체만으로도 매우 흥미롭게 다가오네요. ^^

  4. 지난번의 외부 swf의 심벌을 불러오는 것의 답변 감사가 늦었네요

    이렇게 저렇게 해 보아서 이제야 이해가 되고 하네요^^

    AIR를 책보면 하던중 중단했는데 오늘 지돌스타님 글을 보니 다시 관심이 생기네요

    차근차근 하고 싶은데 이렇게 좋은 기능이 많이 나오면 또 성급해지기도 하고 ㅋㅋㅋ

    좋은 매번 감사합니다^^

  5. ㅎㅎ 아닙니다. 별로 큰 도움도 안되었는데요. ^^
    제글로 좋은 자극받으셨다니 저로서도 기쁩니다.

  6. 언제나 보기만 하다가 오늘은 리플을- ^^;;

    HTMLLoader 괜찮은 클래스라 생각하지만서도..

    점유율이 넘 높은 것 같아서요..

    ++
    var htm:HTMLLoader;

    (어쩌고 저쩌고.. ^^)

    htm = null
    ++

    가비지 콜렉션 대상을 만들어 주어도-

    처음 실행 하지 않았을 경우 보다

    실행하고, 삭제(remove + null) 했을 경우가 1M 정도의 메모리를 잡아먹는것 같아요-

    ㅠㅠ 어렵네요..

    아참참 환절기 건강 잘 챙기시구요~ 포스팅은 항상 잘 보고 있습니당- 홧팅요~!

  7. AIR 전반적으로 메모리 점유율이 높죠. 그 부분은 AIR 2.0이 되면서 많이 개선될 것 같습니다. 그래도 높은건 사실... ^^