페르시아 왕자를 Flash 버전으로 만든겁니다. 제가 만든게 아니라요 ㅎㅎ
우비소프트에서 개발했다고 하네요.
동작방식은 기존 페르시아 왕자 1과 비슷하지만 맵이 다릅니다.
또한 기존보다 더 어렵더군요.

개발은 어떻게 했을까요?
일반 백터 방식으로 하지 않고 순수 BitmapData만 가지고 처리하도록 만들었을 겁니다. 그말은 DisplayObject객체는 거의 사용하지 않았다는 의미입니다. 그래야 저런 그래픽이 나오겠죠.

어쨌든 한번 과거 페르시아 왕자에 푹 빠져보세요.


웹서핑하다가 재미있는 flash 게임을 발견해서 소개한다.

 

이름하야 “Kill the puppy!”

 

헉! 강아지를 죽이는 게임이다. (너무 잔인해서 19금이다. ㅋ)

이 게임은 보통 Flash 게임과 달리 웹캠을 이용한 게임이다. 해보면 알겠지만 매우 독특하면서 재미도 있다. 화면의 모션감지를 이용해서 나오는 강아지를 때려 죽이는 거다.(입에 담기도 힘듬 ㅜㅜ)

준비물은 컴퓨터와 웹캠만 있으면 된다. 강아지를 때리는 도구는 머리, 손, 발, 방망이,칼(ㅡㅡ;)어느것이든 상관없다. 어떤 것이든 사용해서 강아지를 죽이길 바란다. 헤드폰은 반드시 착용야 개 때리는 재미를 더할 수 있다. ㅋ

 

아참… 강아지 때린다고 컴퓨터 모니터를 때리면 안습이다.

 ^^;

 

정말 아이디어 죽인다. ^^

 

게임하러 가기

 

http://www.neuroproductions.be/puppy-WebcamGame/

 

 

 

ActionScript 3.0은 스크립트 언어이기 때문에 어떻게 코딩을 하느냐에 따라 퍼포먼스에 크게 영향을 줄 수 있다. 다음 글에서 최적화 코딩 기술에 대해 언급하고 있으니 참고하면 좋겠다.

 

ActionScript 3.0 and Flex optimization techniques and practices

 

글쓴이 : 지돌스타(http://jidolstar.com/blog)

이 글은 ActionScript 3.0 프로젝트에서 BlazeDS와 통신하는 환경을 배경으로 한다.  Flex MXML를 배제하고 단지 Flex에 제공되는 RPC, 리모팅 관련 클래스를 사용하고자 한다. Flex MXML를 배제하는 이유는 다음과 같다.

 

Flex SDK의 Visual 컴포넌트들은 일반 솔루션 제품을 개발하는데 매우 유용하지만 어떤 경우에는 프로그램의 용량과 퍼포먼스 향상에는 도움이 안될 수 있다. 가령, Flex SDK에서 제공하는 Button과 List만 사용한다고 가정하자. 이 컴포넌트만 사용하는데도 불구하고 Flex 컴파일러는 그와 관련된 클래스까지 함께 컴파일해서 프로그램 용량을 크게 한다. 또한 Visual 컴포넌트들은 커다란 프레임워크로 만들어져 있기 때문에 Sprite만을 이용해서 만든 프로그램보다 퍼포먼스가 떨어진다.

 

물론 Flex에서 제공하는 컴포넌트를 적극적으로 다양하게 사용할 필요가 있다면 개발 효율성 및 유지보수 측면에서 Flex 프로젝트로 개발해야한다. 하지만 겨우 컴포넌트 몇개만 가져다가 사용하는 경우에는 Flex로 개발하는 것이 오히려 프로그램의 질을 떨어뜨릴 수 있는 결과를 초래할 수 있게 된다.(Flex가 나쁘다는 것이 아니라 필요에 맞게 사용해야한다는 것을 언급하는 것임을 강조한다.)  

 

Flex 환경에서 AMF3 통신

 

다음 예를 보자.

 

아래처럼 Flex 프로젝트로 만들어 Release 버전으로 컴파일 하게 되면 262kb가 나온다. 매우 단순한 프로그램인데도 불구하고 용량이 꽤 크다. 이유는 앞서 설명했다.

 

<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute">
	<mx:RemoteObject id="ro" destination="my-dest" showBusyCursor="true">
		<mx:method
			name="getData"
			result="returnHandler(event)"
			fault="mx.controls.Alert.show(event.fault.faultString)"/>
	</mx:RemoteObject>
	<mx:Button name="load" click="load()"/>
	 <mx:Script>
	<![CDATA[
		import mx.rpc.events.ResultEvent;

		private function returnHandler(e:ResultEvent):void
		{
			trace( e.result );
		}

		private function load():void
		{
			ro.getData( 1, 2 );
		}
	]]>
	</mx:Script>
</mx:Application>

 

 

AMF3 직렬화 처리가 안된 ActionScript 3.0 프로젝트 개발

 

AMF3 통신만 원한다면 앞서 설명한 이유 때문에 굳이 Flex로 개발할 필요가 없다. ActionScript 3.0 프로젝트 환경에서도 충분히 AMF 통신이 가능하기 때문이다. 아래는 OpenAMF나 PHPAMF와 같은 서버측 프로그램을 이용할 때 ActionScript 3.0 코드이다.

 

package {
	import flash.display.Sprite;
	import flash.net.NetConnection;
	import flash.net.ObjectEncoding;
	import flash.net.Responder;
	import flash.net.registerClassAlias;

	public class AS3NetConnectionTest extends Sprite
	{
		private var nc:NetConnection;

		public function AS3NetConnectionTest()
		{
			//create a netconnection
			nc = new NetConnection();
			nc.objectEncoding = ObjectEncoding.AMF3;
			nc.connect("http://192.168.*.*/_FLEX/amf");

			//create a responder
			var responder:Responder = new Responder(result, fault);

			//make the call
			nc.call("com.jidolstar.service.MyClass.getData", responder, 2);
		}

		private function result( data:Object ):void
		{
			trace("RPC Ok");
		}

		private function fault( event:Object ):void {
			 trace("RPC Fail");
			 trace(ObjectUtil.toString(event););
		}
	}
}

 

위 코드는 Flex에 관련된 것이 없고 단순히 flash.* 패키지 기반의 순수 ActionScript 3.0 환경에서 개발한 것이다. 용량도 Release로 컴파일하면 1kb도 안된다. Flex로 개발할때와 거의 250배 이상 용량차이를 보인다.

 

 

AMF3 직렬화 처리가 된 ActionScript 3.0 프로젝트 개발

 

하지만 BlazeDS, LCDS 서버의 리모트 객체와 클라이언트 객체간에 직렬화(serialization)가 필요한 경우 위 ActionScript 코드만으로는 해결할 수 없다. 이 문제는 본인의 경우 기존 프로젝트에서 BlazeDS로 만들어진 서버측의 코드를 변경하지 않고 Flex대신 ActionScript 3.0로 개발할때 문제가 발생했다. Flex Builder에서 ActionScript 3.0 프로젝트로 하면 playerglobal.swc, flex.swc, utilites.swc만 라이브러리로 등록되어 있기 때문에 BlazeDS에서 AMF3를 통해 넘겨주는 message관련 객체나 ArrayCollection 객체등을 직렬화 처리를 할 수 없게 된다. 왜냐하면 이들 3개 swc에는 이와 관련된 클래스가 정의되어 있지 않기 때문이다.

 

BlazeDS의 message관련 객체에 대한 직렬화를 위해서는 Flex SDK에 있는 rpc.swc가 필요하다. 또 ArrayCollection 직렬화를 위해서는 framework.swc 가 필요하다. 그럼 Flex를 사용하는 것과 무엇이 다르냐라고 반문할 수 있다.  그러나 ActionScript 3.0 기반이기 때문에 framework.swc에 정의된 비주얼 컴포넌트를 전부 사용하지 않고 단지 ArrayCollection과 그와 관련된 클래스만 사용하기 때문에 이것만으로도 프로그램의 용량을 획기적으로 줄이게 된다.

 

아래에서 설명하는 대로 프로젝트 환경을 구성해보자.

 

 

1. Flex Builder 3 Professional에서 ActionScript 3.0 프로젝트를 만든다.

 

2. 프로젝트에 Flex SDK에 있는 rpc.swc, rpc_rb.swc, framework.swc, framework_rb.swc를 라이브러리로 추가한다.

 

참고로, SWC는 윈도우의 경우 C:/Program Files/Adobe/Flex Builder 3/sdks/SDK버전/frameworks/에 libs 폴더와 locale 폴더에 있다. SWC에서 _rb라고 붙은 것은 resource bundle의 약어이다. Flex는 리소스 번들을 지원해주는데 우리의 목적과는 상관없는 것이지만 컴파일을 위해서는 추가해주어야 에러가 없이 컴파일이 된다.

 

 

3. 아래처럼 컴파일러 옵션에 -locale=en_US -include-resource-bundles=collections,rpc,messaging 를 추가한다.

 

 

4. 아래 코드처럼 코딩한다. (서버측 BlazeDS 설정은 여기서 설명을 생략한다.)

 

package {
	import com.jidolstar.vo.Class1;
	import com.jidolstar.vo.Class2;

	import flash.display.Sprite;
	import flash.net.registerClassAlias;

	import mx.collections.ArrayCollection;
	import mx.collections.ArrayList;
	import mx.core.mx_internal;
	import mx.logging.targets.TraceTarget;
	import mx.messaging.ChannelSet;
	import mx.messaging.channels.AMFChannel;
	import mx.messaging.config.ConfigMap;
	import mx.messaging.config.LoaderConfig;
	import mx.messaging.events.*;
	import mx.messaging.messages.*;
	import mx.rpc.events.FaultEvent;
	import mx.rpc.events.ResultEvent;
	import mx.rpc.remoting.RemoteObject;
	import mx.utils.ObjectProxy;

	use namespace mx_internal;

	/**
	 * RemoteObject 이용
	 */
	public class AS3RemoteObjectTest extends Sprite
	{
		public function AS3RemoteObjectTest()
		{

			registerClassAlias("flex.messaging.messages.CommandMessage",CommandMessage);
			registerClassAlias("flex.messaging.messages.RemotingMessage",RemotingMessage);
			registerClassAlias("flex.messaging.messages.AcknowledgeMessage", AcknowledgeMessage);
			registerClassAlias("flex.messaging.messages.ErrorMessage",ErrorMessage);
			registerClassAlias("flex.messaging.io.ArrayList", ArrayList);
			registerClassAlias("flex.messaging.config.ConfigMap",ConfigMap);
			registerClassAlias("flex.messaging.io.ArrayCollection",ArrayCollection);
			registerClassAlias("flex.messaging.io.ObjectProxy",ObjectProxy);
			registerClassAlias("flex.messaging.messages.HTTPMessage",HTTPRequestMessage);
			registerClassAlias("flex.messaging.messages.SOAPMessage",SOAPMessage);
			registerClassAlias("flex.messaging.messages.AsyncMessage",AsyncMessage);
			registerClassAlias("flex.messaging.messages.MessagePerformanceInfo", MessagePerformanceInfo);
			registerClassAlias("DSA", AsyncMessageExt);
			registerClassAlias("DSC", CommandMessageExt);
			registerClassAlias("DSK", AcknowledgeMessageExt);

			registerClassAlias("com.jidolstar.service.domain.Class1", Class1 );
			registerClassAlias("com.jidolstar.service.domain.Class2", Class2);

			//Sets up some more descriptive tracing on the client
			var target:TraceTarget = new TraceTarget();
			target.level = 0;

			//This is needed so the Flex libraries have access to the root objects properties
			var swfURL:String = this.loaderInfo.url;
			LoaderConfig.mx_internal::_url = this.loaderInfo.url;
			LoaderConfig.mx_internal::_parameters = this.loaderInfo.parameters;

			//This is the channel definition
			//This tells RemoteObject where to go
			var amfChannel:AMFChannel = new AMFChannel( "my-amf", "http://192.168.0.17/amf" );
			amfChannel.requestTimeout = 3;
			amfChannel.connectTimeout = 3;
			amfChannel.addEventListener(ChannelFaultEvent.FAULT, handleChannelFault);
			amfChannel.addEventListener(ChannelEvent.CONNECT, handleChannelConnect);
			amfChannel.addEventListener(ChannelEvent.DISCONNECT, handleChannelDisconnect);

			var channelSet:ChannelSet = new ChannelSet();
			channelSet.addChannel( amfChannel );

			//Make sure you include the right RemoteObject at
			//import mx.rpc.remoting.RemoteObject;
			//not
			//import  mx.rpc.remoting.mxml.RemoteObject;
			var ro:RemoteObject = new RemoteObject;
			ro.destination = "mydest";
			ro.channelSet = channelSet;
			ro.addEventListener(ResultEvent.RESULT, onResult );
			ro.addEventListener(FaultEvent.FAULT, onFault );
			ro.getData( 1, "jidolstar" );
		}

		public function onResult(event:ResultEvent):void
		{
			trace("RPC Ok");
			trace(event.result);

		}

		public function onFault(event:FaultEvent):void
		{
			trace("RPC Fail");
        	trace(event.message);
		}

		public function handleChannelFault(e:ChannelFaultEvent):void {
		    trace("Channel Fault");
			trace(e);
		}		

		public function handleChannelConnect(e:ChannelEvent):void {
		    trace("Channel Connect");
			trace(e);
		}

		public function handleChannelDisconnect(e:ChannelEvent):void {
		    trace("Channel Disconnect");
			trace(e);
		}

	}
}

 

위 코드는 BlazeDS나 LCDS 환경에서 RPC기반인 RemoteObject로 AMF3 통신처리를 하는 ActionScript 3.0 프로젝트 예제이다.

 

registerClassAlias()는 BlazeDS에서 오는 AMF3 메시지 객체를 ActionScript 3.0으로 직렬화처리를 위한 것이다. 이 함수에 message관련 클래스와 ArrayCollection이 등록되어 있는 것을 확인하기 바란다. 이것으로서 ActionScript 3.0 프로젝트를 통해서도 충분히 ArrayCollection을 쓸 수 있게 된다.

 

registerClassAlias("com.jidolstar.service.domain.Class1", Class1 )는 서버측에 정의된 com.jidolstar.service.domain.Class1 객체를 ActionScript 3.0에 정의된 com.jidolstar.vo.Class1으로 직렬화해준다. Flex에서는 com.jidolstar.vo.Class1이 만들어진 Class1.as파일의 Class명 위에 [RemoteClass(alias="com.jidolstar.service.domain.Class1")] 를 정의함으로서 registerClassAlias()에서 한 것과 동일하게 처리할 수 있지만 ActionScript 3.0환경에서 개발하므로 [RemoteClass]는 사용하기에 적합하지 못하다.

이 프로그램을 Release모드로 컴파일하면 89kb였다. Flex로 개발시에 262kb이였던 것에 비해서 분명 크게 줄었다. 이는 Flex 비주얼 컴포넌트에 관련된 것이 컴파일에서 제외되었기 때문이다. 하지만 순수하게 NetConnection으로만 만들었던 것이 1kb였다는 것을 감안한다면 여전히 크기가 크다. 위 코드에서는 RemoteObject를 이용했는데 NetConnection을 이용해보면 어떨까? 다음 코드를 보자.

 

package {
	import com.jidolstar.vo.Class1;
	import com.jidolstar.vo.Class2;

	import flash.display.Sprite;
	import flash.net.NetConnection;
	import flash.net.ObjectEncoding;
	import flash.net.Responder;
	import flash.net.registerClassAlias;

	import mx.collections.ArrayCollection;
	import mx.messaging.messages.*;

	/**
	 * NetConnection이용
	 */
	public class AS3NetConnectionTest extends Sprite
	{
		public function AS3NetConnectionTest()
		{
			registerClassAlias("flex.messaging.messages.RemotingMessage",RemotingMessage);
			registerClassAlias("flex.messaging.messages.AcknowledgeMessage", AcknowledgeMessage);
			registerClassAlias("flex.messaging.messages.ErrorMessage",ErrorMessage);
			registerClassAlias("flex.messaging.io.ArrayCollection",ArrayCollection);

			registerClassAlias("com.jidolstar.service.domain.Class1", Class1  );
			registerClassAlias("com.jidolstar.service.domain.Class2", Class2  );

			//create a netconnection
			var nc:NetConnection = new NetConnection();
			nc.objectEncoding = ObjectEncoding.AMF3;
			nc.connect("http://192.168.0.17/amf");

			//create the arguments that will be sent to the method
			var methodArguments:Array = [ 1, "jidolstar" ];

			//create a remoting message
			var remotingMessage:RemotingMessage = new RemotingMessage();
			remotingMessage.operation = "getData";
			remotingMessage.body = methodArguments;

			//The values of these 2 values come from the services-config.xml which is located in the ColfFusion server
			remotingMessage.destination = "mydest";
			remotingMessage.headers = {DSEndpoint: "my-amf"};

			//create a responder
			var responder:Responder = new Responder(result, fault);

			//make the call
			nc.call(null, responder, remotingMessage);
		}

		private function result(e:AcknowledgeMessage):void
		{
			trace("RPC Ok");
			trace(e.body);
		}

		private function fault(e:ErrorMessage):void {
			 trace("RPC Fail");
			 trace(e.faultString);
		}
	}
}

 

위 프로그램은 RemoteObject를 사용할 때와 동일하게 동작한다. 하지만 더 저레벨로 만들었기 때문에 프로그램 용량은 44kb로 RemoteObject를 사용할때보다 1/2로 줄었다. Flex로 개발할때와 비교할때 거의 6배 차이가 난다. 그러나 여전히 순수하게 NetConnection만으로 만들었을 때와 용량차이가 나는데 그것은 메시지 관련 클래스와 ArrayCollection을 컴파일할때 그와 연관된 클래스들이 포함되어 컴파일 되기 때문이다. 그 중에서는 우리의 목적인 AMF3 직렬화와는 상관없는 resource bundle에 관련된 것도 포함되어 있다. 필요하다면 resource bundle부분을 제외한 라이브러리를 만들어 내는 것도 하나의 방법이 될 수 있겠다. 일단 6배 이상 프로그램 용량을 줄일 수 있다는 것에 만족하려고 한다. 만약 이 부분에 대해서 더욱 가볍게 만드신 분이 있다면 함께 공유했으면 한다.(제가 그것까지 하기에는 시간이 ^^;;)

 

위 프로그램은 단순한 예제이므로 필요할 때 클래스화 시켜서 만드는 것이 좋을 것이라 생각한다.

 

 

관련글

 

 

 

Tour de Flex가 AIR버전에 이어 웹버전도 나왔다.

 

3월 말에 나왔는데 이제 알았다.

 

웹버전은 아래 링크를 따라가면 되겠다.

 

http://www.adobe.com/devnet/flex/tourdeflex/web

 

참고로 AIR버전은 Tour de Flex component explorer에서 설치하면 되겠다.

 

매우 재미있고 유익한 프로그램이다.

 

 

웹서핑하다가 Flex로 만든 아주 간단한 그림그리기 툴을 발견했다.

 

이 그림그리기 툴은 그래픽 관련 API를 사용하고, 로컬에 저장하기 위해 Flash Player 10 FileReference API를 사용한다. 만약 Flash Player 9이하라면 이 프로그램은 정상동작하지 않는다. Flash Player 10 부터 로컬에 Flash Player에서 만들어진 리소스(바이너리 데이타, 텍스트 데이타 모두)를 사용자의 인터렉션이 있다는 조건하에 저장이나 읽기가 가능해졌다. 이 프로그램은 단순히 그림그리는 툴을 만들었다는 내용보다는 이것을 강조한 것이다. Flash Player 9 이하의 버전이라면 이것을 구현하기 위해 반드시 서버로 이미지를 저장한 다음에 그 이미지를 가져오는 단계가 필요하다.

 

100줄도 안되는 MXML코드로 구현한 것이니 내용을 참고한다면 도움이 될 것이라 생각한다.

 

(Flash Player 10 이상에서만 동작한다.)

 

 

이것을 만든 사람 블로그

http://www.jamesward.com/blog/2009/04/16/flex-paint-2/

 

소스코드

http://www.jamesward.com/demos/FlexPaint2/srcview/index.html

 

참고글

[Flash Player 10]FileReference의 변경된 보안정책과 새롭게 추가된 기능에 대한 나의 생각

 

 

 

ActionScript 3.0 환경에서는 클래스내에서 함수의 오버로딩(overloading)을 지원하지 않고 있다. 그래서 클래스 설계시 알게 모르게 어려움이 따르게 된다.  보통 함수는 method1, method2 등으로 이름을 붙여서 오버로딩이 아니더라도 해결이 가능하다. 하지만 생성자의 경우에는 클래스에 딱 하나의 함수만 가능하므로 오버로딩 기능이 절실해진다. 다행히도 완벽하지 않지만 오버로딩처럼 구축하는 것은 가능하다.

다음 예제를 보자.

 

package
{
    public class Foo
    {
        public function Foo( ...args )
        {
            if( args[ 0 ] is Date )
            {
                _init( args[ 0 ] );
            }
            else
            {
            	if( args.length == 3 )
            	{
		            if( args[ 0 ] is Number && args[ 1 ] is Number && args[ 2 ] is Number)
		            {
		                _init( new Date( args[ 0 ], args[ 1 ], args[ 2 ] ) );
		            }
		            else
		            {
            			throw new ArgumentError( "인자는 Number형이여야 합니다." );
		            }
            	}
            	else if( args.length == 6 )
            	{
            		try
            		{
			            if( args[ 0 ] is Number && args[ 1 ] is Number && args[ 2 ] is Number &&
			            	args[ 3 ] is Number && args[ 4 ] is Number && args[ 5 ] is Number)
	            		{
	            			_init( new Date( args[ 0 ], args[ 1 ], args[ 2 ], args[ 3 ], args[ 4 ], args[ 5 ] ) );
	            		}
	            		else
	            		{
	            			throw new ArgumentError( "인자는 Number형이여야 합니다." );
	            		}
	            	}
	            	catch( e:Error )
	            	{
	            		throw new ArgumentError( e.message );
	            	}

            	}
            	else
            	{
            		throw new ArgumentError( "인자수가 3개 또는 6개 여야합니다." );
            	}
			}
        }

        private function _init( date : Date ) : void
        {
            //do something
            trace( date.toString() );
        }
    }
}

 

생성자에 ...args를 받는 것을 볼 수 있다. ActionScript 3.0은 ...args를 이용해 함수에 입력되는 인자의 수 및 형태에 구애받지 않고 인자를 넘겨줄 수 있다. args는 Array이므로 args.length 속성을 이용해 인자수를 받을 수 있고 각각의 인자를 args[0]과 같은 방법으로 접근이 가능하기 때문에 형태도 체크할 수 있다. 이것이 ActionScript 3.0에서 문법적으로 안되는 Overloading을 해결하는 열쇠가 된다. 컴파일시에 문법적 문제를 해결할 수 없지만 그래도 런타임시에는 throw를 이용해 Error를 던져줄 수 있게 된다. 위 코드는 아래처럼 사용한다.

 

var bar : Foo;
bar = new Foo( new Date() );
bar = new Foo( 2007, 7, 12 );
bar = new Foo( 2007, 7, 12, 12, 3, 4 );
//bar = new Foo( "2007", "07", "12" ); //String으로 입력하여 Error발생
//bar = new Foo( 2007, 7 ); //인자수가 맞지 않아 Error발생

 

C++이나 Java등에서는 당연히 객체지향프로그래밍에서 중요하게 언급되는 Overloading이 ActionScript 3.0에서는 안되니깐 정말 답답하기 그지 없었지만 다행히 이 방법으로 해결해본다. 완벽하지 않지만 분명 오도가도 못할때 도움이 될 수 있는 팁이다. ^^;

 

 

Flash Player가 Cross OS, Cross Browser가 된다고 한다지만 유독 한글 입력 문제에 있어서 만큼은 제대로 되지 않는 경우가 있다. 가령 MS Windows 환경에서 Explorer외에 Firefox, Safari, Chrome, Opera 등에서 SWF를 브라우저에 Embed할 때 wmode를 transparent로 지정하면 TextField에 한글입력이 되지 않는다.(2009년 3월 25일 현재)

 

본인은 스타플(http://starpl.com)의 별지도에 댓글 달기 기능을 추가하면서 사용자의 OS, Browser를 판별하여 한글입력이 되지 않는 Browser의 경우 "*한글 입력이 되지 않아요!"라는 문구를 넣어야 했다.

 

 

위처럼 IE 경우엔 한글입력이 잘되므로 문구가 필요없다.

 

 

그러나 FireFox의 경우에는 한글입력이 되지 않아서 문구가 들어간다. 저 문구를 누르면 문제에 대한 안내페이지로 이동하게 되어 있다.

 

불행히도 ActionScript 3.0 라이브러리에서는 사용자가 어떤 브라우져를 이용하는지 알 수 있는 API를 제공하지 않고 있다. 그럴만도 한 것이 SWF는 브라우저에 Embed되어 작동되는 것 외에도 Flash Player 자체에서도 동작하기 때문일 것이다. (참고로 flash.system.Capabilities의 os 속성을 이용해 운영체제의 상세정보는 얻어올 수 있다.)

 

다행히도 사용자의 Browser정보를 알 수 있는 방법은 있다. 그것은 JavaScript 를 이용하는 것이다. JavaScript를 이용해만 쉽게 Browser 종류, 버전, OS 종류등을 얻어올 수 있다.

 

관련정보를 검색해본 끝에 Browser 종류, 버전, OS 종류를 범용적으로 사용할 수 있는 Javascript 코드를 발견했다. 아래 링크를 참고하자.

 

Browser detect : http://www.quirksmode.org/js/detect.html

 

사용자는 Javascript코드내에서 단지 아래처럼 사용하면 그만이다.

 

  • Browser name: BrowserDetect.browser
  • Browser version: BrowserDetect.version
  • OS name: BrowserDetect.OS

 

너무 편하다. 그럼 이것을 어떻게 ActionScript 3.0 에서 사용할 수 있다는 것인가? 결과적으로 flash.external.ExternalInterface를 이용하면 된다. ExternalInterface의 call()메소드를 이용해 JavaScript 코드를 호출하면 된다. 여기서 두가지 선택을 하게 된다.

 

  1. 위의 BrowserDetect를 js 파일로 만들어 HTML에 포함해서 ActionScript 3.0에서 ExternalInterface로 호출
  2. 위의 BrowserDetect를 커스터마이징한 JavaScript 코드를 ActionScript 3.0에 삽입해서 ExternalInterface로 호출

 

첫번째 방법은 ExternalInterface를 학습한 사람이라면 쉽게 접근할 수 있을 것이다. 하지만 AS3 코드와  JavaScript 코드가 둘로 나뉘어 관리하기가 힘들어지는 단점이 있다. 그래서 하나의 AS3에서 관리할 수 있는  두번째 방법으로 문제를 해결하고자 한다.

 

package com.starpl.utils
{
	import flash.external.ExternalInterface;

	/**
	 * Browser detect
	 * @author Yongho Ji
	 * @since 2009.03.24
	 * @see http://www.quirksmode.org/js/detect.html
	 */
	public class BrowserDetectUtil
	{
		/**
		 * Here we define javascript functions which will be inserted into the DOM
		 */
		private static const DATA_OS:String =
				"var dataOS = [" +
					"{" +
						"string: navigator.platform," +
						"subString: \"Win\"," +
						"identity: \"Windows\"" +
					"}," +
					"{" +
						"string: navigator.platform," +
						"subString: \"Mac\"," +
						"identity: \"Mac\"" +
					"}," +
					"{" +
						"string: navigator.userAgent," +
						"subString: \"iPhone\"," +
						"identity: \"iPhone/iPod\"" +
					"}," +
					"{" +
						"string: navigator.platform," +
						"subString: \"Linux\"," +
						"identity: \"Linux\"" +
					"}" +
				"];";

		private static const DATA_BROWSER:String =
				"var dataBrowser = [" +
						"{" +
							"string: navigator.userAgent," +
							"subString: \"Chrome\"," +
							"identity: \"Chrome\"" +
						"}," +
						"{" +
							"string: navigator.userAgent," +
							"subString: \"OmniWeb\"," +
							"versionSearch: \"OmniWeb/\"," +
							"identity: \"OmniWeb\"" +
						"}," +
						"{" +
							"string: navigator.vendor," +
							"subString: \"Apple\"," +
							"identity: \"Safari\"," +
							"versionSearch: \"Version\"" +
						"}," +
						"{" +
							"prop: window.opera," +
							"identity: \"Opera\"" +
						"}," +
						"{" +
							"string: navigator.vendor," +
							"subString: \"iCab\"," +
							"identity: \"iCab\"" +
						"}," +
						"{" +
							"string: navigator.vendor," +
							"subString: \"KDE\"," +
							"identity: \"Konqueror\"" +
						"}," +
						"{" +
							"string: navigator.userAgent," +
							"subString: \"Firefox\"," +
							"identity: \"Firefox\"" +
						"}," +
						"{" +
							"string: navigator.vendor," +
							"subString: \"Camino\"," +
							"identity: \"Camino\"" +
						"}," +
						"{" +
							"string: navigator.userAgent," +
							"subString: \"Netscape\"," +
							"identity: \"Netscape\"" +
						"}," +
						"{" +
							"string: navigator.userAgent," +
							"subString: \"MSIE\"," +
							"identity: \"Explorer\"," +
							"versionSearch: \"MSIE\"" +
						"}," +
						"{" +
							"string: navigator.userAgent," +
							"subString: \"Gecko\"," +
							"identity: \"Mozilla\"," +
							"versionSearch: \"rv\"" +
						"}," +
						"{" +
							"string: navigator.userAgent," +
							"subString: \"Mozilla\"," +
							"identity: \"Netscape\"," +
							"versionSearch: \"Mozilla\"" +
						"}" +
					"];";		

		private static const FUNCTION_SEARCH_BROWSER:String =
			"document.insertScript = function ()" +
			"{"	+
				"if (document.searchBrowser==null)" +
				"{" +
					"searchBrowser = function ()" +
					"{" +
						DATA_BROWSER +
						"var browser = null;" +
						"for (var i=0;i<dataBrowser.length;i++)	" +
						"{" +
							"var dataString = dataBrowser[i].string;" +
							"var dataProp = dataBrowser[i].prop;" +
							"if (dataString) " +
							"{" +
								"if (dataString.indexOf(dataBrowser[i].subString) != -1)" +
								"{" +
									"browser = dataBrowser[i].identity;" +
									"break;" +
								"}" +
							"}" +
							"else if (dataProp)" +
							"{" +
								"browser = dataBrowser[i].identity;" +
								"break;" +
							"}" +
						"}" +
						"if( browser == null )" +
						"{" +
							"browser = \"An unknown browser\";" +
						"}" +
						"return browser;" +
					"}" +
				"}" +
			"}";

		private static const FUNCTION_SEARCH_BROWSER_VERSION:String =
			"document.insertScript = function ()" +
			"{"	+
				"if (document.searchBrowserVersion==null)" +
				"{" +
					"searchBrowserVersion = function () " +
					"{" +
						DATA_BROWSER +
						"var browser;" +
						"for (var i=0;i<dataBrowser.length;i++)	" +
						"{" +
							"var browserString = dataBrowser[i].string;" +
							"var browserProp = dataBrowser[i].prop;" +
							"if (browserString) " +
							"{" +
								"if (browserString.indexOf(dataBrowser[i].subString) != -1)" +
								"{" +
									"browser = dataBrowser[i];" +
									"break;" +
								"}" +
							"}" +
							"else if (browserProp)" +
							"{" +
								"browser = dataBrowser[i];" +
								"break;" +
							"}" +
						"}" +
						"var versionSearchString = browser.versionSearch || browser.identity;" +
						"var index;" +
						"var version;" +
						"version = navigator.appVersion;" +
						"index = version.indexOf(versionSearchString);" +
						"if (index != -1) " +
						"{" +
							"return parseFloat(version.substring(index+versionSearchString.length+1));" +
						"}" +
						"version = navigator.userAgent;" +
						"index = version.indexOf(versionSearchString);" +
						"if (index != -1) " +
						"{" +
							"return parseFloat(version.substring(index+versionSearchString.length+1));" +
						"}" +
						"return \"an unknown version\";" +
					"}"+
				"}"+
			"}";	

		private static const FUNCTION_SEARCH_OS:String =
			"document.insertScript = function ()" +
			"{" +
				"if (document.searchOS==null)" +
				"{" +
					"searchOS = function ()" +
					"{" +
						DATA_OS +
						"var os = null;" +
						"for (var i=0;i<dataOS.length;i++)	" +
						"{" +
							"var dataString = dataOS[i].string;" +
							"var dataProp = dataOS[i].prop;" +
							"if (dataString) " +
							"{" +
								"if (dataString.indexOf(dataOS[i].subString) != -1)" +
								"{" +
									"os = dataOS[i].identity;" +
									"break;" +
								"}" +
							"}" +
							"else if (dataProp)" +
							"{" +
								"os = dataOS[i].identity;" +
								"break;" +
							"}" +
						"}" +
						"if( os == null )" +
						"{" +
							"os = \"An unknown OS\";" +
						"}" +
						"return os;" +
					"}"+
				"}"+
			"}";	

		private static var initFlag:Boolean = false;
		private static var _browserVersion:String = null;
		private static var _browser:String = null;
		private static var _os:String = null;

		private static function insertScript():void
		{
			if( initFlag ) return;
			if (! ExternalInterface.available)
			{
			    throw new Error("ExternalInterface is not available in this container. Internet Explorer ActiveX, Firefox, Mozilla 1.7.5 and greater, or other browsers that support NPRuntime are required.");
			}

			initFlag = true;
			ExternalInterface.call( FUNCTION_SEARCH_BROWSER );
			ExternalInterface.call( FUNCTION_SEARCH_BROWSER_VERSION );
			ExternalInterface.call( FUNCTION_SEARCH_OS );
		}

		/**
		 * browser name
		 */
		public static function get browser():String
		{
			if( _browser ) return _browser;
			insertScript();
			_browser = ExternalInterface.call( "searchBrowser" );
			return _browser;
		}

		/**
		 * browser version
		 */
		public static function get browserVersion():String
		{
			if( _browserVersion ) return _browserVersion;
			insertScript();
			_browserVersion = ExternalInterface.call( "searchBrowserVersion" );
			return _browserVersion;
		}

		/**
		 * OS name
		 */
		public static function get OS():String
		{
			if( _os ) return _os;
			insertScript();
			_os = ExternalInterface.call( "searchOS" );
			return _os;
		}

	}
}

 

 

사용법은 매우 간단하다.

 

  • Browser name: BrowserDetectUtil.browser
  • Browser version: BrowserDetectUtil.version
  • OS name: BrowserDetectUtil.OS

 

ExternalInterface를 활용하면 매우 무궁무진하게 확장이 가능해진다.

 

단, ExternalInterface를 사용할 수 있는 조건을 잘 고려해야한다. 첫번째로 ExternalInterface를 지원하는 Flash Player 버전을 확인한다. 둘째로 SWF를 Embed할때 allowScriptAccess속성과 allowNetworking 속성을 체크한다. 두번째의 경우 본인 블로그에 있는 “위젯도 마음대로 못다는 네이버 블로그” 글을 보면 되겠다.

 

참고글

 

ActionScript 3.0의 ApplicationDomain은 로드한 SWF파일 내부에 정의된 Class를 참조하는데 유용하게 쓰인다.

 

ApplicationDomain은 현재 hasDefinition(name:String):Boolean 과 getDefinition(name:String):Object 등 2개의 public 메소드를 지원한다. 이 메소드를 이용하여 ApplicationDomain내 정의된 클래스 정의 유무를 알 수 있고 그에 대한 참조를 얻을 수 있다. 다음은 사용 예이다.

 

var myInstance:DisplayObject = new
(ApplicationDomain.currentDomain.getDefinition("com.flassari.MyClass"));

 

위 코드의 경우 현재 애플리케이션의 ApplicationDomain상에 정의된 com.flassari.MyClass 클래스의 정의를 읽어와 DisplayObject 객체를 만드는 것이다. 사용하기 매우 쉽다. 그러나 클래스의 이름을 알고 있다는 전재하에 쓰일 수 있는 방법이다. 그럼 ApplicationDomain 내에 정의된 이름을 알지 못하는 클래스들의 이름을 목록을 알고 싶을 때는 어떻게 해야할까? 불행히도 ApplicationDomain 클래스는 이것을 지원해주고 있지 않다. ApplicationDomain.getAllDefinitions():Array 와 같은 메소드가 지원된다면 좋을 텐데 말이다.

 

해결방법이 있다. 바로 여기에서 소개하는 SwfClassExplorer 클래스를 이용하면 된다. 사용방법은 매우 단순하다. 하나의 static 메소드인 getClasses(bytes:ByteArray):Array 를 이용하면 된다. 아래 코드는 간단한 사용예제이다.

public function init():void {
	// Load the swf
	var urlLoader:URLLoader = new URLLoader();
	urlLoader.dataFormat = URLLoaderDataFormat.BINARY;
	urlLoader.addEventListener(Event.COMPLETE, onSwfLoaded);
	urlLoader.load(new URLRequest("pathToSwf"));
}

private function onSwfLoaded(e:Event):void {
	// Get the bytes from the swf
	var bytes:ByteArray = ByteArray(URLLoader(e.currentTarget).data);
	// And feed it to the SwfClassExplorer
	var traits:Array = SwfClassExplorer.getClasses(bytes);
	for each (var trait:Traits in traits) {
		trace(trait.toString()); // The full class name
	}
}

 

이는 매우 유용함을 알 수 있다. SWF를 Loader가 아닌 URLLoader로 로드했음을 볼 수 있다. 그리고 실제 DisplayObject 객체를 생성하기 위해 다음과 같이 코딩하면 되겠다.

 

private var _loader:Loader;

public function init():void {
	var urlLoader:URLLoader = new URLLoader();
	urlLoader.dataFormat = URLLoaderDataFormat.BINARY;
	urlLoader.addEventListener(Event.COMPLETE, onSwfLoaded);
	urlLoader.load(new URLRequest("pathToSwf"));
}

private function onSwfLoaded(e:Event):void {
	var bytes:ByteArray = ByteArray(URLLoader(e.currentTarget).data);
	var traits:Array = SwfClassExplorer.getClasses(bytes);
	for each (var trait:Traits in traits) {
		trace(trait.toString()); // The full class name
	}

	selectedClassName = traits[0];
	_loader = new Loader();
	var loaderContext:LoaderContext = new LoaderContext();
	_loader.contentLoaderInfo.addEventListener(Event.COMPLETE, onLoaderInit);
	_loader.loadBytes( bytes, loaderContext );
}

private function onLoaderInit(event:Event){
	var obj:Object;
	try {
		obj = new (_loader.contentLoaderInfo.applicationDomain.getDefinition(selectedClassName ));
	} catch (e:Error) {
		trace( "Can't instantiate class:\n" + e.toString() );
	}
	if (obj != null && obj is DisplayObject) {
		addChild( DisplayObject(obj) );
	}
}

 

결국 Binay형태로 받아온 SWF를 Loader클래스의 loadBytes()메소드를 이용해 비동기적으로 로드처리한 뒤에 Loader.contentLoaderInfo.applicationDomain의 getDefinition() 메소드를 이용해 객체를 생성하는 방식이다.

 

아쉬운 점은 여기서 loadBytes()를 이용한 비동기적 처리가 아닌 동기적 처리를 통해 클래스 참조를 얻어낼 수 있다면 더욱 좋을 것 같다.  사실 이 부분을 찾다가 이 예제를 찾아내게 된건데 매우 유용해서 포스팅 한 것이다.

 

세부적인 예제 및 소스는 아래 출처를 참고한다.

 

출처

Swf Class Explorer for AS3

소스

위 출처에서 소스를 다운받을 수 없으면 아래 링크로 다운받는다.

 

 

ActionScript 2.0 기반으로 만들어진 swf는 AVM1이며 ActionScript 3.0기반에서 작성된 것 swf는 AVM2이다. AVM2와 AVM1 간에 스크립트 통신이 직접적으로 진행되지 않고 LocalConnection만으로만 가능하다.

 

여기서 소개하는 AVM2Loader는 AVM1, AVM2 기반의 SWF를 모두 AVM2의 형태로 로드시켜 줌으로써 해당 SWF간 스크립트 통신이 가능하게 하는데 목적이 있다. 구현방법은 flash.display.Loader 클래스를 확장해 로드된 컨텐츠가 AVM1인 경우는 AVM2로 변경해준다. 정확한 의미는 본인도 잘 모른다. 시간이 허락한다면 분석해보려고 한다.

 

소개하는 자료는 TroyWorks Blog에 나온 자료임을 밝혀둔다. 직접 테스트는 안해봤다. ^^ 아래코드는 AVM2Loader이다.

package
{
	import flash.display.Loader;
	import flash.events.*;
	import flash.net.*;
	import flash.system.LoaderContext;
	import flash.utils.ByteArray;
	import flash.utils.Endian;

	/**
	 * Loads both of AVM1 and AVM2 swf as AVM2.
	 */
	public class AVM2Loader extends Loader
	{
		private var _urlLoader:URLLoader;
		private var _context:LoaderContext;

		/**
		 * loads both of AVM1 and AVM2 movie as AVM2 movie.
		 */
		override public function load(request:URLRequest,context:LoaderContext=null):void
		{
			_context=context;

			_urlLoader=new URLLoader ;
			_urlLoader.dataFormat=URLLoaderDataFormat.BINARY;
			_urlLoader.addEventListener(Event.COMPLETE,_binaryLoaded,false,0,true);
			_urlLoader.addEventListener(IOErrorEvent.IO_ERROR,_transferEvent,false,0,true);
			_urlLoader.addEventListener(ProgressEvent.PROGRESS,_transferEvent,false,0,true);
			_urlLoader.addEventListener(Event.OPEN,_transferEvent,false,0,true);
			_urlLoader.addEventListener(HTTPStatusEvent.HTTP_STATUS,_transferEvent,false,0,true);
			_urlLoader.addEventListener(SecurityErrorEvent.SECURITY_ERROR,_transferEvent,false,0,true);
			_urlLoader.load(request);
		}

		private function _binaryLoaded(e:Event):void
		{
			loadBytes(ByteArray(_urlLoader.data),_context);
			_urlLoader=null;
		}

		private function _transferEvent(e:Event):void
		{
			dispatchEvent(e);
		}

		/**
		 * loads both of AVM1 and AVM2 movie as AVM2 movie.
		 */
		override public function loadBytes(bytes:ByteArray,context:LoaderContext=null):void
		{
			//uncompress if compressed
			bytes.endian=Endian.LITTLE_ENDIAN;
			trace("loadBytes" + bytes[0] );
			if (bytes[0] == 0x43) {
				//trace(" hackA");
				//many thanks for be-interactive.org
				var compressedBytes:ByteArray=new ByteArray() ;
				compressedBytes.writeBytes(bytes,8);
				compressedBytes.uncompress();

				bytes.length=8;
				bytes.position=8;
				bytes.writeBytes(compressedBytes);
				compressedBytes.length=0;

				//flag uncompressed
				bytes[0]=0x46;
			}
			hackBytes(bytes);
			super.loadBytes(bytes,context);
		}

		/**
		 * if bytes are AVM1 movie, hack it!
		 */
		private function hackBytes(bytes:ByteArray):void
		{
			//trace(" hackB");
			if (bytes[4] < 0x09)
			{
				trace("hack 9");
				bytes[4]=0x09;
			}

			//trace(" hackC");
			//dirty dirty
			var imax:int=Math.min(bytes.length,100);
			for (var i:int=23; i < imax; i++)
			{
				if (bytes[i - 2] == 0x44 && bytes[i - 1] == 0x11)
				{
					bytes[i]=bytes[i] | 0x08;
					return;
				}
			}
		}
	}
}
아래는 AVM2Loader 테스트 소스이다.
import flash.display.DisplayObject;
import flash.display.Loader;
import flash.display.LoaderInfo;
import flash.display.Sprite;
import flash.events.Event;
import flash.events.MouseEvent;
import flash.net.URLLoader;
import flash.net.URLLoaderDataFormat;
import flash.net.URLRequest;
import flash.utils.ByteArray;
import flash.display.SimpleButton;
import AVM2Loader;

var avm1:AVM1Movie;

function testAudio(e:Event = null):void
{
	trace("testAudio");

	//var request:URLRequest = new URLRequest("video.swf");
	var request:URLRequest = new URLRequest("Flash8Movie.swf");
	var ldr:Loader;
	if (true)
	{
		trace("start AVM2Loader");
		ldr = new AVM2Loader();
	}
	else
	{
		ldr = new Loader();
	}

	ldr.contentLoaderInfo.addEventListener(Event.COMPLETE, _onLoaderComplete);
	ldr.load(request);
}

function _onLoaderComplete(event:Event):void
{
	trace(" _onLoaderComplete");
	var info:LoaderInfo = event.target as LoaderInfo;
	trace(" _onLoaderComplete " + info.content);

	if (info.content is AVM1Movie)
	{
		avm1 = AVM1Movie(info.content);
	}
	else
	{
		addChild(info.content);
		MovieClip(info.content).stop();
		var dO:DisplayObject = DisplayObject(info.content);
	}
}

stage.addEventListener(MouseEvent.CLICK, testAudio);
testAudio();
 
관련글

 

ShowSpace(http://showspace.co.kr)는 사진을 여러개 넣어서 블로그 및 게시판등에서 공유할 수 있도록 한 사진 플레이어 제공 서비스이다. 기술적으로 Adobe Flex로 구현되어 있다.

 

 

위는 ShowSpace를 이용해 사진첩을 하나 만들었다. 마우스 드래그로 사진을 볼 수 있고 확대/축소도 가능하다. 사용자 편의 성을 위해 아래 스크롤바로 사진을 스크롤링해볼 수 있다. 또한 전체화면 보기와 블로그/게시판등에 붙여넣을 수 있도록 HTML태그 복사도 가능하다. 아직 Beta라 여러가지 미흡한 점이 보이지만 몇가지만 더 개선한다면 사용자들에게 좋은 경험을 줄 수 있을 것이라 생각한다. 본인이 생각하는 ShowSpace가 좀더 개선했으면 하는 부분은 다음과 같다.

 

  1. 여러가지 스킨 적용 플레이어 스킨이 오직 하나라는 점이 아쉽다. 여러스킨으로 변경할 수 있도록 한다면 훨씬더 좋을 것 같다.
  2. 다양한 크기 적용 ShowSpace를 다양한 크기로 볼 수 있도록 한다면 다양한 형태로 쓸 수 있게 될 것이다. 가령, 블로그에 위젯으로도 쓸 수 있을 것이다. 지금은 하나의 크기로 제한되어 있는데 이렇게 되면 다양한 곳에 공유하기 어렵지 않을까?
  3. 스크롤바 라이브 드래깅 기능 향상 현재 스크롤바를 이용해 사진을 볼 수 있다. 하지만 마우스로 이동한 뒤에나 사진의 위치가 바뀐다. 뭔가 어색해보인다. 이를 라이브 드래깅을 할 수 있도록 만들면 훨씬 사용성이 향상될 것이라 생각한다.
  4. 사진보여주기 방식 선택의 편리성 향상 ShowSpace는 1줄보기, 3줄보기, 다섯줄보기, 입체한줄보기 등의 사진보기 방식을 지원하고 있다. 하지만 사진첩 설정이 따로 있어 관리하므로 약간 불편함이 느껴진다. 하나로 통합시키던가 사용자가 선택해서 다양한 형태로 보여줄 수 있게 만들면 더욱 좋을 듯하다.
  5. 외부링크시 새창뜨기가 잘 안되는 문제 ShowSpace의 사진을 클릭하면 원하는 링크로 갈 수 있다. 좋은 기능임에도 불구하고 Flash Player 내부에서 새창을 띄우려고 하기 때문에 IE에서는 노란색 바가 뜬다. 이것을 방지하기 위해 javascript의 window.open과 as3의 navigateToURL을 적절히 섞어서 사용하는 것이 좋겠다.
  6. 너무 적은 업로드 허용 용량 오늘날 대부분의 사진은 1Mb를 넘는다. 하지만 사진 업로드시 1Mb 용량제한으로 사진올리기가 불편하다. 업로드 용량은 10Mb정도로 하되 보여주는 사진을 Resize해주는 것이 더욱 좋을 듯 하다. 그래야 사용의 불편함을 없앨 수 있지 않을까? 또한 Flash Player 10부터는 사용자 인터렉션이 있다는 전재하에 로컬의 자원 가공이 가능하다. 즉, 10이상의 사용자는 클라이언트에서 이미지 사이즈 조정이 가능하다는 의미이다. 그러므로 10이상의 경우에는 클라이언트에서 사이즈 조정해주어 서버로 보내주고 그 이하 버전의 경우 기존방식대로 사용할 수 있게 끔하면 서버부하를 줄여줄 수 있게 될 것이다.
  7. Flex 대신 Flash(순수 ActionScript 3.0)으로 개발 Flex는 사실 너무 무겁니다. ShowSpace처럼 위젯형태로 블로그나 게시판등에 붙여야하는 애플리케이션이라면 Flash기반으로 만드는 것이 훨씬 좋은 퍼포먼스를 줄 수 있을 것이라 생각한다. 물론 업로드 툴은 Flex로 그냥 놔둬도 괜찮을 듯 싶다.
  8. 외부 사이트의 사진도 볼 수 있도록 확장 사진을 꼭 업로드 방식으로만 지원할 것이 아니라 사진 URL을 입력하면 사진을 읽어와 보여주는 방법도 고려해볼 만하다. 물론 클라이언트에서는 crossdomain 정책적인 보안적인 문제가 걸려있긴 하다. 하지만 클라이언트가 아닌 서버측에서 지원한다면 할 수 없는 것도 아니다.
  9. 이미지 에디터 기능 추가 이미지를 약간 가공해서 이쁘게 꾸민것을 보여주고 싶은 사용자가 있을 것이다. 이런 사용자들을 위해 이미지 에디터 기능을 추가해서 사진을 이쁘게 꾸밀 수 있도록 배려하면 더욱 좋을 듯 싶다.

앞으로 많은 사람들이 이용하는 서비스가 되길 희망한다. ^^ 오해하시는 분들이 계신데... 이거 제가 만든 것 아닙니다.

 

Adrian Parr’s Blog 에 올라온 ActionScript 3.0 Code 라이브러리 모음이다. 매우 유용해서 긁어왔다.

 

3D Engines

3D Game Engines

3D Animation Framework

3D Physics Engines

Animation Tweening Kits

2D Physics Engines

Security

Audio Libraries

Particle Systems

Data Visualization

Loading Kits

OOP Frameworks

Other APIs and libraries

 

출처 : http://www.adrianparr.com/?p=83

1. Application Domain 사용 예제

 

지난 호에서 우리는 Application domain에 대해서 알아보았다. 지금까지 설명한 내용을 확인하기 위해 몇가지 예제를 만들고자 한다.

 

 [그림 1] 메인과 모듈에서 정의되어 있는 Sprite 클래스

 

[그림 1]은 메인 프로그램과 모듈 프로그램에 각각 정의되어 있는 Sprite를 확장한 클래스를 보여주고 있다. 메인은 Yellow, Blue를 가지며 둥근 모양이다. 모듈은 Blue, Red, Green을 가지며 사각형 모양이다. 서로 클래스 명이 중복되는 것은 Blue이다.

 

메인과 모듈에서 모두 아래와 같은 형태로 4개의 Sprite를 확장한 클래스의 정의를 참조한다.

var ClassRef:Class =
ApplicationDomain.currentDomain.getDefinition("클래스명") as Class;

 

클래스 참조값을 이용해 인스턴스를 생성하여 메인과 모듈에서 그림을 그려준다. 모듈을 불러올 때 메인 프로그램의 Application domain의 설정에 따라 메인과 모듈에 정의된 Sprite 클래스들을 참조 권한이 달라지게 될 것이다.

 

아래 코드는 모듈 프로그램에서 정의된 BlueSprite 클래스의 정의다. draw() 함수를 호출하면 30×30의 파란색 사각형을 그려준다.

 

 

//BlueSprite.as – 모듈 프로그램에서 정의됨
package com.jidolstar.blue
{
        import flash.display.Sprite;

        public class BlueSprite extends Sprite
        {
               public function BlueSprite()
               {
                       super();
               }

               public function draw():void
               {
                       graphics.clear();
                       graphics.beginFill(0x0000ff, 1);
                       graphics.drawRect(0,0,30,30);
                       graphics.endFill();
               }
        }
}

 

아래 코드는 메인 프로그램에서 정의되는 BlueSprite이다. 모듈에서의 정의와 달리 draw()함수를 호출하면 파란색 원을 그려준다. 메인과 모듈에서 정의한 이름(BlueSprite)은 같지만 그 기능은 다르다는 것을 기억하자.

 

//BlueSprite.as – 메인 프로그램에서 정의됨
package com.jidolstar.blue
{
        import flash.display.Sprite;
        public class BlueSprite extends Sprite
        {
               public function BlueSprite()
               {
                       super();
               }
               public function draw():void
               {
                       this.graphics.clear();
                       this.graphics.beginFill(0x0000ff, 1);
                       this.graphics.drawCircle(15,15,15);
                       this.graphics.endFill();
               }
        }
}

 

이처럼 메인과 모듈에 Sprite 확장 클래스를 만들어둔다.

모듈 프로그램은 다음처럼 작성된다.

 

 

//TestModule.as
package {
	import com.jidolstar.blue.BlueSprite;
	import com.jidolstar.green.GreenSprite;
	import com.jidolstar.red.RedSprite;

	import flash.display.Sprite;
	import flash.system.ApplicationDomain;

	public class TestModule extends Sprite
	{
		private var blue:BlueSprite;
		private var red:RedSprite;
		private var green:GreenSprite;

		public function TestModule()
		{
			super();

			var BlueSpriteRef:Class;
			var RedSpriteRef:Class;
			var GreenSpriteRef:Class;
			var YellowSpriteRef:Class;
			var msg:String = "";
			var sprite:*;
			var x:int = 0;

			try
			{
				var ClassRef:Class = ApplicationDomain.currentDomain.getDefinition("클래스명") as Class;
				BlueSpriteRef = ApplicationDomain.currentDomain.getDefinition("com.jidolstar.blue.BlueSprite") as Class;
				sprite = new BlueSpriteRef;
				sprite.x = x;
				sprite.y = 10;
				x += 40;
				sprite.draw();
				addChild( sprite );
			}
			catch (e:Error)
			{
				msg+= "Module에서 com.jidolstar.blue.BlueSprite 로드 실패n"
			}

			try
			{
				RedSpriteRef = ApplicationDomain.currentDomain.getDefinition("com.jidolstar.red.RedSprite") as Class;
				sprite = new RedSpriteRef;
				sprite.x = x;
				sprite.y = 10;
				x += 40;
				sprite.draw();
				addChild( sprite );
			}
			catch (e:Error)
			{
				msg += "Module에서 com.jidolstar.red.RedSprite 로드 실패n"
			}                       

			try
			{
				GreenSpriteRef = ApplicationDomain.currentDomain.getDefinition("com.jidolstar.green.GreenSprite") as Class;
				sprite = new GreenSpriteRef;
				sprite.x = x;
				sprite.y = 10;
				x += 40;
				sprite.draw();
				addChild( sprite );

			}
			catch (e:Error)
			{
				msg += "Module에서 com.jidolstar.green.GreenSprite 로드 실패n"
			}       

			try
			{
				YellowSpriteRef = ApplicationDomain.currentDomain.getDefinition("com.jidolstar.yellow.YellowSprite") as Class;
				sprite = new YellowSpriteRef;
				sprite.x = x;
				sprite.y = 10;
				x += 40;
				sprite.draw();
				this.addChild( sprite );
			}
			catch (e:Error)
			{
				msg += "Module에서 com.jidolstar.yellow.YellowSprite 로드 실패n"
			}

			trace(msg);
		}
        }
}

 

메인, 모듈에서 정의되는 BlueSprite, RedSprite, GreenSprite, YellowSprite 클래스 정의를 모듈의 current domain으로부터 얻어와 성공하면 그림을 그려주는 형태로 되어 있다. 특별히 모듈에서는 BlueSprite, RedSprite, GreenSprite가 정의되도록 아래와 같은 코드가 있다.

 

private var blue:BlueSprite;
private var red:RedSprite;
private var green:GreenSprite;

 

메인 프로그램은 모듈과 거의 동일한 모습으로 만들어진다. 다른 점은 모듈을 읽어올 수 있는 부분과 Application domain을 설정하는 부분이 추가가 된다. Application domain을 적용하는 상황이 총 세가지이므로 중복되는 코드를 줄이기 위해 Base 클래스를 만들고 이 Base 클래스를 확장해 3개의 상황에 맞게 메인 프로그램 A,B,C를 작성한다. 그럼 Base 프로그램을 보자.

 

//MainAppBase.as
package
{
import com.jidolstar.blue.BlueSprite;
import com.jidolstar.yellow.YellowSprite;

import flash.display.DisplayObject;
import flash.display.Loader;
import flash.display.Sprite;
import flash.events.Event;
import flash.net.URLRequest;
import flash.system.ApplicationDomain;
import flash.system.LoaderContext;
import flash.text.TextField;
import flash.text.TextFormat;

public class MainAppBase extends Sprite
{
	private var blue:BlueSprite;
	private var yellow:YellowSprite;
	private var loader:Loader;
	protected var applicationDomain:ApplicationDomain;

	public function MainAppBase()
	{
		super();

		loader = new Loader();
		var request:URLRequest = new URLRequest("TestModule.swf");
		var context:LoaderContext = new LoaderContext();
		context.applicationDomain = applicationDomain;
		loader.contentLoaderInfo.addEventListener( Event.COMPLETE, completeHander );
		loader.load(request,context);
	}

	private function completeHander(event:Event):void
	{
		var BlueSpriteRef:Class;
		var RedSpriteRef:Class;
		var GreenSpriteRef:Class;
		var YellowSpriteRef:Class;
		var msg:String = "";
		var sprite:*;
		var x:int = 0;
		var ModuleRef:Class = loader.contentLoaderInfo.applicationDomain.getDefinition("TestModule") as Class;
		var module:DisplayObject = new ModuleRef();
		//var module:DisplayObject = event.target.content as DisplayObject
		module.x = 0;
		module.y = 50;
		this.addChild( module );

		loader.contentLoaderInfo.removeEventListener( Event.COMPLETE, completeHander );
		loader.unload();
		loader = null;

		try
		{
			BlueSpriteRef = ApplicationDomain.currentDomain.getDefinition("com.jidolstar.blue.BlueSprite") as Class;
			sprite = new BlueSpriteRef;
			sprite.x = x;
			sprite.y = 10;
			x += 40;
			sprite.draw();
			this.addChild( sprite );
		}
		catch (e:Error)
		{
			msg+= "Main에서 com.jidolstar.blue.BlueSprite 로드 실패n"
		}

		try
		{
			RedSpriteRef = ApplicationDomain.currentDomain.getDefinition("com.jidolstar.red.RedSprite") as Class;
			sprite = new RedSpriteRef;
			sprite.x = x;
			sprite.y = 10;
			x += 40;
			sprite.draw();
			this.addChild( sprite );
		}
		catch (e:Error)
		{
			msg += "Main에서 com.jidolstar.red.RedSprite 로드 실패n"
		}                       

		try
		{
			GreenSpriteRef = ApplicationDomain.currentDomain.getDefinition("com.jidolstar.green.GreenSprite") as Class;
			sprite = new GreenSpriteRef;
			sprite.x = x;
			sprite.y = 10;
			x += 40;
			sprite.draw();
			this.addChild( sprite );
		}
		catch (e:Error)
		{
			msg += "Main에서 com.jidolstar.green.GreenSprite 로드 실패n"
		}       

		try
		{
			YellowSpriteRef = ApplicationDomain.currentDomain.getDefinition("com.jidolstar.yellow.YellowSprite") as Class;
			sprite = new YellowSpriteRef;
			sprite.x = x;
			sprite.y = 10;
			x += 40;
			sprite.draw();
			this.addChild( sprite );
		}
		catch (e:Error)
		{
			msg += "Module에서 com.jidolstar.yellow.YellowSprite 로드 실패n"
		}

		trace(msg);
	}
}
}

 

 

completeHandler() 함수를 보면 모듈 프로그램에서 정의했던 것과 별반 다를 바 없다. 단, 불러온 모듈을 화면에 표시하기 위해 아래와 같은 코드가 추가가 되어 있다.

 

var module:DisplayObject = event.target.content as DisplayObject
module.x = 0;
module.y = 50;
this.addChild( module );

 

 

생성자에서 Loader 클래스를 이용해 해당 모듈 SWF를 불러오도록 만들었다. Application domain을 protected로 값을 받아 설정할 수 있도록 만든 것을 확인하기 바란다. 이렇게 만든 의도는 이 Base 클래스를 확장한 메인 프로그램의 생성자에 application domain을 미리 설정하라는 것을 의미한다.

 

아래는 앞서 만든 Base 클래스를 이용해 application domain을 새롭게 정의해서 사용하는 예이다. 생성자에 applicationDomain을 어떻게 설정했는지 확인하면 되겠다.

 

//MainAppUsageA.as
package {
	import flash.system.ApplicationDomain;
	public class MainAppUsageA extends MainAppBase
	{
		public function MainAppUsageA()
		{
			applicationDomain = new ApplicationDomain();
			super();
		}
	}
}
//MainAppUsageB.as
package {
	import flash.system.ApplicationDomain;
	public class MainAppUsageA extends MainAppBase
	{
		public function MainAppUsageA()
		{
			applicationDomain = ApplicationDomain.currentDomain;
			super();
		}
	}
}
//MainAppUsageC.as
package {
	import flash.system.ApplicationDomain;
	public class MainAppUsageA extends MainAppBase
	{
		public function MainAppUsageA()
		{
			applicationDomain = new ApplicationDomain(ApplicationDomain.currentDomain);
			super();
		}
	}
}

 

위에서 제시한 코드는 두 개의 ActionScript3 프로젝트를 만들어서 아래와 같은 구성으로 만든다.

 

[그림 2] 메인과 모듈 프로젝트 구성

 

위처럼 모듈 SWF파일을 메인 쪽에 복사해야 모듈을 불러서 동작시킬 수 있다. 3개의 메인 프로그램을 단독으로 실행할 수 있도록 하기 위해, 프로젝트 폴더 명에서 마우스 오른쪽 버튼을 눌러 컨텍스트 메뉴의 Properties를 클릭해서 뜨는 창의 좌측에 ActionScript Applications에 default로 지정된 것 말고도 나머지 2개를 추가해준다.

 

이렇게 하면 기본적인 테스트 환경이 완료가 된다. 위의 MainAppUsageA.as를 선택하고 실행해보자.

 

[그림 3] 메인과 모듈이 서로 다른 application domain 일 때 실행 화면

 

이것은 new ApplicationDomain()를 이용해 모듈의 application domain을 설정했을 때 결과이다. 메인과 모듈간에 application domain이 다르기 때문에 상대방에서 정의한 클래스를 참조할 수 없다. 윗부분에 메인부분이다. 메인에서 정의한 BlueSprite와 YellowSprite에 대한 정의만 사용하여 그림을 그렸다. 아래 부분은 모듈로 모듈에서 정의한 BlueSprite, RedSprite, GreenSprite 를 이용해 그림이 그려졌다는 것을 알 수 있다.

MainAppUsageB.as를 선택하고 실행해보자.

 

 

[그림 4] 메인과 모듈이 서로 같은 application domain 일 때 실행 화면

 

이것은 ApplicationDomain.currentDomain을 이용해 모듈의 application domain을 설정했을 때 결과이다.

위쪽의 메인 프로그램에서 그려준 것을 보면 자신이 정의한 BlueSprite와 YellowSprite를 이용해 둥근 모양의 그림을 그렸다. 또한 같은 application domain이므로 모듈에서 정의한 RedSprite와 GreenSprite를 이용해 사각형 모양의 그림도 그렸다. 메인 프로그램과 모듈에는 BlueSprite가 중복되었다는 것을 [그림 2]에서 미리 언급했었다. 이 경우 메인에 이미 BlueSprite가 정의된 상태였기 때문에 모듈의 BlueSprite는 무시되었다는 것을 확인할 수 있다.

 

MainAppBase.as의 completeHandler() 함수를 잘 살펴보면 loader.unload()를 호출하고 나서 모듈의 SWF파일을 Unload했음에도 불구하고 모듈에서 정의한 RedSprite, GreenSprite를 그대로 사용하고 있다는 것을 볼 수 있다. 즉, 부모의 application domain에 등록된 자식의 Class 정의는 그 자식이 Unload되더라도 가비지 컬렉션 대상이 되지 않는다.

 

마지막으로 MainAppUsageC.as를 선택하고 실행해보자.

 

 

[그림 5] 메인과 모듈이 application domain이 부모-자식 관계일 때 실행 화면

 

이것은 new ApplicationDomain(ApplicationDomain.currentDomain)를 이용해 모듈의 application domain을 설정했을 때 결과이다. Application domain이 부모-자식관계가 되어 메인 프로그램에서는 모듈에서 정의한 클래스를 참고할 수 없으므로 RedSprite와 GreenSprite 클래스의 정의를 읽을 수 없다. 하지만 모듈은 자식 application domain 에 있기 때문에 부모의 BlueSprite와 YellowSprite를 사용한다. 중복정의된 BlueSprite는 이미 메인 프로그램에 정의되어 있던 BlueSprite로 대체되어진다.

 

 

2. 정리하며

 

Application domain에 대해서 자세하게 알아보았다. 모듈화 프로그래밍을 하기 위해서는 반드시 숙지해야 하는 부분이다. ActionScript 3.0로 모듈 프로그래밍을 할 때는 Loader 클래스를 이용해야하는데 상당히 불편한 점이 많다. 하지만 Flex에서는 Module 및 ModuleManager, ModuleLoader 등을 이용해 모듈 프로그래밍을 더욱 간편하게 할 수 있도록 도와주고 있다. 하지만 내부적으로는 Loader 클래스를 사용하고 있기 때문에 Application domain의 개념을 알고 있어야 이 클래스들을 적절히 잘 사용할 수 있다.

 

<참고내용>

 

이 글은 adobeflex.co.kr에 기술문서로 올린 글입니다.

1. Application domain에 대해

1.1 모듈화와 모듈화에 따른 application domain의 역할

 

프로젝트를 하다 보면 모듈화를 할 필요가 생긴다. 모듈화는 하나의 메인 응용 프로그램에 모든 기능이 있는 대신, 관련된 기능을 떼어내어 모듈을 만들어 필요한 시점에 동적으로 불러 사용할 수 있도록 만드는 것을 의미한다.

 

가령 Flex로 만든 블로그가 있다고 가정하면. 블로그에는 글쓰기, 리스트, 방명록, 관리, 메뉴 등이 존재할 것이다. 이러한 것이 하나의 SWF 파일에 모두 존재한다면 보여주지도 않을 방명록, 관리 부분까지 처음 프로그램 실행부터 전부 불러와야 하는 부담이 생긴다. 그래서 필요에 따라 동적으로 읽어서 사용할 수 있도록 글쓰기에 관련된 SWF, 리스트에 관련된 SWF 등으로 나눠서 만들어 놓으면 필요할 때 사용할 수 있으므로 더욱 효율적일 것이다.

 

이렇게 모듈화를 할 경우 블로그 메인 프로그램과 각 글쓰기 모듈 프로그램, 리스트 모듈 프로그램, 방명록 모듈 프로그램간에 통신을 하게 될 것이고, 때에 따라서는 메인 프로그램에서 정의한 자원을 모듈에 포함할 것 없이 재사용할 필요도 있을 것이다. 이렇게 하면 모듈에 메인 프로그램과 동일한 중복 코드를 가지고 있을 필요가 없어 용량이 줄어들 수 있기 때문이다. 그렇게 되면 자연스럽게 메인-모듈, 모듈-모듈간에 자원을 공유하는 일이 발생하게 된다.

 

이 문서는 모듈화를 하는 방법을 제시해준다기 보다는 모듈화를 위해 선행적으로 알아야 할 한가지를 언급하고자 한다. 그 중 하나가 바로 여기서 지금부터 다룰 application domain(응용 프로그램 영역)이다. application domain은 서로 다른 SWF 프로그램끼리 자원 공유를 위한 영역을 구분 지을 수 있도록 도와준다.

 

Namespace와의 비교

 

application domain에 대해서 이해하기 위해 Namespace와 비교해 보도록 하겠다.

 

Namespace는 이름, 용어 혹은 단어 항목들을 위한 문맥(context)을 제공하는 추상적인 컨테이너(container)라고 정의되어 있다. Namespace는 Java나 Flex, ActionScript3에서 사용하는 패키지(Package)와 거의 동일하다. Namespace는 프로그램 안에 같은 이름의 Class가 있을 때 구분해주는 역할을 한다. 가령 com.jidolstar.URLUtil과 flash.jidolstar.URLUtil 이 있다면 URLUtil로 이름은 같지만 Namespace는 다르므로 이 클래스는 이름만 같지 전혀 다른 클래스이다. 이렇듯 Namespace를 잘 활용하면 같은 이름의 클래스로 다른 동작을 하도록 만들 수 있고 또는 프로젝트 진행시 클래스 명이 충돌하는 경우도 방지할 수 있다.

 

그럼 application domain의 경우는 어떨까? Namespace는 단일 응용 프로그램에 같은 이름의 Class를 구분하기 위해 사용되는 반면 application domain은 여러 개의 응용 프로그램간에 같은 이름의 Class를 구분하기 위해 사용된다.

 

프로그램들이 하나의 application domain안에 포함되어 있다면 같은 이름을 가진 Class는 단 한 개만 존재가 가능하다. 가령 부모 프로그램의 Class 정의만 남고 자식 프로그램의 Class는 무시된다. 반면 프로그램들의 application domain이 서로 다르다면 같은 이름을 가진 Class라 할지라도 이들 Class는 서로 다른 application domain에 존재하는 것이므로 다른 것으로 해석된다.

 

 

1.2 Application domain 개념

 

아래는 application domain에 대해서 간단히 설명하고 있다.

application domain은 Loader의 load() 또는 loadBytes() 메쏘드를 이용해 ActionScript3.0으로 만들어진 SWF 파일을 불러들일 때만 사용된다.

application domain를 적용하기 위해 ApplicationDomain 클래스를 LoaderContext 클래스의 applicationDomain 속성에 대입하여 Loader의 load() 및 loadBytes() 메쏘드를 호출시 인자로 LoaderContext의 인스턴스를 넘겨주면 된다.

application domain은 동일한 보안 도메인(security domain)안에서 하나 이상의 SWF 파일 안에 제작된 ActionScript3.0 코드들을 목록화하여 관리한다. 다시 말하면 하나의 application domain안에서 여러 개의 SWF 파일에서 작성한 ActionScript3.0 코드를 관리할 수 있다. 또한 서로 다른 SWF 파일간에 다른 application domian을 안에 있다면 SWF 파일에 작성된 ActionScript3.0 코드는 중복되어지지 않는다. ActionScript3.0 코드는 Class 형태로 제작되며 결과적으로 application domain을 이용해 SWF 파일에 정의된 Class들이 서로 같은 영역에서 정의될 수 있고 또는 다른 영역에서 정의될 수도 있다. 또한 부모 SWF 파일에서 자식 SWF 파일에만 Class 정의 사용할 수 있도록 허용할 수 있다.

위처럼 application domain은 SWF 프로그램에 ActionScript 3.0으로 제작된 Class 정의를 원하는 영역(도메인)에 나누는 역할을 한다. 어도비 라이브 독(Adobe Livedocs)에 의하면 application domain에 대해서 아래 중요한 사실을 언급하고 있다.

 

  1. SWF 파일 안에 있는 모든 코드는 하나의 application domain 안에 존재되도록 정의된다. 메인 응용 프로그램(main application)의 application domain은 current domain이다. system domain current domain을 비롯하여 모든 application domain을 포함하기 때문에 모든 Flash Player 클래스를 가지고 있다.
  2. system domain을 제외한 모든 application domain은 그와 연결된 parent domain을 가진다. 메인 응용 프로그램의 application domain에 대한 parent domain은 system domain이다. 같은 application domain안에서 부모 프로그램과 자식 프로그램이 있다고 가정하자. 이때 부모 프로그램이 불러들인 자식 프로그램 안에 클래스들은 그 부모 안에 같은 이름의 클래스가 정의되어 있지 않을 때만 정의되고 사용할 수 있다. 또한 자식 프로그램의 클래스가 부모의 있는 클래스를 재정의(override) 할 수 없다.

 

같은 보안 도메인(scrutiny domain) 안에서 LoaderContext의 Application Domain 속성에 자신이 사용하고자 하는 Application Domain 속성을 아래 4가지 중 하나를 선택하면 되겠다.

 

  1. 부모의 application domain을 상속받은 application domain
    이것이 기본값이다.
    이 선택은 new ApplicationDomain(ApplicationDomain.currentDomain) 구문을 이용하는 것이다.
    이 방식은 자식의 application domain에서 부모에서 정의한 Class를 직접 사용할 수 있다. 가령 자식에서 ApplicationDomain.currentDomain.getDefinition(“부모에서 정의된 클래스명”)로 접근이 가능하다는 것을 의미한다. 반면 부모는 자신의 application domain에서 자식에서 정의된 Class에 접근이 불가능하다.
    이 방식의 장점은 자식에서 부모에 이미 정의된 Class와 이름이 같은 Class를 정의해도 오류가 나지 않는다. 자식은 해당 Class에 대한 부모의 정의를 상속받기만 하며, 부모와 충돌하는 클래스 정의는 무시된다.
  2. 부모 자신의 application domain
    ApplicationDomain.currentDomain 구문을 사용하는 것과 같다. 자식 SWF 파일을 불러오면 부모와 자식이 상대방에서 정의한 Class에 직접 접근이 가능하다. 서로 ApplicationDomain.currentDomain.getDefinition()을 이용해 접근이 가능하다. 단, 자식에서 부모와 같은 Class를 정의를 사용했다면 자식의 Class 정의는 무시된다.
  3. 시스템 application domain을 상속받은 application domain
    이는 new ApplicationDomain(null) 구문을 사용하는 것과 같다. 이렇게 하면 부모와 자식간에 정의된 Class는 서로 다른 application domain에 있기 때문에 자신의 application domain 안에서는 상대방에서 정의한 Class를 참고할 수 없게 된다. 부모와 자식간에 같은 이름의 Class가 있더라도 application domain이 다르므로 자식의 Class정의가 무시되는 경우는 없다.
  4. 임의의 application domain을 상속받은 application domain
    이 방법은 application domin 계층 구도가 복잡해질 수 있으므로 지양하는 방법이다. 자식 SWF을 자신의 security domain 안에 있는 임의의 application domain을 이용해 사용하는 것으로 가령, new ApplicationDomain(ApplicationDomain.currentDomain.parentDomain.parentDomain)을 사용하면 자식 SWF이 현재 application domain의 부모의 부모 domian을 상속받아 application domain을 만든다.

 

때때로 부모와 자식이 상대방의 application domain에 접근해야 하는 경우도 있다. 상대방이 application domain에 접근할 수 있는 경우 ApplicationDomain.currentDomain으로 상대방의 Class 정의를 가져올 수 있다. 또 다른 경우 부모가 자식 SWF를 로드가 완료했을 때, Loader 함수의 인스턴스가 loader라면 loader.contentLoaderInfo.applicationDomain.getDefinition()를 이용해서 자식의 application domian에 접근하여 자식에서 정의한 Class를 사용할 수 있다. 자식이 로드되는 방식을 알고 있는 경우 부모의 application domian에 접근할 수 있는데 기본적으로 ApplicationDomain.currentDomain.parentDomin을 이용하여 부모에서 정의한 Class에 접근이 가능하겠다.

 

 

2. ApplicationDomain Class 사용법에 따른 동작방식 해석

 

아래 <그림 1>은 단일 도메인(domain1.com)에 존재하는 다양한 SWF 프로그램(module1.swf, module3.swf, application2.swf)을 메인 SWF 프로그램(application1.swf)에서 Loader 클래스의 인스턴스를 통해 불러서 사용하는 모습을 묘사하고 있다. 이때 불러오는 SWF 프로그램들은 각자의 application domain에 등록되어 있다는 것을 알 수 있다.

 


<그림 1> Application domain과 SWF 프로그램 간의 관계

 

 

아래 예제는 [그림 1]의 Usage B의 경우와 같다.

 

package
{
    import flash.display.Loader;
    import flash.display.Sprite;
    import flash.events.*;
    import flash.net.URLRequest;
    import flash.system.ApplicationDomain;
    import flash.system.LoaderContext;

    public class ApplicationDomainExample extends Sprite
    {
        private var ldr:Loader;
        public function ApplicationDomainExample()
        {
            ldr = new Loader();
            var req:URLRequest = new URLRequest("Greeter.swf");
            var ldrContext:LoaderContext = new LoaderContext(false, ApplicationDomain.currentDomain);
            ldr.contentLoaderInfo.addEventListener(Event.COMPLETE, completeHandler);
            ldr.load(req, ldrContext);    

        }

        private function completeHandler(event:Event):void
        {
            var ClassRef:Class = ApplicationDomain.currentDomain.getDefinition("Greeter") as Class;
            var myGreeter:* = new ClassRef();
            var message:String = myGreeter.welcome("Tommy");
            trace(message); // Hello, Tommy
        }
    }
}

 

위 예제를 간단하게 설명하면 메인 프로그램에서 모듈 SWF를 불러올 때 메인과 같은 application domain으로 설정한다.  모듈 SWF 로드를 완료하면 모듈 SWF 파일에 정의된 클래스(Greeter)를 이용해 인스턴스를 생성하고  그 클래스의 메서드에 접근하는 것을 확인할 수 있다.

 

이처럼 부모(메인)가 Loader 클래스를 이용해서 ActionScript 3.0으로 작성된 외부 SWF 파일을 자식으로 불러와 자식의 클래스와 메서드에 접근할 수 있다. Loader 클래스의 context 속성에 LoaderContext 객체를 넘겨주는데 LoaderContext에 바로 applicationDomain 매개변수를 넣을 수 있다. 위 예제에서는 메인 프로그램의 application domain인 current domain을 매개변수로 넘겨주었다.  자식의 SWF 파일에 부모와 동일한 application domain을 적용하겠다는 것을 의미한다. 자식을 부모의 application domain에 포함했기 때문에 부모는 자식에서 정의한 Greeter Class를 부모의 application domain 안에서 사용할 수 있게 된다.

 

위의 과정에 대해서 좀더 자세히 알아보자. <그림 1>에서 application domain 사용 예가 3가지 있다는 것을 알 수 있다. 아래 <그림 2>는 application domain 안에서 어떻게 Class가 공유되는지 살펴보기 위한 것이다.

 

<그림 2> application domain을 지정하는 3가지 방법

 

 

<그림 2>에서 클래스 명에 X표는 로드 후 정의에서 무시되는 클래스라는 것만 일단 기억하자. 지금부터 <그림 2>에서 표현하고 있는 application domain의 3가지 사용법에 대해서 언급하고 그에 따른 동작방식에 대해 이해해 보겠다.

 

 

2.1 ApplicationDomain 사용법A 새로운 application domain 사용

var loader:Loader = new Loader();
var request:URLRequest = new URLRequest("application2.swf");
var context:LoaderContext = new LoaderContext();
context.applicationDomain = new ApplicationDomain();
loader.load(request,context);

 

위 코드는 메인 프로그램인 application1.swf 내에서 짜여진다. 이는 메인 SWF 프로그램과 전혀 다른 application domain 안에 존재하도록 만든다. 불러오는 컨텐츠의 이름이 module2.swf가 아닌 application2.swf로 표현한 이유는 서로 다른 application domain이기 때문에 독립적인 실행이 가능할 수 있다는 전제가 들어갈 수 있기 때문이다. application2.swf의 parent domain은 system domain이 된다.

 

<그림 3> 다른 application domain으로 외부 SWF를 불러오는 경우

 

<그림 3>에서 application1.swf와 application2.swf가 완전히 다른 application domain이기 때문에 서로의 각자 영역에서 정의된 com1::Class1과 com2::Class2는 상대방의 프로그램에 의해 영향을 받지 않는다. 같은 클래스 이름을 가지는 com1::Class의 경우 각자 다른 영역에 존재하므로 이름만 같은 다른 클래스로 이해할 수 있다.

 

var ClassRef:Class = getDefinitionByName("com1.Class1") as Class;

 

참고로 위에서 사용된 getDefinitionByName() 함수는 내부적으로 ApplicationDomain.currentDomain.getDefinition()을 호출하도록 만들어져 있다.

만약 application1.swf에서 위와 코드와 같이 접근하면 application2.swf를 불러온 상태에서도 application1.swf에 정의한 com1.Class1에 접근하게 된다. 반대로 application2.swf에서 같은 방식으로 접근할 때 application1.swf로 불려졌다 할지라도 서로 다른 application domain이므로 application2.swf의 com1.Class1에 접근하게 된다. 즉, 자기 자신의 application domain 영역에 정의된 Class를 사용하며 서로 간섭을 일으키지 않는다.

 

var ClassRef:Class = getDefinitionByName("com2.Class2") as Class;

 

위의 접근 방식은 <그림 3>에서 application2.swf에서만 사용이 가능하다. com2.Class2는 application2.swf에서 정의되었고 application1.swf의 application domain이 application2.swf의 것과 다르기 때문에 application1.swf에서는 이 Class를 사용할 수 없다. 완전히 다른 application domain 영역이므로 unload시 바로 가비지 컬렉션 대상이 된다.

 

application1.swf에서 application2.swf의 com2.Class2에 접근하기 위해서는 application2.swf의 application domain의 참조를 알아야하는데 아래와 같이 참조가 가능하겠다.

var ClassRef:Class = loader.contentLoaderInfo.applicationDomain.getDefinition("com2.Class2") as Class;

 

 

2.2 ApplicationDomain 사용법B 부모의 current domain을 사용

 

var loader:Loader = new Loader();
var request:URLRequest = new URLRequest("module1.swf");
var context:LoaderContext = new LoaderContext();
context.applicationDomain = ApplicationDomain.currentDomain;
loader.load(request,context);

 

위 코드에서 module1.swf는 부모와 같은 application domain을 사용하고 있다. 이것은 부모 프로그램의 자원을 공유하겠다는 의미와 동일하다.

 

<그림 4> 부모 프로그램과 동일한 application domain을 사용하는 경우

 

<그림 4>에서 볼 수 있듯이 동일한 application domain에 있기 때문에 자식인 module1.swf는 부모 application1.swf 내에 정의된 클래스를 사용할 수 있다. 만약 클래스가 부모 프로그램에 정의되어 있지 않는 경우 자식 프로그램에서 정의된 클래스가 부모 프로그램의 application domain에 등록되어 함께 공유하게 된다. 자식 프로그램과 부모 프로그램에 같은 이름의 Class가 존재한다면 자식 프로그램에서 정의한 Class는 무시된다.

 

var ClassRef:Class = getDefinitionByName("com1.Class1") as Class;

 

 

위와 같이 양쪽 프로그램에서 클래스 정의를 참조한다고 할 때 이미 application1.swf에 com1.Class1의 정의가 존재한 상태에서 module1.swf가 불려온 경우 module1.swf에 정의된 com1.Class1은 무시가 되어버린다. 결과적으로, 양쪽 프로그램에서 getDefinitionByName(“com1.Class1”)에 의해 얻어진 Class참조는 바로 application1.swf에 있는 com1.Class1이다.

 

var ClassRef:Class = getDefinitionByName("com2.Class2") as Class;
var ClassRef:Class = loader.contentLoaderInfo.applicationDomain.getDefinition("com2.Class2") as Class;

 

위 경우 두가지 모두 똑같은 com2.Class2의 클래스 정의를 얻는 방법이다. 같은 application domain에 있으므로 가능한 것이다. 단, application1.swf가 module1.swf를 로드하기 전에는 이와 같이 사용할 수 없다. 왜냐하면 application1.swf의 application domain에는 com2.Class2가 정의되어 있지 않기 때문이다. 하지만 module1.swf을 로드한 후에는 같은 application domain에 com2.Class2가 등록이 된다. 그러므로 application1.swf에서도 module1.swf에서 정의한 com2.Class2 클래스 정의를 이용할 수 있다.

 

이러한 방법은 RSLs(Runtime Shared Libraries)에서 사용되며 로드된 SWF는 RSL로 처리된다. 이 말은 RSL과 동일하게 가비지 컬렉션 대상이 될 수 없다는 것을 의미한다. 왜냐하면 메인 프로그램인 application1.swf의 application domain에 로드된 module1.swf의 클래스 정의가 이미 부모의 application domain 내에 등록되었기 때문에 자식인 module1.swf의 참조를 지우고 언로드(unload)하더라도 한번 등록된 Class 정의는 지워지지 않는다.

 

RSL과 다른 점은 RSL은 프로그램 실행시 특별한 코딩이 필요 없이 자동으로 로드하는 반면 이 방법은 필요한 시점에 동적 로드가 가능하다는 것이다. 자동으로 로드한 다는 것은 Flex 프레임워크가 자체 지원하고 있다는 것을 의미한다.

 

 

2.3 ApplicationDomain 사용법C 부모의 current domain를 상속받은 새로운 application domain 사용

 

var loader:Loader = new Loader();
var request:URLRequest = new URLRequest("module3.swf");
var context:LoaderContext = new LoaderContext();
context.applicationDomain =
new ApplicationDomain(ApplicationDomain.currentDomain);
loader.load(request,context);

 

new ApplicationDomain()시 인자는 parentDomain이다. 즉, 새로운 application domain을 작성하되 부모의 current domain을 인자로 넘겨준다.

 

 

<그림 5> 부모의 application domain을 이용해 새로운 자식 application domain을 생성하는 경우

 

 

이렇게 되면 application domain 자체가 부모-자식관계가 된다. <그림 5>에서 보면 application domain이 걸쳐있는 것을 볼 수 있다.

 

var ClassRef:Class = getDefinitionByName("com1.Class1") as Class;

 

application1.swf가 module3.swf를 불러온 후 com1.Class1을 접근하면 자식 프로그램인 module3.swf에서 정의된 com1.Class1은 무시되어 버린다. 왜냐하면 application1.swf에 이미 com1.Class1이 정의되어 있고 module3.swf은 application1.swf의 같은 application domain을 참고하기 때문이다.

 

var ClassRef:Class = getDefinitionByName("com2.Class2") as Class;

 

재미있는 사실은 이 경우이다.

 

application1.swf가 module3.swf를 불러왔지만 application1.swf에서 정의되지 않는 com2.Class2를 module3.swf에서 정의한 com2.Class2를 application1.swf의 application domain 에서는 이 클래스 정의를 참조할 수 없다. com2.Class2 정의는 자식 프로그램인 module3.swf의 application domain에서만 참조가 가능하다.

 

이 관계를 쉽게 이해하기 위해 Class를 상속받는 관계와 비교해 생각하면 된다. 자식은 부모에 있는 모든 자원을 사용할 수 있으나 부모는 자식에서 새로 정의된 자원은 사용할 수 없다라는 사실을 기억하면 이러한 관계가 쉽게 이해될 수 있을 것이다.

 

이 방법은 부모에서 정의된 클래스를 최대한 활용하면서 자식의 독립된 클래스 영역을 확보하는데 사용된다. 자식에서 정의된 클래스가 부모의 application domain에 포함되지 않으므로 부모가 자식의 SWF에 대한 참조를 포함하지 않는 경우, 자식에서 정의한 클래스는 모두 가비지 컬렉션 대상이 된다.

 

 

2.4 Application domain의 사용법에 따른 동작방식 표

 

지금까지 application domain을 적용하는 방법과 그에 따른 결과가 어떻게 되는지 알아보았다. 관련된 내용을 <표 1>에 정리해 보았다. 참고하길 바란다.

 

[표 1] Application domain 사용법에 따른 동작방식

 

3. Application Domain의 다양한 응용

앞에서 application domain의 사용법 및 동작방식에 대해서 알 수 있었다. 많은 경우 이 3가지 방법을 섞어가며 사용하게 될 것이다. 아래 몇 가지 예제를 통해서 어떻게 자원이 공유되는지 이해해보자.

 

<그림 6> application domain 응용 예시 도면 1

 

<그림 6>을 보자. 부모 프로그램인 application1.swf의 application domain안에서 module1.swf가 먼저 로드되고 module2.swf가 나중에 로드된다.

 

처음 로드될 때를 살펴보자. 앞에서 이미 언급한 것과 동일하게 module1.swf의 com1.Class1는 application1.swf에 이미 정의되어 있기 때문에 무시된다. 하지만 com2:Class2는 정의되어 있지 않았기 때문에 application domain에 등록되고 공유된다.

 

나중 로드될 때를 보자. module2.swf 에는 application1.swf에서 정의된 com1.Class1이 있으므로 무시된다. 또한 이미 module1.swf에 의해 application domain에 com2.Class2 등록되어 있으므로 module2.swf의 com2.Class2는 무시되어 버린다.


 

<그림 7> application domain 응용 예시 도면 2

 

<그림 7>의 경우 앞에서 언급한 것과 같은데 다른 점은 module이 그의 application domain안에서 다른 module을 불러들이고 있다.

 

메인 프로그램인 application1.swf에 이미 com1.Class1이 정의되어 있으므로 모듈에서 정의된 모든 com1.Class1은 무시된다. 두 모듈은 전부 application1.swf의 com1.Class1의 정의를 사용할 수 있다.

모두 로드가 완료된 경우에 application1.swf는 module4.swf의 com3.Class3를 사용할 수 없지만 module3.swf는 사용할 수 있다. 왜냐하면 application1.swf과 module4.swf의 application domain관계는 부모-자식 관계이기 때문이다.

 

<그림 8> application domain 응용 예시 도면 3

 

<그림 8>에서 Application domain은 앞에서 언급한 것과 동일하게 적용된다.

 

module5.swf에 다른 application domain을 가진 application2.swf가 로드되는 경우 메인 프로그램(application1.swf)과 module5.swf와 전혀 다른 영역을 가지므로 application2.swf안에 정의된 모든 클래스는 application1.swf와 module5.swf에서 정의된 클래스들과 별개가 된다.


이 글은 adobeflex.co.kr의 기술문서로 올린 글이다.

 

 

 

폰트(Font)를 동적으로 로딩해서 시스템에 등록한다는 의미는 Embed 폰트를 적용하되 애플리케이션에 포함된 폰트가 아닌 폰트를 담은 SWF를 원하는 시점에 읽어와 애플리케이션에 사용하겠다는 의미이다.

 

OS에서 기본적으로 제공하는 폰트는 System Font라고 불리고 애플리케이션에 포함되어 컴파일된 폰트는 Embedded Font라고 불리운다. System Font는 사용하기 편하지만 사용자의 사용환경에 따라 폰트가 설치되어 있지 않으면 심하게는 글씨가 안보이는 일이 생길 수 있다. 대신 Embedded Font는 애플리케이션에 포함되기 때문에 어떤 사용자가 애플리케이션을 사용하든지 항상 폰트를 사용한 글자를 볼 수 있다. 예를 들어 한글에 관련된 폰트를 애플리케이션에 포함시켜버리면 시스템에 한글폰트가 전혀없는 아랍국가에 있는 사용자도 애플리케이션 실행시 문제없이 한글을 볼 수 있다. 그리고 Embedded Font는 Anti-aliasing, 회전등 다양한 효과를 줄 수 있는 반면 System Font는 이런 효과에 제약이 있다. Embeded Font는 애플리케이션에 포함하는 것이므로 애플리케이션 자체가 커진다. 무거워진 애플리케이션은 결국 사용자들의 시간과 인내를 한계점에 도달시키게 할지도 모른다. 그러므로 Embeded Font를 꼭 사용해야하는 경우, 직접 애플리케이션 내부에 포함하지 않고 따로 폰트만을 담은 SWF파일을 만들어 애플리케이션이 실행되는 중간에 필요할 때 폰트를 로드할 수 있도록 설계를 하게 된다. 이러한 작업은 생각보다 단순하다. 가령, Adobe Flex의 경우 CSS의 @font-face를 이용해 폰트를 등록하고 CSS를 SWF로 만들어 StyleManager를 이용해 사용할 수 있으며 ActionScript 3.0 기반에서도 Class를 만들어 폰트를 담은 SWF를 만들어 Loader로 읽어들여 해당 폰트를Font.registerFont()로 등록할 수 있다. 본인은 이미 폰트에 관련된 글을 수도 없이 적었다. 아래 링크에 있는 내용만 가지고도 폰트를 적용하는데 충분할 것이다.

 

이렇게 많이 폰트에 관련된 글을 적고도 또 언급하는 이유는 ActionScript 3.0 프로젝트에서 Font 관리자 제작 필요성을 느꼈기 때문이다. Font 클래스가 있는데 무슨 또 관리자냐고 생각할 수 있다. 하지만 Font는 이미 등록되어 있는 폰트를 관리하는 것이다.(System Font든 Embeded Font든) 본인이 하고자 하는 것은 동적으로 폰트를 로딩해서 시스템에 적용하는데 필요한 클래스를 작성하는 것이다. 즉, 폰트를 동적으로 로드하고 등록해주는 본인이 제작한 FontManager 클래스를 소개하는 것이 이 글을 쓰는 목적이다.

 

1. Flex에서 Font를 어떻게 동적으로 등록하여 사용하나?

(Flash 개발자이지만 Flex는 생소하다면 그냥 가볍게 읽자. 자세하게 볼 필요는 없다.) 결과를 먼저 이야기 하자면 flash.text.Font 클래스를 사용하도록 컴파일 된다. 아래 코드를 보자.

<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="vertical" creationComplete="init()">

	<mx:Style>
		/* CSS file */
		@font-face
		{
			src:url("../assets/DinmedRegular.ttf");
			fontFamily: "DinmedRegular";
			advancedAntiAliasing: true;
		}

		@font-face
		{
			src:url("../assets/MetaPluBolRom.TTF");
			fontFamily: "MetaPluBolRom";
			advancedAntiAliasing: true;
		}

		Application
		{
			color:#ffffff;
			fontFamily : "DinmedRegular";
		}

		.myStyle
		{
			color:#ffcfff;
			fontFamily : "MetaPluBolRom";
		}
	</mx:Style>

	<mx:Label text="I'm Jidolstar"/>
	<mx:Label text="http://jidolstar.com/blog" styleName="myStyle"/>
</mx:Application>

위 코드는 어렵지 않게 해석할 수 있을 것이다. 잘 알고 있겠지만 Flex의 MXML과 CSS는 컴파일시 ActionScript 3.0으로 변환되어 진다. 위 코드에서 CSS부분만 어떻게 변경되는가 보면 다음과 같다. (참고로 컴파일 옵션으로 -keep-generated-actionscript=true 을 설정하면 아래 코드 처럼 볼 수 있겠다.)

mx_internal static var _FontTest_StylesInit_done:Boolean = false;

mx_internal function _FontTest_StylesInit():void
{
	//	only add our style defs to the StyleManager once
	if (mx_internal::_FontTest_StylesInit_done)
		return;
	else
		mx_internal::_FontTest_StylesInit_done = true;

	var style:CSSStyleDeclaration;
	var effects:Array;

	// myStyle
	style = StyleManager.getStyleDeclaration(".myStyle");
	if (!style)
	{
		style = new CSSStyleDeclaration();
		StyleManager.setStyleDeclaration(".myStyle", style, false);
	}
	if (style.factory == null)
	{
		style.factory = function():void
		{
			this.color = 0xffcfff;
			this.fontFamily = "MetaPluBolRom";
		};
	}
	// Application
	style = StyleManager.getStyleDeclaration("Application");
	if (!style)
	{
		style = new CSSStyleDeclaration();
		StyleManager.setStyleDeclaration("Application", style, false);
	}
	if (style.factory == null)
	{
		style.factory = function():void
		{
			this.color = 0xffffff;
			this.fontFamily = "DinmedRegular";
		};
	}

	StyleManager.mx_internal::initProtoChainRoots();
}

// embed carrier vars
[Embed(advancedAntiAliasing='true', mimeType='application/x-font', _pathsep='true', source='../assets/DinmedRegular.ttf', _file='D:/FontTest/src/FontTest.mxml', fontName='DinmedRegular', _line='8')]
 private var _embed__font_DinmedRegular_medium_normal_444147531:Class;

[Embed(advancedAntiAliasing='true', mimeType='application/x-font', _pathsep='true', source='../assets/MetaPluBolRom.TTF', _file='D:/FontTest/src/FontTest.mxml', fontName='MetaPluBolRom', _line='15')]
 private var _embed__font_MetaPluBolRom_medium_normal_1521687713:Class;

 

매우 신기하게 Flex 컴파일러(mxmlc)는 저렇게 바꿔준다. CSS를 적용할 때는 StyleManager 클래스를 사용하고 Font와 같은 외부 자원을 사용하는 경우에는 그냥 Embed 시켜버린다. 폰트는 @font-face가 [Embed(…)]형태로 바뀐다는 것이다. 위의 경우 애플리케이션에 CSS가 포함되는 경우이고 반대로 동적으로 CSS를 만든 경우는 어떨까? CSS를 mxmlc로 컴파일해서 사용할 수 있다는 것은 알고 있을 것이다. 다음 코드는 동적으로 CSS를 로드하는 코드이다.

 

<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="vertical" creationComplete="init()">
	<mx:Script>
		<![CDATA[
			import mx.events.StyleEvent;
			import com.starpl.net.FontEvent;
			import com.starpl.net.IFontInfo;
			import com.starpl.net.FontManager;

			private var fontInfo:IFontInfo;
			private var textField:TextField;
			private var styleEventDispatcher:IEventDispatcher;

			private function init():void
			{
				styleEventDispatcher = StyleManager.loadStyleDeclarations( "MyStyle.swf" );
				styleEventDispatcher.addEventListener( StyleEvent.COMPLETE, onComplete );
				styleEventDispatcher.addEventListener( StyleEvent.ERROR, onError );
			}

			private function onComplete( event:StyleEvent ):void
			{
			}

			private function onError( event:StyleEvent ):void
			{

			}

		]]>
	</mx:Script>
	<mx:Label text="I'm Jidolstar"/>
	<mx:Label text="http://jidolstar.com/blog" styleName="myStyle"/>
</mx:Application>

 

그럼, 위 코드에서 StyleManager.loadStyleDeclarations()에서 로드하는 MyStyle.swf는 MyStyle.css로 만들어졌다는 이야기인데 이 코드는 어떻게 변해있을까? 다음 코드는 mxmlc로 MyStyle.css를 컴파일할때 만들어지는 ActionScript 3.0 코드이다.

 

package
{
import flash.system.Security;
import flash.text.Font;
import mx.core.mx_internal;
import mx.managers.SystemManager;
import mx.modules.ModuleBase;
import mx.styles.CSSStyleDeclaration;
import mx.styles.IStyleModule;
import mx.styles.StyleManager;

[ExcludeClass]
public class MyStyle extends ModuleBase implements IStyleModule
{
    /**
     * @private
     */
    private var selectors:Array = [];
    /**
     * @private
     */
    private var overrideMap:Object = {};
    /**
     * @private
     */
    private var effectMap:Object = {};
    /**
     * @private
     */
    private var unloadGlobal:Boolean;

    /**
     * @private
     */
    private static var domainsAllowed:Boolean = allowDomains();

    /**
     * @private
     */
    private static function allowDomains():Boolean
    {
		// allowDomain not allowed in AIR
		if (Security.sandboxType != "application")
			Security.allowDomain("*");
        return true;
    }

    [Embed(advancedAntiAliasing='true', mimeType='application/x-font', _pathsep='true', source='../assets/DinmedRegular.ttf', _file='D:/FontTest/src/MyStyle.css', fontName='DinmedRegular', _line='4')]
    private static var _embed__font_DinmedRegular_medium_normal_731457564:Class;
    [Embed(advancedAntiAliasing='true', mimeType='application/x-font', _pathsep='true', source='../assets/MetaPluBolRom.TTF', _file='D:/FontTest/src/MyStyle.css', fontName='MetaPluBolRom', _line='11')]
    private static var _embed__font_MetaPluBolRom_medium_normal_1234377680:Class;

    public function MyStyle()
    {
        var style:CSSStyleDeclaration;
        var keys:Array;
        var selector:String;
        var effects:Array;
        var addedEffects:Array;

        selector = ".myStyle";

        style = StyleManager.getStyleDeclaration(selector);

        if (!style)
        {
            style = new CSSStyleDeclaration();
            StyleManager.setStyleDeclaration(selector, style, false);
            selectors.push(selector);
        }

        keys = overrideMap[selector];

        if (keys == null)
        {
            keys = [];
            overrideMap[selector] = keys;
        }

        style.mx_internal::setStyle('color', 0xffcfff);
        keys.push("color");
        style.mx_internal::setStyle('fontFamily', "MetaPluBolRom");
        keys.push("fontFamily");

        selector = "Application";

        style = StyleManager.getStyleDeclaration(selector);

        if (!style)
        {
            style = new CSSStyleDeclaration();
            StyleManager.setStyleDeclaration(selector, style, false);
            selectors.push(selector);
        }

        keys = overrideMap[selector];

        if (keys == null)
        {
            keys = [];
            overrideMap[selector] = keys;
        }

        style.mx_internal::setStyle('color', 0xffffff);
        keys.push("color");
        style.mx_internal::setStyle('fontFamily', "DinmedRegular");
        keys.push("fontFamily");

        Font.registerFont(_embed__font_DinmedRegular_medium_normal_731457564);
        Font.registerFont(_embed__font_MetaPluBolRom_medium_normal_1234377680);
    }

    public function unload():void
    {
        unloadOverrides();
        unloadStyleDeclarations();

        if (unloadGlobal)
        {
            StyleManager.mx_internal::stylesRoot = null;
            StyleManager.mx_internal::initProtoChainRoots();
        }
    }

    /**
     * @private
     */
    private function unloadOverrides():void
    {
        for (var selector:String in overrideMap)
        {
            var style:CSSStyleDeclaration = StyleManager.getStyleDeclaration(selector);

            if (style != null)
            {
                var keys:Array = overrideMap[selector];
                var numKeys:int;
                var i:uint;

                if (keys != null)
                {
                    numKeys = keys.length;

                    for (i = 0; i < numKeys; i++)
                    {
                        style.mx_internal::clearOverride(keys[i]);
                    }
                }

                keys = effectMap[selector];

                if (keys != null)
                {
                    numKeys = keys.length;
                    var index:uint;
                    var effects:Array = style.mx_internal::effects;

                    for (i = 0; i < numKeys; i++)
                    {
                        index = effects.indexOf(numKeys[i]);
                        if (index >= 0)
                        {
                            effects.splice(index, 1);
                        }
                    }
                }
            }
        }

        overrideMap = null;
        effectMap = null;
    }

    /**
     * @private
     */
    private function unloadStyleDeclarations():void
    {
        var numSelectors:int = selectors.length;

        for (var i:int = 0; i < numSelectors; i++)
        {
            var selector:String = selectors[i];
            StyleManager.clearStyleDeclaration(selector, false);
        }

        selectors = null;
    }
}

}

 

위 코드는 CSS코드가 ActionScript 3.0으로 만들어진 것을 보여준다. 재미있게도 ModuleBase 클래스를 확장했다. 또한 IStyleModule을 구현했다. StyleManager.loadStyleDeclaration()을 호출할때 ModuleManager를 이용해 이 MyStyle.swf를 로드한다는 것을 의미한다. 실제로 StyleManager를 살펴보면 ModuleManager 활용해서 CSS를 동적으로 로드한다. 또한 CSS로 SWF로 만들면 내부적으로 StyleManager를 이용해 Style을 적용하고 폰트도 Embed를 통해서 포함하고 있다. 코드를 보면 Embed된 폰트를 Font.registerFont()함수를 이용해서 등록하고 있는 것 또한 확인할 수 있다. 살펴본 결과만 보면 Flex가 ActionScript 3.0 기반이고 Flex는 개발의 효율성을 증대시키기 위한 일종의 프레임워크이다라는 것을 쉽게 이해시켜주는 부분이 아닐가 싶다.

 

 

2. FontLoader 만들기 – 잘못 제작된 클래스

 

앞서 Flex SDK를 살펴본 결과 폰트를 동적으로 로드해서 등록하는 것은 생각보다 어려울 것 같지 않다. 결국 Font.registerFont()를 이용하면 되는 것이다. 그런데 본인은 Flex SDK를 이용하지 않고 순수 ActionScript 3.0 환경에서도 Font를 동적으로 로드해서 사용할 수 있도록 하는 환경을 만들고 싶다. 이런 환경을 구축하기 위한 조건은 다음과 같다.

 

  • 조건 1 : ActionScript 3.0 환경에서도 정상적으로 동작하도록 한다.
  • 조건 2 : 이미 로드된 폰트는 다시 로드하지 않는다.

 

조건 2는 당연하다. 이미 로드되어 Font.registerFont()를 이용해 등록된 폰트를 또 로드할 필요가 없지 않는가? 이 조건에 따르도록 FontLoader 클래스를 작성해보았다.

 

package com.jidolstar.fonts
{
	import flash.display.Loader;
	import flash.events.ErrorEvent;
	import flash.events.Event;
	import flash.events.EventDispatcher;
	import flash.events.IOErrorEvent;
	import flash.events.SecurityErrorEvent;
	import flash.net.URLRequest;
	import flash.system.ApplicationDomain;
	import flash.system.LoaderContext;
	import flash.system.Security;
	import flash.system.SecurityDomain;
	import flash.text.Font;
	import flash.utils.describeType;    

	[Event(name="loaded", type="com.starpl.net.FontLoaderEvent")]
	[Event(name="error", type="com.starpl.net.FontLoaderEvent")]

	/**
	 * 폰트를 담은 SWF를 동적으로 로딩하고 로드한 SWF로 부터 Embed 폰트의 Class를 시스템에 등록하는 역할을 한다.
	 *
	 * <p>폰트를 담은 SWF를 만드는 방법</p>
	 * <ul>
	 * <li> 아래 예제처럼 fontName(JidolstarFont)은 클래스이름, AS파일 이름과 같아야한다.
	 * <pre>
	 * 		package
	 * 		{
	 * 			import flash.display.Sprite;
	 * 			public class JidolstarFont extends Sprite
	 * 			{
	 * 				[Embed(	mimeType='application/x-font',
	 * 						source='../../assets/MetaPluBolRom.TTF',
	 * 						fontName='JidolstarFont',
	 * 						unicodeRange='U+0041,U+0044,U+0059,U+0061,U+0064,U+0079,U+0030-U+0039,U+002B,U+002D'
	 * 				)]
	 * 				static public var FontClass:Class;
	 * 			}
	 * 		}
	 * </pre>
	 * <li> FontClass 이름은 바뀌면 안된다.
	 * <li> mxmlc등을 이용해 단독으로 컴파일해서 swf파일을 만든다.
	 * </ul>
	 *
	 * <p>클래스를 사용하는 방법 </p>
	 * <pre>
	 * 	var testFontName:String = "HYNamuB";
	 *	var textField:TextField = new TextField;
	 *	textField.text = "안녕하세요";
	 *	addChild( textField );
	 *
	 *	textField.x = 50;
	 *	textField.y = 50;
	 *
	 *	fontLoader = new FontLoader( testFontName+".swf", testFontName );
	 *	fontLoader.addEventListener( FontLoaderEvent.LOADED, onFontLoaded );
	 *	fontLoader.addEventListener( FontLoaderEvent.ERROR, onFontLoadError );
	 *	fontLoader.load();
	 *
 	 *	private function onFontLoaded( event:FontLoaderEvent ):void
	 *	{
	 *		//Embed 처리된 폰트 리스팅
	 *		var embededfonts:Array = Font.enumerateFonts( false );
	 *		for each( var embededfont:Font in embededfonts )
	 *		{
	 *			trace( 	"fontName="+embededfont.fontName+
	 *					",fontType="+embededfont.fontType+
	 *					",fontStyle="+embededfont.fontStyle+
	 *					",hasGlyphs = "+embededfont.hasGlyphs( "안녕하세요" ) );
	 *		}
	 *
	 *		var textFormat:TextFormat = new TextFormat(testFontName, 14)
	 *		textField.embedFonts = true;
	 *		textField.setTextFormat( textFormat );
	 *	}
	 *
	 *	private function onFontLoadError( event:FontLoaderEvent ):void
	 *	{
 	 *		trace( event.fontName, event.fontURL, event.message );
	 *	}
	 * </pre>
	 *
	 * @author Yongho Ji(jidolstar[at]gmail.com)
	 * @since 2009.03.06
	 */
	public class FontLoader extends EventDispatcher
	{
		private var loader:Loader;
		private var loaderContext:LoaderContext;

		private var _fontURL:String;
		private var _fontName:String;

		/**
		 * 생성자
		 * @param fontURL 폰트를 로드할 경로
		 * @param fontName 폰트이름
		 */
		public function FontLoader( fontURL:String, fontName:String )
		{
			super();
			this._fontURL = fontURL;
			this._fontName = fontName;
		}

		/**
		 * 폰트를 로드할 URL
		 */
		public function get fontURL():String
		{
			return _fontURL;
		}

		/**
		 * 폰트 이름
		 */
		public function get fontName():String
		{
			return _fontName;
		}

		/**
		 * 해당폰트가 등록되어 있는지 검사후에 폰트 로드 실시
		 * 등록되어 있는 경우 loaded 이벤트 송출됨.
		 * 등록되어 있지 않은 경우 로드요청.
		 */
		public function load():void
		{
			//해당 폰트가 이미 등록되어 있는지 조사
            var currentFonts:Array = Font.enumerateFonts( false );
            var hasFont:Boolean = false;
            for each ( var f:Font in currentFonts )
            {
                if( fontName == f.fontName )
                {
                    hasFont = true;
                    break;
                }
            }		

            //폰트가 등록되어 있다면 Loaded이벤트를 날려준다!
            if( hasFont )
            {
            	this.dispatchEvent(new FontLoaderEvent(FontLoaderEvent.LOADED, _fontName, _fontURL, null ));
            }
            //폰트가 등록되어  있지 않다면 폰트 로드를 시도한다.
            else
            {
				if( !loader )
				{
					loader = new Loader();
				}
				if( !loaderContext )
				{
					loaderContext = new LoaderContext();
					loaderContext.checkPolicyFile = true;
					loaderContext.applicationDomain = new ApplicationDomain( ApplicationDomain.currentDomain );
					if ( Security.sandboxType == Security.REMOTE)
					{
					    loaderContext.securityDomain = SecurityDomain.currentDomain;
					}
					else
					{
				        loaderContext.securityDomain = null;
					}
				}
				loader.contentLoaderInfo.addEventListener( Event.COMPLETE, onLoaded, false, 0, true);
            	loader.contentLoaderInfo.addEventListener( IOErrorEvent.IO_ERROR, onError, false, 0, true);
            	loader.contentLoaderInfo.addEventListener( SecurityErrorEvent.SECURITY_ERROR, onError, false, 0, true);
            	loader.load( new URLRequest( _fontURL ), loaderContext );
            }
		}

		/**
		 * 로드 완료시 해당 폰트가 있는지 확인후 등록한다.
		 */
		private function onLoaded( event:Event ):void
		{
			if( loader.contentLoaderInfo.applicationDomain.hasDefinition( _fontName ) )
			{
				//폰트 등록
				var fontClass:Class = loader.contentLoaderInfo.applicationDomain.getDefinition( _fontName ) as Class;
				Font.registerFont( fontClass.FontClass );
				this.dispatchEvent( new FontLoaderEvent( FontLoaderEvent.LOADED, _fontName, _fontURL ) );
			}
			else
			{
				//해당폰트를 찾을 수 없으므로 실패 이벤트 송출
				this.dispatchEvent( new FontLoaderEvent( FontLoaderEvent.ERROR, _fontName, _fontURL, "Font Load fail" ) );
			}
			clearLoader();
		}

		/**
		 * 폰트 로드 실패시 처리
		 */
		private function onError( event:ErrorEvent ):void
		{
			this.dispatchEvent( new FontLoaderEvent( FontLoaderEvent.ERROR, _fontName, _fontURL, event.text ) );
			clearLoader();
		}

		/**
		 * 로더 삭제
		 */
	    private function clearLoader():void
	    {
	        if (loader)
	        {
	            if (loader.contentLoaderInfo)
	            {
	                loader.contentLoaderInfo.removeEventListener( Event.COMPLETE, onLoaded, false );
	                loader.contentLoaderInfo.removeEventListener( IOErrorEvent.IO_ERROR, onError, false );
	                loader.contentLoaderInfo.removeEventListener( SecurityErrorEvent.SECURITY_ERROR, onError, false );
	            }

	            try
	            {
	            	loader.close();
	                loader.unload();
	            }
	            catch(error:Error)
	            {
	            }
	            loader = null;
	            loaderContext = null;
	        }
	    }		

	}
}

 

FontLoader 클래스를 사용한 예제는 다음과 같다.

 

package
{
	import com.jidolstar.fonts.FontLoader;
	import com.jidolstar.fonts.FontLoaderEvent;

	import flash.display.Sprite;
	import flash.display.StageAlign;
	import flash.display.StageScaleMode;
	import flash.text.Font;
	import flash.text.TextField;
	import flash.text.TextFormat;

	/**
	 * FontLoader 테스터
	 * @author Yongho, Ji
	 * @since 2009.03.17
	 */
	public class FontTest extends Sprite
	{
		private var fontLoader:FontLoader;
		private var testFontName:String = "HYNamuB";

		private var textField:TextField;

		public function FontTest()
		{
			super();

			stage.scaleMode = StageScaleMode.NO_SCALE;
			stage.align = StageAlign.TOP_LEFT;

			textField = new TextField;
			textField.text = "안녕하세요";
			addChild( textField );

			textField.x = 50;
			textField.y = 50;

			fontLoader = new FontLoader( testFontName+".swf", testFontName );
			fontLoader.addEventListener( FontLoaderEvent.LOADED, onFontLoaded );
			fontLoader.addEventListener( FontLoaderEvent.ERROR, onFontLoadError );
			fontLoader.load();
		}

		private function onFontLoaded( event:FontLoaderEvent ):void
		{
			//Embed 처리된 폰트 리스팅
			var embededfonts:Array = Font.enumerateFonts( false );
			for each( var embededfont:Font in embededfonts )
			{
				trace( 	"fontName="+embededfont.fontName+
						",fontType="+embededfont.fontType+
						",fontStyle="+embededfont.fontStyle+
						",hasGlyphs = "+embededfont.hasGlyphs( "안녕하세요" ) );
			}

			var textFormat:TextFormat = new TextFormat(testFontName, 14)
			textField.embedFonts = true;
			textField.setTextFormat( textFormat );
		}

		private function onFontLoadError( event:FontLoaderEvent ):void
		{
			trace( event.fontName, event.fontURL, event.message );
		}
	}
}

 

따로 분석은 하지 않겠다. 위에서 사용한 소스는 아래 링크를 통해 다운로드 받아볼 수 있다. Flex Builder 3에 import 해서 테스트 해보길 바란다.

 

위 소스는 FontLoader 클래스를 이용해 Embed폰트를 담은 SWF를 로드해 Font.registerFont()를 이용해 등록하고 나름대로 똑같은 Font는 등록이 되지 않도록 노력했다. 실제로 매우 잘 돌아가는 소스이다.
하지만 이 코드에는 맹점이 존재한다. load() 함수 호출시에 같은 폰트(같은 url)를 2번 이상 로드하는 경우에 그대로 2번 로드될 수 있다. load() 함수 앞에 이전에 등록된 폰트가 없나 검사를 하고 있지만 비동기적으로 폰트가 로드되는 것이므로 “조건 2. 이미 로드된 폰트는 다시 로드하지 않는다.”를 지키지 못한 것이다.(지킨줄 알았는데… ㅎㅎ) 폰트는 왠만하면 거의 100kb 이상이다. 한글의 경우 300kb가 되는 경우도 많다. 보통 Flex 애플리케이션도 500kb를 넘지 않는 마당에 이렇게 큰 용량의 폰트가 2번이상 로드하게 된다면… 말 안해도 알거다.
 
“그럼 개발자가 같은 폰트를 로드하지 않도록 1개만 로드하고 다음에는 로드를 시도하지 않도록 잘~ 만들면 되지 않느냐?” 라고 반문할 수 있다. 하지만 이런 문제를 알면서도 잊고 그냥 넘어가 버리는 경우가 발생할 수 있다. 또한 다음과 같은 경우도 있다. 가령, 이 FontLoader를 사용하는 위젯 SWF가 3~4개가 있다. 이들 위젯을 관리하는 애플리케이션이 각각의 위젯 SWF를 거의 동시에 로드한다. 로드된 위젯들은 FontLoader를 이용해 해당 폰트를 로드한다. 물론 FontLoader는 애플리케이션의 ApplicationDomain에 위치하고 있고 각각의 위젯은 이 ApplicationDomain내에 정의된 FontLoader를 이용하는 것이다. 이 경우 위젯들에서 사용하는 폰트가 중복되지 않으리라는 보장을 어떻게 할 것인가? 또한 위젯들에게 “내가 이 폰트 로드했으니깐 너는 로드하지마!” 라고 어떻게 전달할 것인가? 결국 잘못 만들어진 클래스 하나로 애플리케이션의 퍼포먼스는 저하될 수 밖에 없는 것이다. 이는 FontLoader 클래스를 만든 개발자 잘못인 것이다. 조건에 맞게 제대로 만들지 못했다는 것을 의미한다. 이러한 FontLoader를 쓰라고? 너무 무책임해지는 것이다.
 

3. FontManager 만들기 – 중복으로 폰트 로드하는 것을 완벽하게 방지

위에서 FontLoader의 문제점을 살펴보았다. 생각보다 간단히 해결될 것 같지는 않다. 결국 약간의 디자인(설계) 패턴을 응용해야한다. 본인은 이 문제를 해결하기 위해 Singleton 디자인 패턴Proxy 디자인 패턴을 활용하고자 한다.

 

Singleton 패턴은 주로 제한된 객체를 생성하기 위해 사용한다. 이름 자체만 보더라도 1개의 객체만을 허용하는 클래스를 만드는 것을 목적으로 한다.

 

Proxy 패턴은 말그대로 대리자 클래스를 만들어 사용하는 것을 의미한다. 기존 클래스를 그대로 사용하되 부가적인 기능을 더하는데 사용하는 설계 패턴으로 생각하면 되겠다.

 

참고로 이런 디자인 패턴은 GoF의 23가지 디자인 패턴에 포함하므로 필요할 때 학습해두면 클래스 설계시 매우 유용할 것이다. 2번의 조건(중복로드방지, AS3 프로젝트에서 실행가능)을 가능하게 하기 위해 FontManager를 만들었다. FontManager는 크게 3개 ActionScript 코드로 이뤄진다. IFontInfo.as, FontManager.as, FontEvent.as가 그것이다. 아래는 IFontInfo.as 이다. IFontInfo 인터페이스이며 사용자는 이 인터페이스를 이용해서 Font를 로드한다.

 

package com.jidolstar.fonts
{
	import flash.events.IEventDispatcher;

	/**
	 * Font 컨텐츠를 로드하는 중간 에러 발생시 송출된다.
	 * @eventType  com.jidolstar.fonts.FontEvent.ERROR
	 */
	[Event(name="error", type="com.jidolstar.fonts.FontEvent")]

	/**
	 * Font 컨텐츠를 로드하고 있는 중일때 송출
	 * @eventType  com.jidolstar.fonts.FontEvent.PROGRESS
	 */
	[Event(name="progress", type="com.jidolstar.fonts.FontEvent")]

	/**
	 * Font 컨텐츠를 로드를 완료하고 애플리케이션에 폰트 적용이 완료되었을때 송출
	 * @eventType  com.jidolstar.fonts.FontEvent.COMPLETE
	 */
	[Event(name="complete", type="com.jidolstar.fonts.FontEvent")]

	/**
	 * Font 컨텐츠가 로드 준비가 완료되었을때 송출
	 * @eventType  com.jidolstar.fonts.FontEvent.SETUP
	 */
	[Event(name="setup", type="com.jidolstar.fonts.FontEvent")]

	/**
	 * 폰트 로드를 하기 위한 인터페이스
	 */
	public interface IFontInfo extends IEventDispatcher
	{
		/**
		 * Font 컨텐츠 로드 실시하면 true
		 */
		function get start():Boolean;		

		/**
		 * 에러발생시 true
		 */
		function get error():Boolean;

		/**
		 * Font 컨텐츠 로드 완료되고 적용되었을때 true
		 */
		function get complete():Boolean;

		/**
		 * Font 컨텐츠 로드 준비가 완료되었을때 true
		 */
		function get setup():Boolean;

		/**
		 * Font 컨텐츠를 로드할 URL
		 */
		function get url():String;

		/**
		 * Font 컨텐츠 로드 시작
		 */
		function load():void;
	}
}

 

아래는 FontEvent.as 파일이다.

 

package com.jidolstar.fonts
{
	import flash.events.Event;
	import flash.events.ProgressEvent;

	/**
	 * 폰트 이벤트
	 * @author Yongho, Ji
	 * @since 2009.03.09
	 */
	public class FontEvent extends ProgressEvent
	{
		/**
		 * 로드 성공시
		 */
		public static const COMPLETE:String = "complete";
		/**
		 * 로드 설정시
		 */
		public static const SETUP:String = "setup";

		/**
		 * 로드 진행시
		 */
		public static const PROGRESS:String = "progress";

		/**
		 * 로드 실패시
		 */
		public static const ERROR:String = "error";

		/**
		 * 에러 메시지
		 */
		public var errorText:String;

		/**
		 * 폰트 정보
		 */
		public var fontInfo:IFontInfo;

		/**
		 * 폰트 로더 이벤트
		 * @param type 이벤트 Type
		 * @param bubbles
		 * @param cancelable
		 * @param bytesLoaded
		 * @param bytesTotal
		 * @param errorText
		 * @param fontInfo
		 */
		public function FontEvent(type:String, bubbles:Boolean = false, cancelable:Boolean = false,
								bytesLoaded:uint = 0, bytesTotal:uint = 0,
								errorText:String = null, fontInfo:IFontInfo = null )
		{
			super(type, bubbles, cancelable, bytesLoaded, bytesTotal)
			this.errorText = errorText;
			this.fontInfo = fontInfo;
		}

		/**
		 * 이벤트 클론
		 */
		public override function clone():Event
		{
			return new FontEvent( type, bubbles, cancelable, bytesLoaded, bytesTotal, errorText, fontInfo );
		}
	}
}

 

마지막으로 FontManager.as이다

 

package com.jidolstar.fonts
{
	/**
	 * Font를 동적으로 로드해서 적용하기 위한 매니저이다.
	 * Font 컨텐츠는 SWF로 존재하며 이 SWF을 로드하기 위한 URL이 있다면 사용자는 FontManager.getInstance().getFontInfo( url )로
	 * IFontInfo 인스턴스를 얻을 수 있으며 폰트 로드/적용 되는 상황을 FontEvent로 받을 수 있다.
	 *
	 * @author Yongho, Ji
	 * @since 2009.03.09
	 */
	public class FontManager
	{
		private static var _singleton:FontManager;

		private var fontInfoList:Object = {};

		/**
		 * 싱글턴을 위한 생성자
		 */
		public function FontManager( singletonForce:SingletonForce )
		{
		}

		/**
		 * 싱글턴 인스턴스 생성
		 */
		public static function getInstance():FontManager
		{
			if( !_singleton )
			{
				_singleton = new FontManager( new SingletonForce );
			}
			return _singleton;
		}

		/**
		 * URL에 대한 Font 정보를 얻오온다.
		 */
		public function getFontInfo( url:String ):IFontInfo
		{
			var info:FontInfo = fontInfoList[url] as FontInfo;
			if( !info )
			{
				info = new FontInfo( url );
				fontInfoList[url] = info;
			}
			return new FontInfoProxy( info );
		}
	}
}

import com.jidolstar.fonts.FontEvent;
import com.jidolstar.fonts.IFontInfo;

import flash.display.DisplayObject;
import flash.display.Loader;
import flash.events.ErrorEvent;
import flash.events.Event;
import flash.events.EventDispatcher;
import flash.events.IOErrorEvent;
import flash.events.ProgressEvent;
import flash.events.SecurityErrorEvent;
import flash.net.URLRequest;
import flash.system.ApplicationDomain;
import flash.system.LoaderContext;
import flash.system.Security;
import flash.system.SecurityDomain;
import flash.utils.getQualifiedClassName;

/**
 * @private
 * FontManager 싱글톤 패턴을 위한 클래스
 */
class SingletonForce {}

/**
 * @private
 * 폰트를 담은 컨텐트를 로드하고 관리하는 클래스
 */
class FontInfo extends EventDispatcher
{
	private var _url:String;

	private var loader:Loader;
	private var _error:Boolean = false;
	private var _start:Boolean = false;
	private var _complete:Boolean = false;
	private var _setup:Boolean = false;
	private var _bytesTotal:int = 0;
	private var reference:DisplayObject;
	private var applicationDomain:ApplicationDomain;

	public function FontInfo( url:String )
	{
		super();
		_url = url;
	}

	public function get start():Boolean
	{
		return _start;
	}

    public function get error():Boolean
    {
        return _error;
    }

    public function get complete():Boolean
    {
    	return _complete;
    }	

    public function get setup():Boolean
    {
    	return _setup;
    }	    

    public function get url():String
    {
        return _url;
    }

    public function get size():int
    {
    	return _bytesTotal;
    }	

	public function load():void
	{
		if( _start )
			return;

		_start = true; 

		var r:URLRequest = new URLRequest( _url );
		var c:LoaderContext = new LoaderContext;
		c.applicationDomain = new ApplicationDomain( ApplicationDomain.currentDomain );
		if( Security.sandboxType == Security.REMOTE )
		{
			c.securityDomain = SecurityDomain.currentDomain;
		}

		loader = new Loader;
        loader.contentLoaderInfo.addEventListener( Event.INIT, initHandler);
        loader.contentLoaderInfo.addEventListener( Event.COMPLETE, completeHandler);
        loader.contentLoaderInfo.addEventListener( ProgressEvent.PROGRESS, progressHandler);
        loader.contentLoaderInfo.addEventListener( IOErrorEvent.IO_ERROR, errorHandler);
        loader.contentLoaderInfo.addEventListener( SecurityErrorEvent.SECURITY_ERROR, errorHandler);

        loader.load(r, c);
	}

    /**
     *  @private
     */
    private function clearLoader():void
    {
        if (loader)
        {
            if (loader.contentLoaderInfo)
            {
                loader.contentLoaderInfo.removeEventListener(
                    Event.INIT, initHandler);
                loader.contentLoaderInfo.removeEventListener(
                    Event.COMPLETE, completeHandler);
                loader.contentLoaderInfo.removeEventListener(
                    ProgressEvent.PROGRESS, progressHandler);
                loader.contentLoaderInfo.removeEventListener(
                    IOErrorEvent.IO_ERROR, errorHandler);
                loader.contentLoaderInfo.removeEventListener(
                    SecurityErrorEvent.SECURITY_ERROR, errorHandler);
            }

            if (_complete)
            {
                try
                {
                    loader.close();
                }
                catch(error:Error)
                {
                }
            }

            try
            {
                loader.unload();
            }
            catch(error:Error)
            {
            }

            loader = null;
        }
    }	

    /**
     *  @private
     */
    public function initHandler(event:Event):void
    {
		reference = loader.content;

		if( !reference )
		{
            var fontEvent:FontEvent = new FontEvent(
                FontEvent.ERROR, event.bubbles, event.cancelable);
            fontEvent.bytesLoaded = 0;
            fontEvent.bytesTotal = 0;
            fontEvent.errorText = "SWF is not a loadable Font";
            dispatchEvent(fontEvent);
            return;
		}

		applicationDomain = loader.contentLoaderInfo.applicationDomain;

        _setup = true;

        dispatchEvent(new FontEvent(FontEvent.SETUP));
    }

    /**
     *  @private
     */
    public function progressHandler(event:ProgressEvent):void
    {
        var fontEvent:FontEvent = new FontEvent(
            FontEvent.PROGRESS, event.bubbles, event.cancelable);
        fontEvent.bytesLoaded = event.bytesLoaded;
        fontEvent.bytesTotal = event.bytesTotal;
        dispatchEvent(fontEvent);
    }

    /**
     *  @private
     */
    public function completeHandler(event:Event):void
    {
        _complete = true;
        var fontEvent:FontEvent;

		try
		{
	        var className:String = getQualifiedClassName( reference );
	        var mainClass:Class = applicationDomain.getDefinition( className ) as Class;
	        new mainClass();
	        fontEvent = new FontEvent( FontEvent.COMPLETE, event.bubbles, event.cancelable);
	        fontEvent.bytesLoaded = loader.contentLoaderInfo.bytesLoaded;
	        fontEvent.bytesTotal = loader.contentLoaderInfo.bytesTotal;
	        dispatchEvent(fontEvent);
	 	}
	 	catch( e:Error )
	 	{
	        fontEvent = new FontEvent( FontEvent.ERROR, event.bubbles, event.cancelable);
	        fontEvent.errorText = e.message;
	        dispatchEvent(fontEvent);
	 	}

        clearLoader();
    }

    /**
     *  @private
     */
    public function errorHandler(event:ErrorEvent):void
    {
        _error = true;
        var fontEvent:FontEvent = new FontEvent(
            FontEvent.ERROR, event.bubbles, event.cancelable);
        fontEvent.bytesLoaded = 0;
        fontEvent.bytesTotal = 0;
        fontEvent.errorText = event.text;
        dispatchEvent(fontEvent);
    }
}

/**
 * @private
 * FontInfo 클래스를 한번 wraping하고 IFontInfo를 구현한 클래스이다.
 *
 *
 * 사용자는 FontManager.getFontInfo( url ) 을 이용해 이 클래스의 인스턴스를 얻게된다.
 * 한개의 url에 대해 여러개의 FontInfoProxy 인스턴스가 생성될 수 있다.
 * 하지만 같은 url을 참조한 FontInfoProxy 인스턴스에 대해서는 단 한개의 FontInfo 인스턴스만 참조하도록 한다.
 * 즉, URL에 대해서는 한개의 FontInfo가 된다.
 *
 *
 * 이런 Proxy를 만든 이유는 폰트를 담은 컨텐츠를 url을 비교하여 중복 로드하는 것을 방지하기 위함이다.
 * 사용자는  FontInfoProxy 존재를 알 필요없이 IFontInfo로 접근 접근만을 허용한다.
 * 따로 Unload는 구현하지 않는다. 왜냐하면 Font가 등록되면 삭제되지는 않기 때문이다.
 *
 */
class FontInfoProxy extends EventDispatcher implements IFontInfo
{
	private var info:FontInfo;

	public function FontInfoProxy( info:FontInfo )
	{
		this.info = info;	

		info.addEventListener( FontEvent.SETUP, fontEventHandler, false, 0, true );
		info.addEventListener( FontEvent.PROGRESS, fontEventHandler, false, 0, true );
		info.addEventListener( FontEvent.COMPLETE, fontEventHandler, false, 0, true );
		info.addEventListener( FontEvent.ERROR, fontEventHandler, false, 0, true );
	}

    /**
     *  @private
     */
    public function get error():Boolean
    {
        return info.error;
    }

    /**
     * @private
     */
    public function get start():Boolean
    {
		return info.start;
    }    

    /**
     *  @private
     */
    public function get complete():Boolean
    {
        return info.complete;
    }

    /**
     *  @private
     */
    public function get setup():Boolean
    {
        return info.setup;
    }       

    /**
     *  @private
     */
    public function get url():String
    {
        return info.url;
    }     

	/**
	 * Font Load 실시
	 * 이미 로드가 진행진행중이거나 완료한 상태라면 따로 load를 실시하지 않도록 한다.
	 * 즉, 실제 로드는 한번만 실시한다.
	 */
	public function load():void
	{
		if( info.error )
		{
			dispatchEvent( new FontEvent( FontEvent.ERROR ) );
		}
		else if( info.start )
		{
			if( info.setup )
			{
				dispatchEvent( new FontEvent( FontEvent.SETUP ) );
				if( info.complete )
				{
					var fontEvent:FontEvent;
					fontEvent = new FontEvent( FontEvent.PROGRESS );
					fontEvent.bytesTotal = info.size;
					fontEvent.bytesLoaded = info.size;
					dispatchEvent( fontEvent );

					fontEvent = new FontEvent( FontEvent.COMPLETE );
					fontEvent.bytesTotal = info.size;
					fontEvent.bytesLoaded = info.size;
					dispatchEvent( fontEvent );
				}
			}
		}
		else
		{
			info.load();
		}
	}

    /**
     *  @private
     */
    private function fontEventHandler(event:FontEvent):void
    {
        dispatchEvent(event);
    }
}

 

조금만 정성을 들인다면 어렵지 않게 해석할 수 있다. 앞서 설명했듯이 Proxy 패턴Singleton 패턴을 섞어서 제작했다. Flex SDK의 ModuleManager에서 Factory 부분이 빠진 구조로 볼 수 있다. 이렇게 만든 클래스 구조는 "조건 2. 이미 로드된 폰트는 다시 로드하지 않는다" 를 만족한다. 사용법은 매우 단순하다. FontManager.getInstance().getFontInfo( url ) 을 사용해서 IFontInfo 형태의 객체를 받아 로드 상태를 알기 위해 FontEvent에 정의된 이벤트 청취자를 등록하고 마지막으로 IFontInfo의 load()함수를 호출하면 해당 폰트가 로딩되고 애플리케이션에 적용된다. 폰트는 아래와 같은 방법으로 ActionScript 3.0으로 만든다.

 

package
{
	import flash.display.Sprite;
	import flash.text.Font;

	public class MyFont extends Sprite
	{
		[Embed(	mimeType='application/x-font', source='../assets/DinmedRegular.ttf', fontName='DinmedRegular', embedAsCFF=false)]
		static public var FontClass1:Class; 

		[Embed(	mimeType='application/x-font', source='../assets/MetaPluBolRom.TTF', fontName='MetaPluBolRom', embedAsCFF=false)]
		static public var FontClass2:Class;

		public function MyFont()
		{
			super();
			Font.registerFont( FontClass1 );
			Font.registerFont( FontClass2 );
		}
	}
}

 

폰트를 Embed하고 flash.text.Font의 registerFont() 함수를 이용해 Embed된 폰트를 등록해준다. 폰트는 위처럼 2개든 여러개든 상관없다. Embed한 폰트수만큼 등록하면 되겠다. 사용하기 위해 이 클래스를 mxmlc로 컴파일한다. 그런 다음 아래 예제처럼 FontManager를 이용해 로드해서 폰트를 애플리케이션에 적용할 수 있다.

 

package {
	import com.jidolstar.fonts.FontEvent;
	import com.jidolstar.fonts.FontManager;
	import com.jidolstar.fonts.IFontInfo;

	import flash.display.Sprite;
	import flash.display.StageAlign;
	import flash.display.StageScaleMode;
	import flash.text.TextField;
	import flash.text.TextFormat;

	public class FontManagerTest extends Sprite
	{
		private var fontInfo1:IFontInfo;
		private var fontInfo2:IFontInfo;
		private var textField1:TextField;
		private var textField2:TextField;		

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

			fontInfo1 = FontManager.getInstance().getFontInfo( "MyFont.swf" );
			fontInfo1.addEventListener( FontEvent.COMPLETE, onComplete );
			fontInfo1.addEventListener( FontEvent.ERROR, onError );
			fontInfo1.load();

			//fontInfo1과 같은 경로의 SWF를 로드한다.
			//fontInfo1.load()를 한번 시도했기 때문에 fontInfo2.load()에서는 실제로드하지 않는다.
			//다만, 사용자는 이벤트를 통해 로드 되는 것처럼 사용하는 것 뿐이다. 즉, 인터페이스는 계속 유지시켜줌으로써 사용자의 오용을 방지시킨다.
			fontInfo2 = FontManager.getInstance().getFontInfo( "MyFont.swf" );
			fontInfo2.addEventListener( FontEvent.COMPLETE, onComplete );
			fontInfo2.addEventListener( FontEvent.ERROR, onError );
			fontInfo2.load();	

			textField1 = new TextField;
			textField1.embedFonts = true;
			textField1.defaultTextFormat = new TextFormat( "DinmedRegular" );
			textField1.text = "I'm jidolstar";
			textField1.x = 10;
			textField1.y = 10;
			textField1.rotation = 10;
			addChild( textField1 );	

			textField2 = new TextField;
			textField2.embedFonts = true;
			textField2.defaultTextFormat = new TextFormat( "MetaPluBolRom" );
			textField2.text = "http://jidolstar.com";
			textField2.x = 10;
			textField2.y = 30;
			textField2.rotation = 10;
			addChild( textField2 );
		}

		private function onComplete( event:FontEvent ):void
		{
			trace( (event.target as IFontInfo).url );
		}

		private function onError( event:FontEvent ):void
		{

		}
	}
}

실행결과 

 

 

이것으로 폰트를 동적으로 로딩하여 ActionScript 3.0 프로젝트에도 쉽게 적용할 수 있게 되었다. Flex에도 적용할 수 있다. 물론 CSS형태의 편의성은 제공하지 못하지만 중복으로 로드하지 않는 장점이 있기 때문에 사용할 가치는 있다.  

 

소스코드는 아래 링크에서 다운로드 받는다. Flex Builder 3 이상에서 Import 해서 테스트 해보면 되겠다.

 

 

정리하며...

ActionScript 3.0 기반 애플리케이션에 동적로딩을 통해 폰트를 시스템에 적용시켜주는 FontManager를 제작해보았다. 하나의 URL에 따라 중복으로 로딩되지 않도록 하고 Flex가 아닌 ActionScript 3.0 프로젝트 진행에 도움이 되도록 했다. Flex의 좋은 프레임워크를 놔두고 이런 클래스를 만드는 이유는 Flex는 대형 프로젝트의 애플리케이션에 적합하기 때문이다. 제작할 애플리케이션이 간단하고 용량이 중요하다면 Flex 사용을 하지 않는 편이 좋을 수 있다. 배보다 배꼽이 더 큰 애플리케이션을 만들지 말자는 것이다. 그래서 손수 FontManager와 같은 클래스를 제작할 필요가 느꼈다. 앞으로 이러한 문제로 FontManager 말고도 다양한 클래스를 제작할 생각이다. 이러다가 프레임워크 만드는거 아닌가? ^^;

참고 : Font Embed시 Flash player 10, AIR 1.5 이상부터 embedAsCFF 속성을 사용해야합니다. 다음 글을 참고하세요. 2009.11.16
http://blog.jidolstar.com/614

 

 

 

국내의 한 회사에서 Flex/Sliverlight관련 RIA 플랫폼 기반의 프레임워크를 오픈소스화 했다. 이름하야 OpenZET. Open 소스도 모자라 무려 한국어를 포함한 5개 국어(영어,프랑스어,스페인어,일본어)로 번역되었다. 회사자체에서 이렇게 오픈 소스를 한다는 것은 폐쇄적이인 국내 IT 시장에서는 매우 획기적이고 즐거운 소식이 아닐 수 없다. 더군다나 다국어 지원까지…

 

아주 많은 자료가 있는 것은 아니지만 현업에서 컴포넌트를 어떻게 만들고 응용하는가 알 수 있고 함께 정보를 공유할 수 있다는 점에서 매우 가치있게 평가한다.

 

다른 많은 RIA관련 회사들도 이렇게 오픈 마인드를 가지고 국내 RIA 시장에 활기를 넣었으면 하는 바람을 가진다.

 

공식홈페이지 : http://openzet.org/

 

TextField에 TextFormat을 적용할 때 TextField의 defaultTextFormat 속성과 setTextFormat() 메소드를 사용한다. 예전에 차이점을 구분했었는데 맨날 Flex만 하다보니 까먹었었다. 이것때문에 1시간 정도 삽질했다. ㅡㅡ;

 

아래내용을 보고 defaultTextFormat ,setTextFormat()의 차이점을 명확히 이해하자.

 

http://butterguy.tistory.com/entry/텍스트-필드에-서식설정하기

Flex SDK 3.3 이 공식배포되었군요.

여전히 한글 문제는 해결된바 없습니다.

ActionScript 3.0에는 MatrixTransform 클래스가 있다.

 

Matrix는 일종의 변환행렬(Transformation Matrix)이다. 3×3 행렬로 이동(translation), 확대/축소(scaling), 회전(rotation), 기울이기(shearing)등을 구현할 수 있다. Papervision3D나 Away3D와 같이 2D화면에 3D 효과가 가능했던 것은 Bitamp과 Matrix가 있기 때문이다. 이들 클래스를 어떻게 잘 조작하느냐에 따라 풍성한 화면효과를 구현할 수 있다.

 

Transform은 DisplayObject 계열의 객체에 만들어진 Matrix를 적용하는 역할을 한다. 즉 Matrix는 이동/회전등의 변환수단으로 작용하고 실제 이 Matrix를 이용해서 DisplayObject에 적용하는 것은 Transform인 것이다.

 

DisplayObject에는 tranform 속성이 있고 transform에는 matrix 속성이 있다. 그러므로 만들어진 Matrix를 DisplayObject에 적용하기 위해 다음과 같이 시도할 수 있다.

 

var content:DisplayObject = new Sprite as DisplayObject;
var mat:Matrix = new Matrix();
(mat 조작 생략)
content.tranform.matrix = mat;

 

 

위와 같이 하는 것만으로도 이동,확대/축소,회전,shearing,거울효과등을 적용할 수 있게 된다.

그럼 어떻게 Matrix를 만들어야 원하는 동작을 만들어낼 수 있을까? 다행히도 Matrix는 사용자가 조작하기 아주 쉽게 만들어져 있다.

 

회전을 하려면 Matrix의 rotate( 라디안각도 ), 이동을 하려면 Matrix의 translate( dx, dy ), 확대/축소하려면 Matrix의 scale( sx, sy ) 함수를 사용하면 된다.

 

보통 이 Matrix를 이용하지 않고도 DisplayObject의 rotation, x, y, scaleX, scaleY 속성을 사용해도 될때가 있다. 사릴 이 속성들은 결국 Matrix로 구현된다. Matrix가 다루기 어렵기 때문에 쉬운 사용을 위해 기본적인 인터페이스는 DisplayObject에 만들어준 것 뿐이다. 하지만 이 외에 사진의 중심으로 회전하던가 거울효과를 적용시키던가 shearing과 같은 효과를 주려면 이들 속성만 가지고는 해결할 수 있는 방법이 없거나 있다고 하더라도 비효율적인 방법일 소지가 많다. 그러므로 고급적으로 DisplayObject를 가공하려면 Matrix의 사용방법과 그 원리에 대해서 익숙해져야 한다.

 

Matrix는 3×3로 구성된다고 했다. 2D인데 3×3 행렬을 이용하는 이유는 이동(translation)이 포함되어 있기 때문이다.

 

일단 전반적인 지식은 아래 링크들을 참고하기 바란다. 아래 내용만 잘 알아도 DisaplyObject 객체를 가지고 기하학적 변형을 위한 기초는 알 수 있다.

 

DisplayObject 객체의 중심을 그의 부모 (0,0)점에 위치하고 회전 및 확대/축소

DisplayObject객체 중심을 객체의 좌측상단점으로 이동하는 행렬 T, 회전행렬 R, 확대/축소행렬 S라고 하자.

 

DisplayObject는 항상 좌측상단이 기준점이 된다. 그러므로 DisplayObject의 중심점 이동->회전->확대/축소를 적용하면 되겠다. 그러므로 이들을 모두 적용할 수 있는 행렬은 다음과 같다.

 

 

M = S x R x T = R x S x T

 

더 명확히 표현하자면 아래와 같다. (아래식에서 각각의 변환행렬을 곱한 결과는 그 아래 실제 결과와 다르게 나왔다. 실제결과를 도출하기 위한 뭔가 다른 설정이 있는 것 같은데 본인은 발견하지 못했다. 혹시 아는분 댓글 부탁한다.)

 

 

 

여기서 θ는 회전각도 radian값이고 sx와 sy는 각각 x축, y축 확대/축소 비율이다. cx와 cy는 DisplayObject 객체의 중심좌표값이다.

 

A=[x,y,1]값을 변환을 거쳐서 나온 결과 좌표값을 A’=[x’,y’,1]이라고 한다면 다음 관계가 성립한다.

 

A’= M x A

 

 

결국 중요한 것은 변환행렬 M을 만들어 내는 일이다.

 

그럼 위 행렬을 어떻게 DisplayObject에 적용할 수 있을까?

 

var content:DisplayObject = new Sprite as DisplayObject;
var cx:Number = content.width/2;
var cy:Number = content.height/2;
var sx:Number = 2;
var sy:Number = 3;
var theta:Number = 45 * Math.PI/180;
var mat:Matrix = new Matrix();
mat.translate( -cx, -cy );
mat.scale( sx, sy );
mat.rotate( theta );
content.tranform.matrix = mat;

위 코드처럼 하면 변환행렬 M을 DisplayObject에 적용한 것과 같다. 다음과 같이 해도 동일한 동작을 하게 된다.

 

var content:DisplayObject = new Sprite as DisplayObject;
var cx:Number = content.width/2;
var cy:Number = content.height/2;
var sx:Number = 2;
var sy:Number = 3;
var theta:Number = 45 * Math.PI/180;
var cos:Number = Math.cos( theta );
var sin:Number = Math.sin(theta);
var mat:Matrix = new Matrix();
mat.a = sx * cos;
mat.b = sy * sin;
mat.c = –sx * sin;
mat.d = sy * cos;
mat.tx = - sx * cx * cos + sy * cy * sin;
mat.ty = - sx * cx * sin - sy * cy * cos;
content.tranform.matrix = mat;

이런 원리를 잘 알아두면 앞으로 DisplayObject의 기하학적 변형을 위한 방법을 익히는 것 뿐아니라 속도향상에도 도움이 될 수 있겠다.

 

예제 애플리케이션 제작

위 설명을 토대로 사진 중심으로 회전, 확대/축소등이 가능한 DisplayObject 객체를 만들고 테스트 해볼 수 있는 예제를 만들어보자.


아래는 만들어진 테스트용 애플리케이션이다. (사진 주인공은 제 딸 예진이  입니다. ^^)

 (서버를 바꾼뒤 이미지 로드를 실패하고 있습니다. 아래 소스는 정상적으로 동작하는 것입니다.)

 

 

아래 클래스는 이동/스케일링/회전/거울효과를 테스트 하기 위한 것이다. 위에서 다 설명했으므로 특별히 분석은 안하도록 하겠다.

 

package
{
	import flash.display.DisplayObject;
	import flash.display.Loader;
	import flash.display.Sprite;
	import flash.display.StageScaleMode;
	import flash.events.Event;
	import flash.geom.Matrix;
	import flash.net.URLRequest;
	import flash.utils.setTimeout;

	public class TestSprite extends Sprite
	{
		private var container:Sprite;
		private var image:Loader;
		private var originalWidth:Number = 0;
		private var originalHeight:Number = 0;
		private var _scale:Number = 1;
		private var _rotation:Number = 0;
		private var _horizontalMirror:Boolean = false;
		private var _verticalMirror:Boolean = false;
		private var isApplyMatrix:Boolean = false;

		public function TestSprite()
		{
			super();

			container = new Sprite;
			addChild( container );

			image = new Loader;
			container.addChild( image );
			image.contentLoaderInfo.addEventListener( Event.COMPLETE, onComplete );
			image.load( new URLRequest( "http://jidolstar.com/blog/wp-content/uploads/2009/03/yaejin.jpg" ) );
		}

		private function onComplete( event:Event ):void
		{
			event.target.removeEventListener( Event.COMPLETE, onComplete );

			//원본 사진의 크기
			originalWidth = event.target.content.width;
			originalHeight = event.target.content.height;

			//Matrix적용
			applyMatrix( container );
		}

		private function applyMatrix( target:DisplayObject ):void
		{
			var mat:Matrix = target.transform.matrix;
			var cos:Number = Math.cos( _rotation * Math.PI/180 );
			var sin:Number = Math.sin( _rotation * Math.PI/180 );
			var cx:Number = originalWidth/2;
			var cy:Number = originalHeight/2;

			//단위행렬로 바꿈
			mat.identity();

			//거울 효과 적용
			if( _horizontalMirror )
			{
				mat.a = -1;
				mat.tx = originalWidth;
			}
			if( _verticalMirror )
			{
				mat.d = -1;
				mat.ty = originalHeight;
			}

			//widget의 (0,0)위치 조정
			mat.translate( -cx, -cy );

			//스케일 적용
			mat.scale( _scale, _scale );	

			//회전 적용
			mat.rotate( _rotation * Math.PI/180 );

			//mat.translate( cy, cy );

			/*
			//주석부분은 위에 mat.translate(),mat.scale(), mat.rotate()을 호출한것과 동일하게 동작한다. 단 거울효과를 적용했을때는 똑같지 않다. 같은 효과를 내려면 Matrix.concat()을 이용해 행렬곱을 실시하면 되겠다.
 			mat.a = _scale * cos;
			mat.b = _scale * sin ;
			mat.c = _scale * sin * -1;
			mat.d = _scale * cos;
			mat.tx = -cx * _scale * cos + cy * _scale * sin;
			mat.ty = -cx * _scale * sin - cy * _scale * cos;
			*/

			//Matrix 적용
			target.transform.matrix = mat;	

			this.width = target.width;
			this.height = target.height;
			this.dispatchEvent( new Event( Event.RESIZE ) );	

			isApplyMatrix = false;
		}

		public override function set rotation(value:Number):void
		{
			_rotation = value;
			if( !isApplyMatrix )
			{
				isApplyMatrix = true;
				setTimeout( applyMatrix, 0, container );
			}
		} 

		public function set scale(value:Number):void
		{
			_scale = value;
			if( !isApplyMatrix )
			{
				isApplyMatrix = true;
				setTimeout( applyMatrix, 0, container );
			}
		}	

		public function set horizontalMirror( value:Boolean ):void
		{
			_horizontalMirror = value;
			if( !isApplyMatrix )
			{
				isApplyMatrix = true;
				setTimeout( applyMatrix, 0, container );
			}
		}

		public function set verticalMirror( value:Boolean ):void
		{
			_verticalMirror = value;
			if( !isApplyMatrix )
			{
				isApplyMatrix = true;
				setTimeout( applyMatrix, 0, container );
			}
		}
	}
}

아래는 위에서 정의한 TestSprite를 사용하는 Flex Application이다.

 

<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="vertical" creationComplete="init()" backgroundGradientColors="[0,0]">
	<mx:VBox width="100%" height="100%">
		<mx:UIComponent id="container" width="100%" height="100%" resize="onResize()"/>
		<mx:Form>
			<mx:FormItem label="scale">
				<mx:HSlider id="sldScale" minimum="0.5" maximum="3" value="1" change="test.scale=sldScale.value" liveDragging="true"/>
			</mx:FormItem>
			<mx:FormItem label="rotation">
				<mx:HSlider id="sldRotation" minimum="0" maximum="360" value="0" change="test.rotation=sldRotation.value" liveDragging="true"/>
			</mx:FormItem>
			<mx:FormItem label="horizontal Mirror">
				<mx:CheckBox id="chHorizontalMirror" change="test.horizontalMirror = chHorizontalMirror.selected"/>
			</mx:FormItem>
			<mx:FormItem label="vertical Mirror">
				<mx:CheckBox id="chVerticalMirror" change="test.verticalMirror = chVerticalMirror.selected"/>
			</mx:FormItem>
		</mx:Form>
	</mx:VBox>
	<mx:Script>
		<![CDATA[
			private var test:TestSprite;
			private function init():void
			{
				test = new TestSprite();
				container.addChild( test );
				test.addEventListener( Event.RESIZE, onResizeTest );
				onResize();
			}

			private function onResize():void
			{
				if( test )
				{
					test.x = container.width/2;
					test.y = container.height/2;
				}
			}

			private function onResizeTest( event:Event ):void
			{
				var w:Number = test.width;
				var h:Number = test.height;
				var hw:Number = w/2;
				var hh:Number = h/2;
				test.graphics.clear();
				test.graphics.lineStyle( 1, 0xff0000, 1 );
				test.graphics.drawRect( -hw, -hh, w, h );
				test.graphics.moveTo( -hw, 0 );
				test.graphics.lineTo( hw, 0 );
				test.graphics.moveTo( 0, -hh );
				test.graphics.lineTo( 0, hh );
			}
		]]>
	</mx:Script>
	<mx:Style>
		global
		{
			color:#ffffff;
		}

		ToolTip
		{
			color:#000000;
		}
	</mx:Style>
</mx:Application>

 

위에서 보여준 것과 달리 좌측상단을 기준으로 하고 사진중심만 회전하고 싶은 경우에는 TestSprite 클래스에서 mat.rotate() 부분 아래에 mat.translate( cx, cy )만 추가하면 된다. 결국 사진중심을 회전하기 위해 이동후, 회전 및 스케일링을 거친다음에 다시 자신의 위치로 옮겨오는 작업이 추가되는 것이다.

 

원리를 알면 많이 고민 안하고도 적용할 수 있다는거…
산수 좀 나온다고 거부하면 다음에도 고생한다. ^^

 

 

 

2008년 10월부터 공개했던 파일업로더를 기능 추가해 다시 공개합니다. 따로 ASdocs를 마련하지 않았기 때문에 불편하긴 하지만 차차 넣을 생각이고 응용프로그램도 몇개 더 만들 예정입니다.

 

이번에 추가된 기능은 “업로드 금지 파일”을 등록하는 기능입니다. 가령, 서버측에 등록되면 민감할 php, js, asp, jsp, pl등의 확장자를 가진 파일을 업로드하려고 할 때 클라이언트 측에서 업로드하는 것을 원천적으로 방지하는 기능입니다.

 

사용하는 방법은 예제 프로그램에서 uploader/html-template/index.template.html 안에 flashvars의 banfileExtensions를 참고하시면 되겠습니다. 금지할 파일 확장자를 “php;js;asp;” 형태로 만드시면 됩니다. 사용자가 여기에 등록된 파일을 올리려고 하는 경우 fileuploader_banFileExtension 이벤트가 발생하게 되며 이벤트가 발생할 때 처리를 하기 위해 FAService의 addEventListener()를 이용해 이벤트 핸들러 함수를 등록합니다. 이벤트 핸들러 함수에 넘어오는 인자값은 object형태로 아래와 같은 값이 넘어옵니다.

 

{”filename”:업로드금지 처리된 파일명, “banFileExtension”: 업로드 금지처리된 파일의 확장자, “banFileExtensions”: 등록된 업로드 금지 확장자들. ;로 구분됨 }

 

질문 및 버그에 대한 댓글은 언제든지 환영합니다.

 

멀티 파일 업로더 실행 동영상

 

왜 만들었나?

Flash Player 10이 나오면서 기존 Javascript-Flash 기반 다중 파일 업로더 기능이 제대로 동작하지 않아 이것을 쓰는 많은 사이트들이 발등에 불떨어지듯한 상황이 발생하게 되었다. 내가 참여하고 있는 스타플(http://starpl.com)도 예외는 아니다.

 

원인을 살펴보자면 Flash Player의 파일 업로드에 대한 보안(?)정책이 바뀐 것에 기인한다. Ajax(Javascript)만 가지고 실시간 다중 파일 업로드가 안되기 때문에 Ajax에서 직접 ExternalInterface로 Flash의 FileReference.browse() 메소드를 호출하여 파일을 업로드하고 그 결과를 다시 Ajax로 ExternalInterface를 이용해 반환하는 거였는데 여기서 문제될 만한 부분은 Ajax쪽에서 사용자 조작이 있어 browse() 메소드를 호출하면 바로 “Error #2176 팝업 창 표시와 같은 특정 동작은 마우스를 클릭하거나 버튼을 누르는 것과 같이 사용자가 조작하는 경우에만 발생합니다.”라는 에러가 발생한다. 즉, Flash Player 10부터는 Flash 내부가 아닌 외부 Ajax와 같은 사용자 조작으로는 browse() 메소드를 호출할 수 없다!

 

Flash Player 10 부터는 Flash에 버튼을 만들고 FileReference.browse()를 호출해야한다.

 

왜 공개했나?

소스 공개의 묘미는 함께 알아가는 가는 거다. 본인의 실력이 특출나기 때문이 아니라는 점을 강조하고 싶다. 오히려 부족함을 느끼기 때문에 블로깅을 하는 것과 동일하다. 블로깅을 하면 나도 모르게 몰랐던 것도 알아간다. 왜냐하면 블로깅을 통해 관심 분야의 사람들과 만날 수 있기 때문이다. 소스 공개도 마찬가지이다. 새로운 이슈에 대해 나만 알고 있다면 얼마나 이기적인가? 어짜피 알거면 같이 알아가면서 함께 발전하자는게 나의 생각이다.

 

그런 의미에서 여러분도 with 블로깅&소스공개?!

 

멀티 파일 업로더 (Multi - file uploader) 에 대해

사용하는데는 어려움이 많이 없을거라 생각합니다.


이미 swfuploader라는 좋은 툴이 있긴 하지만 학습과 쉬운 소스 수정을 위해 직접 만들었습니다.
공유해서 함께 지식을 넓혀가길 희망합니다.


이름은 Multi-file uploader이지만 실제 동작은 1개씩 업로드 되는 겁니다.

 

* author : 지용호 (Yongho, Ji)

* Q&A : jidolstar[at]gmail.com, http://blog.jidolstar.com)

* license : LGPL (수정시에는 소스를 공개합니다. 하지만 그대로 사용하는 것은 사용 출처만 밝혀주세요.)

* 최초제작일 : 2008.10.24

* 최종수정일 : 2009.03.02

* 제작언어 : Adobe ActionScript 3.0

* 제작환경 : Adobe Flex Builder 3 Professional. Flex SDK 3.2

* 구동환경 테스트 : IE6, FF, google chrome

* 제작배경
  Flash Player 10이 정식 릴리즈 됨에 따라 Javascript를 통해 FileReference.browse() 메소드를 호출을 방지하도록 되었기 때문에 이 방식을 사용한 것을 대체하려고…

 

* 첨부파일 설명

  1. FAService는 Javascript-SWF간 통신하기 위한 라이브러리이다.
  2. fileupload는 멀티파일업로더 핵심 라이브러리이다.
  3. fileuploader는 fileupload와 FAService를 이용해서 HTML환경에서 멀티파일업로드를 가능하게 만들어진 애플리케이션이다.
  4. fileuploader/html-template/index.template.html 부분처럼 사용하면 되겠다.
  5. fileuploader/php 에는 예제로 만들어진 php소스가 있다. jsp, asp로 비슷하게 만들어 쓰면 되겠다.

* 추가사항

  •  2008.11.14
         1. POST, GET 방식으로 Variables를 넘길 수 있도록 함
         2. requestHeaders를 추가할 수 있도록 함 (헤더를 보내는 경우 서버측 crossdomain.xml에 allow-http-request-headers-from 설정이 되어야 한다.)
         3. contentType(MINE Type)을 지정할 수 있도록 함
         이러한 방식은 AS3의 URLRequest에 있는 속성이므로 참고하길 바란다.
         사용예는  index.template.html의 flashvars 참고하면 된다.
  •  2009.03.02
         banfileExtensions 추가. flashvars에 추가된 속성으로 업로드를 거절할 확장자를 가진 파일을 등록한다. 대소문자는 자동으로 맞춰준다. 사용자가 업로드 금지 파일을 등록하려는 경우 “fileuploader_banFileExtension” 이벤트가 발생한다. 자세한 사용법은 index.template.html을 참고한다.  ex) banfileExtensions = “mp3;php;pl;”;

* 사용방법

  1. 자바스크립트를 통해 이벤트 핸들러를 등록한다.(FAService Flex-Ajax 통신 브릿지 이용, Flex-JS로 제작됨 , 본인 제작)
  2. 파일 업로드를 위한 설정 Flash Vars로 등록 한다.(가령 업로드할 서버 경로, 파일 사이즈, 필터, 버튼이미지 경로등…)
  3. 업로더 SWF를 HTML상에 붙인다. 예제에서는 swfobject.js를 이용했다.
  4. 서버쪽 프로그램을 만든다. 첨부된 php파일을 참고하면 되겠다. asp, jsp든 어떤 언어를 써도 동일하게 만들면 되겠다.
  5. 예제에선 정상적으로 동작하는 경우 textarea에 ready가 뜬다. 이벤트 핸들러가 호출되면 여기에 출력하도록 짜여졌다.
  6. 파일 선택후 ok하면 이벤트는 fileuploader_startAll, (fileuploader_start, fileuploader_step, fileuploader_end), fileuploader_endAll 순으로 진행된다. 중간에 ()안에 들어간것은 여러개의 파일의 경우에 진행상황에 따라서 번갈아가며 호출된다.
  7. 서버 접속이 원할치 않는다면  fileuploader_fail 이벤트가 발생한다.(보안 또는 IO Error)
  8. 파일 선택을 취소하면 fileuploader_cancel가 발생한다.
  9. 1개의 파일 사이즈가 정해진 크기보다 크면 fileuploader_fileSizeError 이벤트가 발생한다.
  10. . 선택한 파일의 갯수가 정해진 갯수보다 크면 fileuploader_fileCountError 이벤트가 발생한다.
  11. . 중간에 uploaderFAService.call( “stop”, null )을 호출하게 되면 업로드가 최소되고 fileuploader_stopAll 이벤트가 발생한다.
  12. 금지된 파일을 선택한 경우 fileuploader_banFileExtension 이벤트가 발생한다.

* 이벤트

이벤트 발생시 파라미터들은 JSON Object 형태이다.

  • ready  없음
  • fileuploader_startAll : {”totalCount”:total count of files, “totalSize”:total size of files(bytes) }
  • fileuploader_start : {”filename”:file name, “bytesTotal”:size of file(bytes)}
  • fileuploader_step : {”filename”:file name, “bytesTotal”:size of file(bytes), “bytesLoaded”:uploaded size of file(bytes)}
  • fileuploader_end : {”filename”:file name, “bytesTotal”:size of file(bytes), “uploadCompleteData”:…}  여기서 uploadCompleteData는 서버 개발자가 마음대로 값을 바꿀 수 있다. JSON 형태의 String값으로 넘겨주면 프로그램에서는 자동적으로 Object형태로 반환해준다.
  • fileuploader_fail : {”filename”:file name, “bytesTotal”:size of file(bytes, “msg”:error message}
  • fileuploader_endAll : {”failCount”: count of files upload failed, “endCount”: count of files upload successed , “totalCount”: count of files tried to upload }
  • fileuploader_stopAll : {”totalCount”:total count of files, “totalSize”:total size of files(bytes) }
  • fileuploader_fileSizeError : {”filename”:file name, “bytesTotal”:size of file(bytes), “maxFileSize”: maximum file size(bytes)}
  • fileuploader_fileCountError :{”totalCount”:total count of files, “maxFileCount”: maximum count of files }
  • fileuploader_banFileExtension : {”filename”:filename, “banFileExtension”: extension of selected file, “banFileExtensions”: registered ban extensions}

 

* 함수

  • uploaderFAService.call(’browse’,null );  Flash Player 9이하일때는 다음과 같은 방법으로 파일 browsing을 요청할 수 있다. 하지만 이 방법은 사용을 권장하지 않는다.

 

* FAService에 대해

  1. FAService는 Flex-Ajax 통신 라이브러리이다.(ActionScript 3 프로젝트로도 사용이 가능)
  2. 여기서는 소스도 함께 공개했다.
  3. FABridge와 비교할때 최소기능만 사용하도록 만들었다.
  4. 단순히 addEventListener, removeEventListener, call 만으로 Flex와 Ajax간에 통신합니다.
  5. addEventListener은 Flex에서 발생하는 이벤트명, JS이벤트 함수, 우선순위 값이 들어갑니다. 우선순위 값은 같은 이벤트 발생시 호출한 JS이벤트핸들러 함수의 호출 순서를 정합니다. 숫자가 클수록 등록순에 관계없이 먼저 호출된다.
  6. JS 이벤트 핸들러 함수의 파라미터 값들은 Flex에서 정해서 보내줍니다. Object형이 일반적이지만 Array, Boolean, int, float,String 형등 다양한 형태가 될 수 있다.
  7. removeEventListener은 기존에 등록한 이벤트 핸들러를 삭제해준다.
  8. call 함수는 Flex쪽에 “호출명”이 등록되어 있어야만 호출된다. 인수값은 Object, Array, Boolean, int, float,String 형등이 모두 가능하며 이 값은 Flex쪽에서 정한다.
  9. 만약 Flex에서 정한 형태로 만들어지지 않으면 JS Alert 창을 띄우게 된다.
  10. call 함수는 반드시 ready 이벤트가 발생한 시점 이후로 사용해야한다. 이전에는 적용되지 않습니다.(스텍을 이용해서 명령을 저장해두었다가 하는 방법도 모색하고 있음, addEventHandler는 그렇게 하고 있음)
  11. 등록되어진 이벤트 핸들러, 호출가능한 call 함수 목록등을 반환할 수 있는 함수를 만들 필요가 있다고 생각한다.

 

소스 다운로드

 

읽어볼만한 글

스타플(http://starpl.com)에서 제공하는 위젯중에 아래와 같은 “내별정보위젯”이 있다.

 


 

마우스를 올리면 내 별에 방문하거나 별받기를 할 수 있다.

 

스타플의 방문페이지와 별받기페이지에 접속시키기 위해 ExternalInterface.call()과 navigateToURL()을 아래와 같이 사용했다.

 

 

새창띄워 방문하기

ExternalInterface.call("window.open", 경로, "_blank", ""); 

 

방문하기

var request:URLRequest = new URLRequest( 경로 );
flash.net.navigateToURL( request, "_parent");

 

하지만 이러한 것들도 flash 컨텐츠를 웹페이지에 추가할때 쓰는 <object>태그와 <embed> 태그의 allowScriptAccess 속성과 allowNetworking 속성을 어떻게 지정하느냐에 따라 사용가능여부가 달라진다.

 

가령 이들 속성은 아래와 같은 방법으로 쓰여질 수 있다.

 

<object width="425" height="344">
<param name="allowScriptAccess" value="never" />
<param name="allowNetworking" value="internal" />
<param name="movie" value="swf경로"></param>
<param name="allowFullScreen" value="true"></param>
<embed src="swf경로"
    invokeURLs="false"
    autostart="false"
    allowScriptAccess="never"
    allowNetworking="internal"
    type="application/x-shockwave-flash"
    allowfullscreen="true"
    width="425"
    height="344"></embed>
</object>

 

각 속성에 대해서 자세히 설명하자면 다음과 같다.

 

 

allowScriptAccess 속성

 

이 속성은 SWF의 ExternalInterface을 통해 Javascript와 통신가능여부를 결정지어주는 속성이다. 이 속성은 다음 3가지 값중 하나가 된다.

  • never : SWF에서 ExternalInterface를 이용해 Javascript통신을 할 수 없다.
  • always : SWF에서 ExternalInterface를 이용해 Javascript통신이 언제든지 가능하다.
  • sameDomain : 같은 도메인에 있는 SWF일때 Javascript 통신이 가능하다.(기본)

 

allowNetworking 속성 

 

이 속성은 SWF에서 네트워크 기능 API 사용을 제한하는 옵션이다. 다음과 같은 3가지 값중 하나를 가진다.

  • all : 모든 네트워크 API 허용(기본)
  • internal : 브라우저 네비게시션이나 브라우저 상호작용 API 사용 제한
  • none : 모든 네크워크 API 제한

internal인 경우 제한 API

  • navigateToURL()
  • fscommand()
  • ExternalInterface.call()

none인 경우 제한 API (internal 포함)

  • sendToURL()
  • FileReference.download()
  • FileReference.upload()
  • Loader.load()
  • LocalConnection.connect()
  • LocalConnection.send()
  • NetConnection.connect()
  • NetStream.play()
  • Security.loadPolicyFile()
  • SharedObject.getLocal()
  • SharedObject.getRemote()
  • Socket.connect()
  • Sound.load()
  • URLLoader.load()
  • URLStream.load()
  • XMLSocket.connect()

 

이제 네이버에서 스타플 위젯을 통해 스타플에 방문할 수 없는 이유를 보자.

 

<EMBED
    invokeURLs="false"
    autostart="false"
    allowScriptAccess="never"
    allowNetworking="internal"
    src="경로"
    width="54"
    height="96"
    type="application/x-shockwave-flash"
    allowfullscreen="true">
</EMBED>

 

몇몇 분들이 이런 제한정책때문에 네이버 블로그에 있기를 꺼려하시는 분들이 계시다. 물론 보안상 어떻게 할 수 없다고는 할 수 있지만…


참고글

이 문서는 ActionScript 3.0에서 Custom Event를 만들어 사용할 시 Event 클래스의 clone() 메소드의 용법을 이해하는 것을 목적으로 한다. 자주 Event를 확장해서 만드는 사람이라면 꼭 읽어볼 내용이라고 생각한다.

 

1. Event 클래스의 clone() 메소드

 

flash.events.Event 클래스에는 clone() 함수가 정의되어 있다. 이 clone() 함수 내부에는 아마도 아래처럼 정의되어 있을 것이다. (Event클래스는 Native코드이므로 내부를 볼 수 없다. 다만 유추할 뿐이다.)

 

package flash.events
{
	public class Event
	{
		public var type:String;
		public var bubbles:Boolean;
		public var cancelable:Boolean;
		public var target:Object;
		public var currentTarget:Object;

		public function Event( type:String, bubbles:Boolean = false, cancelable:Boolean = false )
		{
			this.type = type;
			this.bubbles = bubbles;
			this.cancelable = cancelable;
		}
		public function clone():Event
		{
			return new Event( type, bubbles, cancelable );
		}
		...(다른 함수 생략)
	}
}

 

 

Event의 clone() 함수는 자신과 똑같은 객체를 새로 만들고 반환하는 역할을 한다.

아래 clone() 함수에 대해서 설명하고 있다.

clone() method

public function clone():Event

Language Version:

ActionScript 3.0

Runtime Versions:

AIR 1.0, Flash Player 9

Duplicates an instance of an Event subclass.

Returns a new Event object that is a copy of the original instance of the Event object. You do not normally call clone(); the EventDispatcher class calls it automatically when you redispatch an event—that is, when you call dispatchEvent(event) from a handler that is handling event.

The new Event object includes all the properties of the original.

When creating your own custom Event class, you must override the inherited Event.clone() method in order for it to duplicate the properties of your custom class. If you do not set all the properties that you add in your event subclass, those properties will not have the correct values when listeners handle the redispatched event.

In this example, PingEvent is a subclass of Event and therefore implements its own version of clone().

 

설명을 간단히 요약해 보면…

  1. Event의 clone() 함수는 Event 서브클래스의 객체를 복제한다.
  2. clone() 객체는 EventDispatcher 클래스의 dispatchEvent(event) 함수에 의해 재송출(redispatch)될 때 자동으로 호출된다.
  3. Event를 확장해 Custom Event를 만드는 경우 반드시 override 해야한다. override하지 않으면 clone()은 그 부모 Event의 clone()을 사용하므로 clone()을 호출 하더라도 Custom Event에 새로 추가된 속성(properties)을 설정할 수 없게된다.

 

여기에서 EventDispatcher의 dispatchEvent()와 Event의 clone()과는 어떤 관계가 있어보인다. 그리고 clone()을 override해야하는 이유와도 엮여 있다는 것을 생각해볼 수 있다.

 

사실 이런 설명만 가지고는 제대로 이해하기 어렵기 때문에 여기서는 EventDispatcher에서 dispatchEvent()가 동작하는 형태와 Event의 clone() 함수의 관계를 하나의 예제로 이해해 보도록 한다.

 

2. EventDispatcher 클래스의 dispatchEvent() 함수의 이해

Event 클래스의 clone()함수에 대한 용도에 대해 알기전에 먼저 아래 내용을 알아야한다.

 

Event 객체를 송출(Dispatch)하기 위해서 ActionScript 3.0에서는 IEventDispather 인터페이스와 EventDispatcher 클래스를 지원한다. IEventDispather를 구현하여 내부적으로 EventDispatcher 객체를 만들어 사용하는 클래스를 정의하거나 EventDispatcher 클래스 자체를 상속하여 만든 클래스라면 Event객체를 송출할 수 있다. 이벤트를 송출할 때 사용하는 것이 EventDispatcher에 정의된 dispatchEvent( event:Event ) 함수이다. dispatchEvent는 인자로 Event의 객체를 인자로 받아 이벤트를 송출한다.

 

그럼 dispatchEvent()는 내부적으로 어떻게 구현되는 것일까? 다음 내용을 보자.

 

dispatchEvent () method
public function dispatchEvent(event:Event):Boolean Language Version : ActionScript 3.0
Runtime Versions : AIR 1.0, Flash Player 9
Dispatches an event into the event flow. The event target is the EventDispatcher object upon which the dispatchEvent() method is called.
Parameters

event:Event — The Event object that is dispatched into the event flow. If the event is being redispatched, a clone of the event is created automatically. After an event is dispatched, its target property cannot be changed, so you must create a new copy of the event for redispatching to work.Returns

Boolean — A value of true if the event was successfully dispatched. A value of false indicates failure or that preventDefault() was called on the event.

Throws

Error — The event dispatch recursion limit has been reached.

 

위 dispatchEvent() 함수에 대한 설명이 담겨 있다. 인자값으로 event:Event가 있는데 이에 대한 설명(빨간부분)을 살펴보면 다음 내용으로 설명된다.

 

  1. Event 객체의 clone() 함수를 사용하는 조건은 해당 Event 객체를 다시 송출할 때이다.
  2. 다시 송출하게 되면 clone() 함수를 이용해 같은 형태의 Event 객체를 새로 생성하고 그 Event 객체의 target 속성을 이벤트를 송출하는 객체로 바꾼다.

 

이 설명으로 우리는 Event가 송출되는 과정에 사용되는 dispatchEvent() 메소드의 동작을 유추할 수 있다.

 

  1. 객체 A, 객체 B 가 있고 A는 이벤트를 송출하는 쪽, B는 이벤트를 받는 쪽이다라고 가정하자.- 이 객체들은 당연히 EventDispatcher를 확장했다.- A,B가 같은 객체일 수도 있고 다를 수도 있다. 다른 경우는 Visual 객체의 경우 이벤트 전파에 의해 가능해진다.

  2. 객체 A에서 이벤트 송출
    처음 A에서 생성된 Event에는 target속성이 정의되지 않는다. A의 dispatch() 함수로 이 Event 객체를 인자로 넘겨주면 clone()함수가 호출되지 않고 Event의 target은 A로 설정된다.
  3. 객체 B에서 이벤트 받음A에서 송출한 Event를 B에서 받는다. 이것은 addEventListener()로 가능하다는 것은 이미 알고 있다. addEventLisener()에 등록된 Event 처리 함수는 인자로 A에서 발생한 Event 객체를 받는다.받은 Event 객체를 다시 B에서 dispatchEvent()로 넘겨주게 되면 Event객체의 target속성이 B가 아닌 A이기 때문에 clone() 함수를 호출해서 새로운 Event 객체를 만들고 target을 B로 설정한다.

 

 

아하! 이쯤되면 EventDispatcher 클래스의 dispatchEvent() 함수가 어찌 생겼는지 알 수 있겠다. Event와 같이 EventDispatcher도 Native코드이므로 볼 수 없지만 어떻게 동작할지 유추만 하는 것이다!

 

 

package flash.events
{
	public var type:String;
	public var bubbles:Boolean;
	public var cancelable:Boolean;
	public class EventDispatcher implements IEventDispatcher
	{
		private target:IEventDispatcher;

		public function EventDispatcher(target:IEventDispatcher = null)
		{
			if( target )
				this.target = target;
			else
				this.target = this;
		}

		public function dispatchEvent(event:Event):Boolean
		{
			if( event == null ) return false;
			if( event.target == null )
			{
				event.target = target;
				event.currentTarget = target;
			}
			else
			{
				var newEvent:Event = event.clone();
				newEvent.target = target;
				newEvent.currentTarget = target;
			}
			dispatchEventFunction( event );
		}

		private function dispatchEventFunction( event:Event ):void
		{
			...(이벤트를 송출하는 알고리즘 구현)
		}

		public function addEventListener(type:String, listener:Function, useCapture:Boolean = false, priority:int = 0, useWeakReference:Boolean = false):void
		{
			...(생략)
		}

		public function removeEventListener(type:String, listener:Function, useCapture:Boolean = false):void
		{
			...(생략)
		}

		public function willTrigger(type:String):Boolean
		{
			...(생략)
		}

		public function hasEventListener(type:String):Boolean
		{
			...(생략)
		}
	}
}

 

 

 

위 코드는 dispatchEvent() 메소드가 어떻게 동작할지 쉽게 알려주는 코드이다. 해석은 앞서 설명했으니 따로 하지 않겠다. (다시 언급하지만 이 클래스는 실제 EventDispatcher 클래스가 아니다. 어떻게 동작하는지 예를 보여주기 위한 코드일 뿐이다. )

 

 

이로써 Event 클래스에 정의된 clone()함수를 언제 쓰냐에 대한 답을 얻은 것이다.

 

 

3. Custom Event를 만들때 clone() 함수를 override 해야하는 이유

지금까지는 clone()함수를 어디서 사용하는가 안 것 뿐이다. 이 문서의 제목을 풀어서 말하면 “Event 클래스를 확장할 때 clone() 함수를 override 해야하는 이유”이다. 글의 목적은 Custom Event를 만들 때 clone() 함수를 override 해서 사용해야하는 이유를 아는 것이다.

 

clone() 함수를 override해야하는 상황을 만든 예를 들어보겠다. 예제는 Flex SDK 3.2 환경에서 작업했다.

 

아래는 Event를 확장한 CustomEvent이다.

 

package
{
	import flash.events.Event;
	public class CustomEvent extends Event
	{
		public static const TEST:String = "test";

		public function CustomEvent(type:String, bubbles:Boolean=false, cancelable:Boolean=false)
		{
			super(type, bubbles, cancelable);
		}

		override public function clone():Event
		{
			return new CustomEvent( type, bubbles, cancelable );
		}
	}
}

 

아래는 Button을 확장해서 만들었다. 이 버튼을 누르면 CustomEvent 객체를 생성해서 test라는 이벤트 type으로 송출해준다.

 

package
{
	import flash.events.Event;
	import flash.events.MouseEvent;

	import mx.controls.Button;

	[Event(name="test", type="CustumEvent")]

	public class AComponent extends Button
	{
		public function AComponent()
		{
			super();
			this.label = "날 눌러줘";
			this.addEventListener( MouseEvent.CLICK, onClick );
		}

		private function onClick( event:MouseEvent ):void
		{
			var customEvent:CustomEvent = new CustomEvent( CustomEvent.TEST, false, false );
			trace( "AComponent 1:",customEvent.target,customEvent.currentTarget );
			this.dispatchEvent( customEvent );
			trace( "AComponent 2:",customEvent.target,customEvent.currentTarget );
		}
	}
}

 

 

아래는 Panel을 확장해 만들었고 자식으로 위에서 만든 AComponent 객체를 등록했다. (이 예제에서 자식으로 등록된 사실은 중요하지 않다.) AComponent에 발생된 CustomEvent 이벤트를 받는다. 받자마자 그 이벤트를 다시 송출하고 있다.

 

package
{
	import flash.events.Event;
	import mx.containers.Panel;

	[Event(name="test", type="CustomEvent")]

	public class BComponent extends Panel
	{
		private var aComp:AComponent;

		public function BComponent()
		{
			super();
		}

		override protected function createChildren():void
		{
			super.createChildren();

			if( !aComp )
			{
				aComp = new AComponent;
				aComp.addEventListener( "test", customEventHandler );
				aComp.width = 200;
				aComp.height = 200;
				addChild( aComp );
			}
		}

		private function customEventHandler( event:Event ):void
		{
			trace( "BComponent 1: ",event.target,event.currentTarget );
			this.dispatchEvent( event ); //re-dispatch
			trace( "BComponent 2: ",event.target,event.currentTarget );
		}
	}
}

 

 마지막으로 Application이다.

 

<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute" xmlns:local="*">
	<local:BComponent test="trace(’Application: ‘,event.target,event.currentTarget )"/>
</mx:Application>

 

 

 

실행한뒤 버튼을 누르면 콘솔창에 아래와 같은 메시지가 나온다.

 

AComponent 1: null null

BComponent 1 : EventTest0.BComponent4.AComponent12 EventTest0.BComponent4.AComponent12

Application: EventTest0.BComponent4 EventTest0.BComponent4

BComponent 2 : EventTest0.BComponent4.AComponent12

EventTest0.BComponent4.AComponent12

AComponent 2: EventTest0.BComponent4.AComponent12

EventTest0.BComponent4.AComponent12

 

아래는 위 결과에 대한 설명이다.

 

  1. [AComponent 1] AComponent에서 Event 객체를 만들어 송출하는데 dispatchEvent를 호출하기 전에 target과 currentTarget 속성은 모두 null이다. 예상한 대로다.
  2. [BComponent 1]이 이벤트가 송출된 다음 BComponent에서 받게 되면 이 이벤트의 target과 currentTarget은 AComponent의 객체를 참조하고 있다.
  3. [Application] BComponent에서 AComponent에서 생성한 Event 객체를 인자로 dispatchEvent()를 호출하면 Application에서는 Event의 target, currentTarget 속성이 BComponent의 객체를 참고하고 있다.
  4. [BComponent 2] BComponent에서 AComponent의 Event 객체를 dispatchEvent()를 재송출한 다음에 Event객체의 target과 currentTarget속성을 보여주고 있다. 단지 clone()메소드를 이용해 생성했기 때문에 Event객체 자신의 속성은 바뀌지 않는다. 단지 Application으로 전달되는 Event는 clone()으로 새로 생성되었고 BComponent의 객체를 참조하고 있다는 것을 알 수 있다.
  5. [AComponent 2] 마지막 줄은 AComponent에서 Event객체를 만들어 dispatchEvent()함수로 만든 Event객체를 송출한 후에 Event객체의 target과 currentTarget 속성을 보여준다. 4번째와 같은 결과지만 dispatchEvent()내부에서 clone() 메소드가 호출된 것이 아니라 단지 AComponent 객체의 참조값을 target과 currentTarget에 설정했을 뿐이라는 것을 알 수 있다.

 

위에서 한가지 기억할 사항은 이벤트가 재송출(redispatch)하는 경우 target과 currentTarget이 변경된다는 것이다.

 

만약 CustomEvent에서 Event의 clone()를 override한 clone() 함수 정의를 빼보고 실행해보자.

 

AComponent 1: null null

TypeError: Error #1034: 유형 강제 변환에 실패했습니다. flash.events::Event@5115971을(를) CustomEvent(으)로 변환할 수 없습니다.
at flash.events::EventDispatcher/dispatchEventFunction()
at flash.events::EventDispatcher/dispatchEvent()
at mx.core::UIComponent/dispatchEvent()[C:\autobuild\3.2.0\frameworks\projects\framework\src\mx\core\UIComponent.as:9298]
at BComponent/customEventHandler()[D:\EventTest\src\BComponent.as:34]

at flash.events::EventDispatcher/dispatchEventFunction()at flash.events::EventDispatcher/dispatchEvent()

at mx.core::UIComponent/dispatchEvent()[C:\autobuild\3.2.0\frameworks\projects\framework\src\mx\core\UIComponent.as:9298]

at AComponent/onClick()[D:\STARPL_DEV_02\Timeline\src03\EventTest\src\AComponent.as:23]

BComponent 1 : EventTest0.BComponent4.AComponent12 EventTest0.BComponent4.AComponent12

BComponent 2 : EventTest0.BComponent4.AComponent12 EventTest0.BComponent4.AComponent12

AComponent 2: EventTest0.BComponent4.AComponent12 EventTest0.BComponent4.AComponent12

 

위처럼 CustomEvent에 clone() 함수를 override하지 않으면 이벤트를 다시 송출할 때 유형 강제 변환 에러(TypeError)가 발생한다.

 

결과적으로 clone() 함수를 override를 해야하는 이유는 받은 Event 객체를 다시 재송출하는 경우에 clone() 함수를 호출하기 때문이다.

 

그럼 어찌 유형 강제 변환 에러(TypeError)가 나는 것일까? 사실 위에서 예를 든 EventDispatcher 클래스의 dispatchEvent() 메소드는 이런 에러가 날 수 없다. CustomEvent의 객체가 Event로 형변환이 일어나지 않을 이유가 없지 않은가? 본인 생각에 이 형변환 에러가 발생시키는 이유는 clone() 메소드에서 엉뚱한 이벤트의 객체를 생성하지 않고 자신 이벤트만을 clone으로 만들어 사용하게 하기 위해 강제성을 부여한 것이 아닌듯 싶다. 가령 다음과 같이 clone()을 만들면 안되게 한다는 것이다.

 

package
{
	import flash.events.Event;
	public class CustomEvent extends Event
	{
		public static const TEST:String = "test";

		public function CustomEvent(type:String, bubbles:Boolean=false, cancelable:Boolean=false)
		{
			super(type, bubbles, cancelable);
		}

		override public function clone():Event
		{
			return new OtherCustomEvent( type, bubbles, cancelable );
		}
	}
}

 

 

위처럼 자기는 CustomEvent인데 clone으로 OtherCustomEvent를 clone으로 삼아 만들면 안되지 않은가? EventDispather의 dispatchEvent는 개발자가 엉뚱하게 프로그래밍하는 것을 막기위해 컴파일 단계에서는 확인할 수 없다. 하지만 런타임시에 전혀 엉뚱하게 clone()함수를 override하는 것을 방지하기 위해 다른 Event Class로 clone함수를 이용하는 경우 TypeError가 나도록 배려(?)한 것이다.

 

이런 TypeError가 날 수 있게 EventDispatcher의 dispatchEvent() 함수를 아래와 같이 만들어봤다.

 

public function dispatchEvent(event:Event):Boolean
{
	if( event == null ) return false;
	if( event.target == null )
	{
		event.target = target;
		event.currentTarget = target;
	}
	else
	{
		var newEvent:Event = event.clone();
		if( getQualifiedClassName(event) != getQualifiedClassName(newEvent) )
		{
			var msg:String = "TypeError: Error #1034: 유형 강제 변환에 실패했습니다. ";
			msg += getQualifiedClassName(event) + "를(을)";
			msg += getQualifiedClassName(event) + "(으)로 변경할 수 없습니다."
			throw new TypeError( msg );
		}
		newEvent.target = target;
		newEvent.currentTarget = target;
	}
	dispatchEventFunction( event );
}

 

 

getQualifiedClassName()은 객체의 클래스 이름을 반환해준다. 그러므로 원래 event객체와 clone()으로 만든 객체가 다르다면 TypeError를 발생시킨다. 이러한 이유로 clone()을 override 하지 않고 다시 Event를 송출하는 경우에는 TypeError가 발생하는 것이다.

 

 

정리하자

Event의 clone() 함수를 override 하는 이유는

  1. Custom Event가 복제될 때 복제된 Event의 속성이 원본 Event 속성과 같은 속성을 가질 수 있도록 하기 위해
  2. 이벤트를 재송출(redispatch)시에 TypeError 직면하지 않기 위해

 

4. clone()함수와 Event 전파와의 관계

많은 분들이 Event전파시 전파가 이뤄질 때마다 clone() 함수가 호출되는 것으로 생각하고 있는 듯하다. 실제로 Flex관련 서적이나 블로그에 보면 그런 내용이 담겨있는 것을 알 수 있다. 여기서는 clone()함수와 Event 전파에 어떤 관계가 있는가 간단한 예제로 알아보고자 한다. 결론을 먼저 말한다면 이들간에는 관계가 없다.

먼저 다음을 살펴보자.

 

ActionScript 3.0 부터 Event를 송출하기 위해 다음과 같은 과정을 거친다.

 

  • 이벤트 생성
    flash.events.Event 클래스를 사용하거나 그것을 확장해서 만든 Custom Event 클래스를 만든다.
    예) var event:MyEvent = new MyEvent( MyEvent.MYTYPE );
  • 이벤트 송출
    Event 클래스나 Custom Event 클래스를 가지고 객체를 생성하여 EventDispatcher를 확장한 클래스의 dispatchEvent()메소드를 이용해 이벤트를 송출한다.
    예) eventdispatcher.dispatchEvent( event );
  • 이벤트 전파
    이벤트 전파는 Visual 객체 즉, DisplayObject를 상속한 모든 객체에서만 가능하다. DisplayObject는 Eventdispatcher 클래스를 확장해서 만들어졌기 때문에 “이벤트 송출”을 할 수 있다.여기서 전파란 부모-자식 관계에 있는 객체들간에 이벤트 전달방법이다. 가령, A-B-C-D 의 순으로 부모-자식 관계를 형성했다면 C 객체에서 이벤트를 송출하는 경우 A->B->C->B->A 형태로 이벤트가 전파된다. 이벤트 전파방식을 세부적으로 나누면 capture, target, bubble 로 나뉠 수 있는데 앞에 A->B(부모->자식들)가 capture 과정, C가 target과정, B->A(자식들->부모)는 bubble 과정을 의미한다. C에서 발생했기 때문에 그의 자식인 D로는 이벤트 전파가 이뤄지지 않는다.이벤트 전파는 반드시 DisplayObject를 확장한 Visual 객체에서만 가능하다. 그게 아니라면 이벤트 전파 단계는 이뤄지지 않으며 앞서 설명한 capture, target, bubble과정중 target 과정만 이루어진다.
  • 이벤트 청취
    송출된 이벤트를 받는 것은 EventDispatcher를 확장한 클래스여야 한다. EventDispatcher에 있는 addEventListener() 메소드를 이용해 송출된 이벤트를 청취하며 더이상 청취하지 않으려면 removeEventListener() 메소드를 호출하면 된다. 이벤트를 송출한 객체와 청취하는 객체가 다른 것은 이벤트 전파단계를 거치는 Visutal 객체만 가능하며 Visual객체가 아니라면 이벤트를 송출한 객체와 청취한 객체가 같다.
    예) eventdispatcher.addEventListener( MyEvent.MYTYPE, myEventHandler );

 

 

지금까지 이벤트를 “생성-송출(재송출 포함)-청취” 하는 단계만 언급했지, “이벤트 전파”는 언급하지 않았다. 이벤트 전파는 Visual 객체만 가능하다. 그럼 위 설명대로 A-B-C-D순으로 부모-자식관계가 형성이 되어 있다면 이벤트 전파시 이벤트가 A->B->C->B->A 형태로 나아갈 때, clone() 메소드가 호출되는지 예제를 들어 알아보겠다.

 

먼저 Custom Event를 아래와 같이 만든다. 주목할 점은 clone()을 override하는데 trace(”clone 호출됨”)을 실행하고 있다는 것이다. 이것을 넣은 이유는 이벤트 전파시 clone() 메소드가 호출되는가 확인하기 위함이다.

 

 

package
{
	import flash.events.Event;

	public class CustomEvent extends Event
	{
		public static const TEST:String = "test";

		public function CustomEvent(type:String, bubbles:Boolean=false, cancelable:Boolean=false)
		{
			super( type, bubbles, cancelable );
		}

		override public function clone():Event
		{
			trace("clone 호출됨");
			return new CustomEvent( type, bubbles, cancelable );
		}
	}
}

 

아래 클래스는 Button을 확장해서 만들었다. 중요하게 볼 것은 위에서 만든 CustomEvent 클래스를 송출한다는 점이다. CustomEvent의 2번째 인자는 bubbles이다. 즉, 이벤트 전파가 가능하게 하기 위해서는 이 인자를 true로 설정해야한다. (마우스 이벤트가 송출되는게 아니다.)

 

package
{
	import flash.events.MouseEvent;

	import mx.controls.Button;

	public class MyButton extends Button
	{
		public function MyButton()
		{
			this.label = "날 눌러줘";
			this.addEventListener( MouseEvent.CLICK, onClick );
		}

		private function onClick( event:MouseEvent ):void
		{
			var customEvent:CustomEvent = new CustomEvent( CustomEvent.TEST, true, false );
			this.dispatchEvent( customEvent );
		}
	}
}

 

 

아래 예제는 위에서 만든 Button을 HBox로 감싸서 자식으로 추가하고 Button에서 발생하는 CustomEvent를 Application과 HBox, 그리고 Button등 모든 전파단계(capture,target,bubble)에서 청취할 수 있도록 하고 있다. 참고로 addEventListener()의 3번째 인자는 cature이다. 이것을 true로 하면 capture단계에서 이벤트 청취를 할 수 있다. false이면 target-bubble과정만 청취가 가능하다.

 

<?xml version="1.0" encoding="utf-8"?>
<mx:Application
xmlns:mx="http://www.adobe.com/2006/mxml"
xmlns:local="*"
layout="absolute"
creationComplete="init()">

	<mx:HBox id="hbox">
		<local:MyButton id="button"/>
	</mx:HBox>

	<mx:Script>
	<![CDATA[
		import flash.events.EventPhase;

		private function init():void
		{
			this.addEventListener( "test", eventHandler );
			hbox.addEventListener( "test", eventHandler );
			button.addEventListener( "test", eventHandler );
			this.addEventListener( "test", eventHandler,true );
			hbox.addEventListener( "test", eventHandler,true );
			button.addEventListener( "test", eventHandler,true );
		}

		private function eventHandler( event:CustomEvent ):void
		{
			var eventPhase:String;
			switch( event.eventPhase )
			{
				case EventPhase.CAPTURING_PHASE:
					eventPhase = "capture";
					break;
				case EventPhase.AT_TARGET:
					eventPhase = "target";
					break;
				case EventPhase.BUBBLING_PHASE:
					eventPhase = "bubble";
					break;
			}
			trace( "target:", event.target, ", currentTarget", event.currentTarget, ", phase:", eventPhase );
		}
	]]>
	</mx:Script>
</mx:Application>

 

위 애플리케이션을 실행하고 버튼을 클릭해보자. 그럼 콘솔창에 다음과 같은 메시지가 보일 것이다.

 

target: EventTest0.hbox.button , currentTarget EventTest0 , phase: capture

target: EventTest0.hbox.button , currentTarget EventTest0.hbox , phase: capture

target: EventTest0.hbox.button , currentTarget EventTest0.hbox.button , phase: target

target: EventTest0.hbox.button , currentTarget EventTest0.hbox , phase: bubble

target: EventTest0.hbox.button , currentTarget EventTest0 , phase: bubble

 

위 메세지에서 target은 변하지 않는다. 왜냐하면 target은 이벤트가 최초 발생한 곳을 참조하기 때문이다. 이 작업은 dispatchEvent()에서 한다는 것을 앞서 설명했다.

 

두번째 currentTarget은 각각 이벤트 전파때마다 다르다. 즉, currentTarget이 지칭하는 것은 현재 이벤트가 지나가고 있는 객체를 참조한다. dispatchEvent()함수에서 최초로 설정되지만 이벤트 전파가 진행됨에 따라 계속 변한다.

 

phase는 위상으로 이벤트 전파가 어떻게 이뤄지고 있는지 보여준다.

 

만약 이벤트 전파를 통해 Event의 clone()이 호출된다면 여기 결과에 “clone 호출됨”메시지가 포함되어야 하지만 보여지지 않고 있다.

 

결론적으로 Event의 clone()은 Event를 재송출(redispatch)와 밀접한 관계가 있다. 하지만 Event 전파와 clone()과는 아무 상관 없으며 각각의 전파 단계(위상)가 변함에 따라 currentTarget과 phase 속성은 변경된다.

 

참고내용

Adobe Flex를 접할때 어려워하는 부분중에 하나가 Event입니다.
깊숙히 이해하려고 할때 잘 이해가 안되는 부분이 많은게 이벤트죠.

기본에 충실해야한다!


프로젝트(http://starpl.com)를 하면서 많이 느꼈습니다.


처음 Flex 접했을때 가장 어려움을 겪었던 것은 기본이 탄탄하지 못한 것으로부터 였습니다.


그래서 Flex와 함께 ActionScript 3.0의 기본 문법 및 대략적 내용을 이해해야한다고 생각합니다.


단순한 문법뿐 아니라 분석/설계에도 관심을 가지고 공부해서 명확하고 깨끗한 프로그램을 만들 수 있도록 노력해야겠구요.

 

아래는 Event에 대한 질문을 걸어놓고 스스로 답변까지 해두었습니다.
이것으로 Event 전부를 설명할 수 없지만 몇가지 궁금한 사항을 이해하는데는 도움이 될겁니다.


그냥 읽고 지식만 얻어가지 마시고 없는 질문이 있다면 스스로 질문/답변을 댓글로 달아주시면 더욱 고맙겠습니다. 잘못된 내용이나 추가해야한다는 것도 포함합니다.

 

1. ActionScript 3.0 이벤트(Event)

1.1 ActionScript 2.0과 3.0에서 이벤트가 바뀐점은?

  • ActionScript 2.0에서는 이벤트 리스너를 추가하는 경우 상황에 따라 addListener() 또는 addEventListener()를 사용하지만 ActionScript 3.0에서는 항상 addEventListener()를 사용합니다.
  • ActionScript 2.0에는 이벤트 흐름이 없으므로 이벤트를 브로드캐스팅하는 객체에서만 addListener() 메서드를 호출할 수 있지만 ActionScript 3.0에서는 이벤트 흐름에 포함된 모든 객체에서 addEventListener() 메서드를 호출할 수 있습니다.
  • ActionScript 2.0에서는 함수, 메서드 또는 객체를 이벤트 리스너로 사용할 수 있지만 ActionScript 3.0에서는 함수 또는 메서드만 이벤트 리스너로 사용할 수 있습니다.
  • on(event) 구문은 이제 ActionScript 3.0에서 지원되지 않으므로 ActionScript 이벤트 코드를 무비 클립에 첨부할 수 없습니다. 이벤트 리스너를 추가할 때는 addEventListener()만 사용할 수 있습니다.

1.2 Event를 사용해야하는 이유? 2가지 이상

  • 객체간 강한 결합 해결 : 애플리케이션 설계시 Callback함수를 사용하는 등의 강한결합을 이용하는 경우 유지보수 및 확장이 어려워지는 경우가 크다. 이벤트는 객체간 느슨한 결합을 해주는데 큰 힘을 발휘할 수 있다.
  • 작업에 대한 일관성 없는 응답 : 동기적 방식과 비동기적 방식에 대한 응답에 대한 처리를 이벤트를 사용하지 않는 경우 서로 다른 방법을 선택해서 사용해야한다. 이벤트는 1가지 방법으로 응답처리를 가능하게 해준다.


2. Event의 구성요소 3가지는 무엇이고 의미를 설명하라.

  • 이벤트 객체(Event Object)
    flash.net.Event의 객체나 이를 확장한 이벤트 객체를 의미한다. 이벤트 객체에는 이벤트에 대한 정보가 들어가있다. 이벤트의 종류(Type)과 이벤트를 발생한 객체(target)에 대한 정보가 포함된다.
  • 이벤트 발생자(Event Dispatcher)
    이벤트를 발생(broadcast)하는 객체이다. 이벤트 객체에 target으로 이벤트 발생자 정보가 담긴다. flash.net.EventDispatcher를 확장한 모든 객체는 이벤트 발생자가 될 수 있다. EventDispatcher의 dispatchEvent() 메소드에 이벤트 객체를 담아 실행하면 이벤트가 발생하게 된다.
  • 이벤트 청취자(Event Listener)
    일종의 메소드 또는 함수이다. 발생한 이벤트를 듣는 역할을 한다. EventDispatcher클래스를 확장한 클래스의 객체에서 addEventListener()을 이용해 이벤트 청취자를 등록할 수 있다. addEventListener()을 이용하면 청취할 이벤트의 종류(type)을 결정할 수 있으며 청취할 이벤트 흐름(capture,target,bubbling) 및 우선순위등을 선택할 수 있다.

3. Event Listener를 등록/삭제하는 방법을 기술하라.

  • 등록
    EventDispatcher의 addEventListener() 메소드를 이용한다.
    예)
    var button:Button = new Button();
    button.addEventListener( MouseEvent.CLICK, onClick, false );
    public function onClick( event:MouseEvent )
    {
      trace( event.type, event.target  );
    }
  • 삭제
    EventDispathcer의 removeEventListener() 메소드를 이용한다.
    예)
    button.removeEventListener( MouseEvent.CLICK, onClick, false );
    3개의 인자가 addEventListener()를 사용할때와 같아야 정상적으로 이벤트 청취자 등록이 삭제된다.
    참고로 맨 마지막 인자는 useCapture이다.

4. Event 전파단계의 종류 3가지가 무엇이며 각각에 대한 설명

  • 이벤트 전파단계의 종류 : 캡쳐(capture) 단계, 타겟(target) 단계, 버블(bubble)단계
  • 이벤트 전파 순서 : 캡쳐->타겟->버블
  • 캡쳐 단계
    루트(root) 디스플레이 객체서부터 이벤트를 발생한 객체(target)까지 디스플레이 객체 리스트(display Object List)를 따라 검색해나가는 단계이다. 즉, 부모->자식으로 이벤트를 전파한다. 이 캡쳐 단계에 이벤트를 청취하기 위해서는 addEventListener()의 3번째 인자인 useCapture가 반드시 true로 되어야 한다. false로 되어 있다면 캡쳐단계가 아닌 타겟과 버블 단계에서 이벤트 청취를 한다.
    캡쳐단계 청취하는 목적은 다양할 수 있다. 가령 중간에 이벤트 전파를 중지시킬때 필요할 수 있다. 이해가 안될 수 있지만 실제 프로젝트를 하면 이런 경우가 다분하다.
    사용 예 )
    button.addEventListener( MouseEvent.CLICK, onClick, true );
  • 타겟 단계
    갭쳐 단계 이후 타겟단계에 도달한다. 타겟 단계는 이벤트를 발생시킨 객체까지 도달했을때 이뤄진다.
  • 버블 단계
    타겟 단계 이후 다시 루트(root) 디스플레이 객체로 이벤트가 전파되는 단계이다. 버블버블 물망울이 수면위로 다시 올라가는 것을 연상하면 되겠다. 캡쳐와 반대로 자식->부모로 이벤트가 전파되는 단계이다.
    이벤트가 버블 단계로 전파되기 위한 조건은 이벤트 송출시 Event의 생성자 인자값중 bubble값을 true로 설정되어 있어야 한다. MouseEvent같은 경우 이 bubble값이 true로 설정되어 있다.
    타겟단계와 버블단계에 이벤트 청취자를 등록하기 위해 addEventListener()의 3번째 인자 useCapture가 false로 되어야 한다. useCapture를 설정하지 않으면 기본값이 false이다.
  • 이벤트 객체가 전파되는 기능이 굳이 필요한 이유
    부모-자식1-자식2….-자식100 의 형태로 디스플레이 객체 리스트가 형성되어있다고 가정하자 자식50에서 자식 100을 마우스 클릭했을때 이벤트를 받고 싶다고 하자. 자식50은 자식100의 존재유무를 모른다(알 수 있지만 알게되면 강한결합이 일어난다.) 이런 경우 이벤트 객체가 전파되는 기능은 매우 유용하게 쓰일 수 있다. 캡쳐나 버블 단계에서 자식50은 자식100에서 발생된 이벤트를 받을 수 있다.
  • flash.display.DisplayObject 클래스는 EventDispatcher를 확장해서 만들어졌다는 것에 주목하자.

5. 4번의 Event 전파는 어떤 환경에서 가능한가?

  • 반드시 디스플레이 객체 리스트(Display Object List) 상에 존재하는 디스플레이 객체(Display Object)여야 이벤트 전파가 가능하다. 그 외의 이벤트 전파는 타겟 단계밖에 없다. 가령, HTTPService나 URLLoader에서 이벤트가 발생하는 경우 이들은 디스플레이 객체가 아니다.
  • 이벤트 전파의 조건
    1. 디스플레이 객체여야 한다.(flash.display.DisplayObject를 확장한 클래스의 객체)
    2. 디스플레이 객체는 디스플레이 객체 리스트에 존재해야 한다. 즉, stage로 부터 addChild되어 있어야 한다.

6. 이벤트 전파시 target과 currentTarget의 차이점은 무엇인가? 예를 들어 설명하라.

  • target은 이벤트를 발생한 객체이고 currentTarget은 이벤트를 청취하는 객체이다.
  • 예시)
    var p:Sprite = new Sprite(); //parent
    var c:Sprite = new Sprite(); //child
    addChild( p );
    p.addChild( c ); //c가 p의 자식인 것에 주목
    p.addEventListener( MouseEvent.CLICK, onClick ); //이벤트 청취를 p가 하는 것에 주목!
    public function onClick( event:MouseEvent )
    {
    trace( event.target, event.currentTarget ); //c를 클릭했을 경우 c, p가 나오고 p를 클릭했을 경우 p,p가 나온다.
    }

7. Event 우선순위에 대해서 간단한 예제를 들어 설명하라.

  • 이벤트 우선순위는 구체적으로 이벤트 청취 우선순위라고 할 수 있다.
  • 이벤트 청취 우선순위는 addEventListener()의 4번째 인자인 priority를 이용한다. 이 값이 클 수록 청취 우선순위가 높아지며 addEventListener()의 등록 순서에 상관없이 우선순위에 따라 이벤트 리스너가 호출된다.
  • 이벤트 청취 우선순위가 유효한 조건
    1. 같은 객체에서 청취한다.
    2. 같은 이벤트 종류(type)을 청취한다.
    3. 같은 useCapure 값을 가진다.
  • ex)
    a.addEventListener(MouseEvent.CLICK, aHandler, false, 3);
    a.addEventListener(MouseEvent.CLICK, bHandler, false, 1);
    a.addEventListener(MouseEvent.CLICK, cHandler, false, 100);

    a객체에 MouseEvent.CLICK 이벤트가 발생시 등록순서에 상관없이 priority값이 가장큰 cHandler가 먼저 호출되고 다음으로 aHandler, bHandler가 호출된다.

8. Event 전파를 중단하는 메소드 2개가 무엇이며 간단한 예제를 들어 각각의 차이점을 설명하라.

  • Event 클래스의 stopPropogation()와 stopImmediatePropogation() 메소드이다.
  • 차이점
    - stopPropogation()
      이벤트 전파를 중단하되 같은 노드의 이벤트를 청취까지는 허락한다.
    - stopImmediatePropogation()
      이벤트 전파를 즉시 중단한다.
  • 예제
    back이 배경 canvas이고 button1이 back에 포함된 button일때
    back.addEventListener(MouseEvent.CLICK, event1Handler);
    back.button.addEventListener(MouseEvent.CLICK, event2Handler);
    back.button.addEventListener(MouseEvent.CLICK, event3Handler);
    back.button.addEventListener(MouseEvent.MOUSE_OUT, outHandler);private function event1Handler(e:MouseEvent):void
    {
           trace(’back’);
    }
    private function event2Handler(e:MouseEvent):void
    {
           trace(’button’);
           e.stopPropagation();
           e.stopImmediatePropagation();
    }
    private function event3Handler(e:MouseEvent):void
    {
           trace(’button2′);
    }
    private function event1Handler(e:MouseEvent):void
    {
           trace(’out’);
    }

    e.stopPropagation(); e.stopImmediatePropagation(); 두가지 다 없을경우
            button
            button2
            back
            out
    e.stopPropagation(); 만 실행
            button
            button2
            out
    e.stopImmediatePropagation(); 만 실행
            button
            out

9. addEventListener의  useCapture 인자는 무엇을 의미하는가? 간단한 예제를 들어 설명하라.

  • 이벤트 전파를 청취할때 캡처단계를 청취할지 타겟 및 버블 단계를 청취할지 결정하는 인자이다.
  • 모든 전파 단계에서 청취하기 위해서는 useCapture가 다른 2개의 addEventListener()를 사용해야한다.
  • 예제 생략

10. Sprite, Shape와 같은 클래스에서 어떻게 하면 이벤트를 송출할 수 있는가?

  • dispatchEvent() 메소드를 호출하면 된다.

11. Sprite, Shape와 같이 이벤트 송출할 수 있는 클래스는 근본적으로 어떤 클래스를 상속받았기 때문에 가능한가?

  • Sprite와 Shape는 궁극적으로 DisplayObject를 확장해서 만들어졌으며 DisplayObject는 EventDispatcher를 확장했다. 그러므로 Sprite와 Shape와 같은 클래스는 EventDispatcher의 메소드를 전부 사용할 수있다. 이중에 dispatchEvent()도 사용할 수 있다.

12. IEventDispatcher는 무엇이며 어떤 경우에 이것을 사용해야할까?

  • EventDispatcher의 인터페이스이다.
  • IEventDispatcher를 활용하는 것은 다중상속이 불가능한 AS3에서 유용하다.
    가령, 어떤 클래스가 ProxyObject를 확장해서 만들어져야한다. 그런데 이 클래스는 이벤트도 발생시켜야 하므로 EventDispatcher클래스도 확장해야한다. ActionScript 3.0은 다중상속을 지원하지 않는다.(다중상속은 이점도 있지만 단점도 있다.) 그러므로 이러한 경우에는 주가 되는 클래스인 ProxyObject를 is-a 확장을 하고 EventDispatcher를 has-a 확장을 감행한다음 만들어진 클래스가 EventDispatcher의 기능을 수행한다는 것을 명시적으로 외부에 알려주고 내부적으로 구현시켜주기 위해 IEventDispatcher 인터페이스를 implements 해준다. 이렇게 하면 원하는 기능인 ProxyObject와 EventDispatcher의 기능을 전부 수행하면서도 다중상속의 문제를 극복할 수 있다.
  • ex)
    public class MyProxyObject extends ProxyObject implements IEventDispatcher
    {
      var evnetDispatcher:EventDispatcher = new EventDispatcher();
     
      //내부적으로 IEventDispatcher에 선언한 메소드를 구현한다.  가령…
      public function dispatchEvent( event:Event ):Boolean
      {
         return  eventDispatcher.dispatchEvent( event );
      }
    }

13. preventDefault()와 stopPropagation(), stopImmediatePropagation()의 근본적 차이점은 무엇인가?


  • preventDefault()는 기본 동작(dafault behaviors)를 취소하는데 쓰이는 메소드인 반면 stopPropagation(), stopImmediatePropagation()는 이벤트 전파에만 영향을 준다.

 

  • preventDefault()가 사용 가능한지는 isDefaultPrevented() 메소드로 알 수 있다.

14. preventDefault()를 호출해서 실행을 가능하게 하기 위한 조건은 무엇인가?

  • 이벤트 객체를 생성시 cancelable속성이 true로 설정되어 있어야 한다.

15. 커스텀 이벤트를 만들어야하는 어떤 경우이며 어떻게 만드는가?

  • 커스텀 이벤트는 기본 이벤트 클래스(flash.events.Event) 또는 그 상위 이벤트 클래스를 확장하는 경우를 의미한다.
  • 이벤트를 확장해야하는 경우는 이벤트 송출시에 필요한 정보를 담아내기 위함이다.
  • 만드는 방법
    class MyEvent extends Event
    {
      public static const MY_EVENT:String = “myevent”;
      public var value:String;
      public function MyEvent( type:String, value:String ):void
      {
         super( type, false, false );
         this.value = value;
      }
      overide public function clone():Event
      {
        return new MyEvent( type, value );
      }
    }

16. 커스텀 이벤트가 bubbling이 가능하기 위해 어떤 조건이 필요한가?

  • Event의 생성자의 2번째 인자인 bubble값을 true로 설정한다.

17. 커스텀 컴포넌트 만들때 clone()함수를 override해야하는 이유는 무엇인가?

  • clone()메소드는 기존과 신규 properties들을 clone 내로 설정해 이벤트 객체의 복사본을 돌려준다.
    clone() 메소드를 Event 클래스 확장시 오버라이드 해야하는 이유는 이벤트를 다시 재전달해야하는 경우 묵시적으로 복사해서 재전달할 필요가 있을때 필요하기 때문이다.

 

  • 참고로 toString()도 확장해서 사용하는 것이 좋다. 이벤트에 대한 명세서가 될 수 있기 때문이다.

18. 약참조에 대해서 GC(가비지 콜렉션)과 관련되어 설명하라.

  • addEventListener()함수로 이벤트 청취자를 등록할때 5번째 인자로 useWeakReference 인자가 있다. 이것을 true로 설정하면 이벤트 청취자가 이벤트 청취를 약참조를 하겠다는 의미이다. 반대로 false(기본)이면 강참조이다.
  • 약참조, 강참조를 설명하기 전에 가비지 콜렉션(GC, Garbage Collection)에 대해서 먼저 알아야한다.
    만들어졌던 객체가 더이상 쓸모가 없어졌을때 GC라고 한다. 쓸모가 없어졌다는 것은 참조의 고리가 끊어졌다는 것을 의미한다. 메모리에 상주해 있지만 조만간 메모리에서 없애야할 대상인 것이다. Flash Player는 GC로 선정된 객체들을 메모리에서 삭제하는 시점을 내부적으로 구현해놓고 있다.(많은 분들이 Flash Player GC처리를 못한다고 하지만 실제로는 문제없이 잘하고 있다. 단지 효율적으로 GC처리를 하는지는 의문이지만…)
    Flash Player GC 대상을 찾아내기 위한 방법은 2가지가 있다. 그것은 레퍼런스 카운팅(reference counting)과 마크 앤 스윕(Mark and Sweep)이다. 레퍼런스 카운팅은 생성된 객체가 참조할때마다 참조 카운트를 1씩 더해준다. 또 참조에서 끊어지는 경우 1씩 감소시켜준다. 만약 레퍼런스 카운팅 값이 0이되는 경우 바로 GC 대상이 되는 것이다. 마크 앤 스윕은 레퍼런스 카운팅 만으로 찾을 수 없는 GC대상을 찾아준다. 가령, 두개 이상의 객체가 서로 다른 객체를 참조하고 있다고 가정한다.(순환참조) 그럼 이들 레퍼런스 카운팅 값은 1 이상이다. 그러므로 레퍼런스 카운팅 방법으로는 GC대상을 만들 수 없다. 마크 앤 스윕은  이들 객체가 최상위 부모와 연결되어 있는지 검사하는 방법이다. 부모로 부터 꼬리에 꼬리를 물고 객체가 참조되어 있는 것인지 조사해나가는 방법으로 GC대상을 검색한다.
    레퍼런스 카운팅과 마크앤 스윕 방법으로 찾아낸 GC대상은 메모리에 상주되지만 Flash Player의 GC 삭제방법 로직에 의해 어느 순간 삭제된다.(이 시점은 프로그래머가 어찌할 수 없다. AIR 또는 Flash Player Debug버전의 경우에만 System.gc()를 이용해 강제적으로 GC를 메모리에서 삭제할 수 있긴 하다.)
  • addEventListener()의 useWeakReference를 true로 설정하면 약참조가 되어 레퍼런스 카운팅을 증가시키지 않는다. 강참조의 경우 이벤트 대상(target)의 참조를 지웠다고 해도 addEventListener로 등록한 청취자 메소드가 GC대상이 아니라면 이벤트 대상의 레퍼런스 카운팅 값은 0이 될 수 없다. 결국 프로그래머의 뜻과 다르게 메모리를 낭비하는 일을 초래할 수 있다. 물론 이벤트 대상이 사라지는 시점에 removeEventListener() 사용하면 되겠지만 때로는 그 시점을 모르는 경우도 발생한다. 이러한 경우 useWeakReference를 true로 설정해서 메모리 낭비를 없앨 수 있겠다.
  • Flex에서 List 계열의 컴포넌트를 살펴보면 dataProvider()에 ArrayCollection 객체를 받을때 ArrayCollection의 변화를 살펴보기 위해 addEventListener()를 사용한다. 그런데 ArrayCollection 객체는 언제 어떻게 될지 List 계열 컴포넌트는 알 수 없을 것이다. 이때 유용하게 사용하는 것이 바로 useWeakReference=true로 설정해서 필요없어진 ArrayCollection이 메모리 남는 것을 방지할 수 있다. 참고 : http://blog.jidolstar.com/375


19. 이벤트가 전파되는 중간에 전파를 멈추게 하기 위해 stopPropagation()을 사용하지 않고removeEventListener() 를 사용하면 안되는 이유를 설명하라. (힌트, ActionScript 3.0의 Native 메커니즘에 입각한다. http://bixworld.egloos.com/2149717)

 

  • 이벤트 송출을 시작하면 flash player는 이벤트가 송출될 디스플레이 객체 리스트를 전부 조사해 미리 이벤트 객체를 복사해두기 때문이다. 이미 전파할 대상이 모두 정해져있고 전파가 시행중인 경우에는 중간에 removeEventLisener를 사용하더라도 일단 송출된 이벤트는 끝까지 전파된다. 다음 시점 이벤트 송출시에는 removeEventLisener()가 적용되어 있는 상태가 된다.
 

 

 

20. willTrigger()와 hasEventListener()의 의미를 설명하고와 두 메소드의 차이점을 보여줄 수 있는 대한 예를 들어보자.

 

 

 

  • 공통점
    hasEventListener()이나 willTrigger() 모두 이벤트 청취자가 있는지 검사한다.
  • 차이점
    hasEventListener() : 지정한 객체에(만) 이벤트 청취자가 있는지 조사한다. 이벤트 흐름과는 무관하다.
    willTrigger() : 지정한 객체를 포함하는 디스플레이 객체 리스트에 해당 이벤트가 청취자가 있는지 조사한다. 그러므로 이벤트 흐름과 관련 있다.
  • 예제
    var button:Sprite = new Sprite();
    addChild( button );
    this.addEventListener( MouseEvent.CLICK, onClick );
    trace( button.willTrigger( MouseEvent.CLICK ) ); //true
    trace( button.hasEventListener( MouseEvent.CLICK ) ); //false ->왜 false가 되는가 아는게 중요!
    trace( this.willTrigger( MouseEvent.CLICK ) ); //true
    trace( this.hasEventListener( MouseEvent.CLICK ) ); //true
  • 이 두개의 함수를 잘 활용하면 쓸데없이 이벤트를 송출하는 경우를 방지할 수 있다.

21. ActionScript 3.0 이벤트에 대해 참고할 만한 문헌 및 자료를 조사하자.

Douglas9님의 블로그에 소개된 ActionScript Cookie Util을 보고 포스팅한다.

 

ActionScript 3.0만으로 쿠키를 제어할 수 있다? 그냥 막연하게 할 수 있다고 생각했는데 소개할 유틸 코드와 같은 방식을 생각하지 않았다. 본인은  HTML안에 작성된 Javascript를 코드를 사용하지 않고 ActionScript 3.0만으로 쿠키를 제어할 수 있다는 생각을 못했다. 근데 그게 가능했다. (왜 진작 이 생각을 못했지?)

 

소개할 유틸의 사용하는 방법은 다음과 같이 아주 간단하다.


import de.aggro.utils.CookieUtil;
//Set a cookie named mycookie with a value of mycookie value with a time to live of 30 days
CookieUtil.setCookie(“mycookie”, “mycookie value”, 30);
//Get that cookie and trace its value
trace(CookieUtil.getCookie(“mycookie”));
//Delete the cookie from the users computer
CookieUtil.deleteCookie(“mycookie”);

 

더 이상 설명할 필요가 없을 정도로 쉽게 사용할 수 있다.  아래는 유틸 소스이다.


package de.aggro.utils
{
    import flash.external.ExternalInterface;
   
    public class CookieUtil
    {
        public function CookieUtil()
        {
        }
       
        private static const FUNCTION_SETCOOKIE:String =
“document.insertScript = function ()” +
“{ ” +
“if (document.snw_setCookie==null)” +
“{” +
“snw_setCookie = function (name, value, days)” +
“{” +
“if (days) {”+
                            “var date = new Date();”+
                            “date.setTime(date.getTime()+(days*24*60*60*1000));”+
                            “var expires = ‘; expires=’+date.toGMTString();”+
                        “}” +
                        “else var expires = ”;”+
                        “document.cookie = name+’='+value+expires+’; path=/’;” +
         “}” +
“}” +
“}”;
       
        private static const FUNCTION_GETCOOKIE:String =
“document.insertScript = function ()” +
“{ ” +
“if (document.snw_getCookie==null)” +
“{” +
“snw_getCookie = function (name)” +
“{” +
“var nameEQ = name + ‘=’;”+
                        “var ca = document.cookie.split(’;');”+
                        “for(var i=0;i < ca.length;i++) {”+
                            “var c = ca[i];”+
                            “while (c.charAt(0)==’ ‘) c = c.substring(1,c.length);”+
                            “if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length,c.length);”+
                        “}”+
                        “return null;” +
         “}” +
“}” +
“}”;

private static var INITIALIZED:Boolean = false;
       
        private static function init():void{
            ExternalInterface.call(FUNCTION_GETCOOKIE);
            ExternalInterface.call(FUNCTION_SETCOOKIE);
            INITIALIZED = true;
        }
       
        public static function setCookie(name:String, value:Object, days:int):void{
            if(!INITIALIZED)
                init();
           
            ExternalInterface.call(“snw_setCookie”, name, value, days);
        }
       
        public static function getCookie(name:String):Object{
            if(!INITIALIZED)
                init();
           
            return ExternalInterface.call(“snw_getCookie”, name);
        }
       
        public static function deleteCookie(name:String):void{
            if(!INITIALIZED)
                init();
           
            ExternalInterface.call(“snw_setCookie”, name, “”, -1);
        }

    }
}

소스를 보니 놀랍다. ExternalInterface를 이용해 JavaScript와 통신할 수 있다는 것은 알고 있을 것이다.  초반에 ExternalInterface의 call()메소드를 이용해 쿠키를 제어할 함수를 Javascript의 document.insertScript로 등록한 뒤, 등록된 함수를 사용하는 형태이다. 즉, 자바스크립트 코드를 심어주고 그것을 호출하는 형태이다. 이러한 방법은 쿠키를 제어하는 것 뿐아니라 다양한 방법으로 응용될 수 있다고 생각한다.

 

소개한 유틸은 순수 ActionScript 3.0에서 만들었기 때문에 Flash, Flex 등 상관없이 유용하게 사용할 수 있겠다.  아래링크에 만든사람의 블로그와 위 코드를 다운로드 받을 수 있는 링크를 걸어두었다. 참고하자.

이런식으로 만든 IFrame 이 있는데 아래에서 볼 수 있다.

플생사모 카페에 데모로스 님이 추상클래스에 대해서 올린 글이 있어서 그 글을 이용해 조금 정리해봤다.

 

ActionScript 3.0에서는 추상클래스(abstract class)가 없다. Java는 abstract 키워드가 있는데 ActionScript 3.0에는 없다. 추상클래스라는 것은 구현체이면서 자기 자신은 직접 생성하는 것을 허락하지 않는 클래스이다. 가령 DisplayObject, DisplayObjectContainer, InteractiveObject,Graphics의 경우 new 연산자를 이용해 직접 생성이 불가능하다. 이 때문에 이들 클래스는 추상클래스라고 추측할 수 있다.

 
(추상클래스와는 별개지만 이들 클래스는 확장도 안된다. 즉 상속할 수 없다는 것이다.  이들이 어떻게 구현되어 있는 것인지 native 코드이므로 알 수 없다.)

 

ActionScript 3.0에서 추상 클래스(abstract class)와 인터페이스(interface)와 비교하면 다음과 같다.

  1. 추상클래스는 구현부분이 존재한다. 하지만 인터페이스는 존재하지 않는다.
  2. 추상클래스는 일반 구현클래스처럼 다중 상속이 지원되지 않지만 인터페이스는 다른 인터페이스 다중상속이 가능하다.
  3. 추상클래스를 확장할 때 extends 키워드 사용하지만 인터페이스를 구현할때는 implements 키워드를 이용한다. 인터페이스 구현은 다중구현이 가능하다.
  4. 추상클래스와 인터페이스 둘 다 new 연산자로 생성할 수 없다.

다음 코드는 ActionScript 3.0으로 추상클래스를 흉내내본 클래스이다.


package 
{
	import flash.errors.IllegalOperationError;
	import flash.utils.getDefinitionByName;
	import flash.utils.getQualifiedClassName;
	import flash.utils.getQualifiedSuperclassName;

	public class AbstractClass 
	{
		public var name:String =""

		public function AbstractClass()
		{
			checkAbstract();
		}

		private function checkAbstract():void 
		{
			var className : String = getQualifiedClassName(this);
			if (getDefinitionByName(className) == AbstractClass ) 
			{
				throw new ArgumentError(
				getQualifiedClassName(this) + "클래스를 인스턴스화할 수 없습니다.");
			}
		}

		public function doSomething1():void
		{
			trace("doSomething1() 실행됨")
		}

		public function doSomething2() : void 
		{
			var message : String = 
				"추상 메서드 doSomething2(" + getQualifiedSuperclassName(this) + ")이(가) " 
				+ getQualifiedClassName(this) + "클래스에서 구현되지 않았습니다.";
			throw new IllegalOperationError(message); 
		}
	}
}

 

위 코드 만으로도 실행시 new 연산자로 이 클래스를 객체화 할 수 없다. 그러나 아쉽게도 이 코드는 컴파일시에 에러를 잡을 수 없다. abstract 키워드가 없는 결과이다. 다른 방법은 없는 것 같다. namespace 또는 외부에서 접근할 수 없는 클래스를 심어 놓아도 완벽히 추상클래스를 만들 수 없다.

 

이 클래스를 이용해 다음과 같이 new 연산자를 사용하면 실행시 에러가 난다.

var instance:AbstractClass = new AbstractClass(); //AbstractClass 클래스를 인스턴스화 할 수 없습니다.

만약 이 AbstractClass를 확장해서 ConcreteClass를 만들었다고 가정하자. 다음과 같이 ConcreteClass를 만들었지만 AbstractClass의 doSomthing2() 함수를 override하지 않는다면 다음과 같은 메세지가 나올 것이다.


package
{
	public class ConcreteClass extends AbstractClass
	{
		public function ConstructClass()
		{
			super();
		}
   }
}

 

doSomething2(AbstractClass)이(가) ConcreteClass클래스에서 구현되지 않았습니다.

반드시 ConcreteClass에서 doSomething2() 함수를 아래와 같이 구현해야한다.
 


package
{
	public class ConcreteClass extends AbstractClass
	{
		public function ConstructClass()
		{
			super();
		}

		override public function doSomething2() : void 
		{
			trace("구현함");
		}
	}
}

 

ActionScript 3.0에서 추상클래스를 어떻게 만들어야 하는가 알아보았다. 역시 부족한 점이 많다. 차기 ActionScript에서는 추상클래스를 지원해줬으면 좋겠다.

Flex/AIR 개발을 하다보면 개발자가 정의하는 커스텀 컴포넌트를 제작할 필요가 있을 때가 많다. 기존 Flex 컴포넌트를 조합하는 경우도 있는 반면, UIComponent를 확장해서 기존에 있지 않던 컴포넌트를 만드는 경우도 종종 있다.

이 글은 Flex 또는 AIR 커스텀 컴포넌트를 만드는데 있어서 한가지 Tip을 언급한다.
아래 프로그램 처럼 Slider를 만들었다고 하자. 이 Slider는 매우 단순하다. 단순히 Button객체만 자식으로 등록되어 있고 마우스를 이용해 버튼을 좌우측으로 이동할 수 있다.

 


여기서 중요하게 다루는 것은 바로 Thumb을 마우스로 잡고 움직일 때이다.

 

위 프로그램에서 Thumb부분을 마우스로 잡아 움직여 보자.  마우스를 누른 상태에서 Thumb의 영역을 벗어나 상단이나 하단에서 움직여보자. 또 빠르게 움직여보자. 마우스 위치가 Thumb위에 있지 않은데도 Thumb은 잘 움직인다. 마우스 버튼을 누르지 않는 이상(Mouse Up) Thumb은 정상적으로 잘 움직인다. 당연히 그래야하지만 개발자에게 당연한 것은 당연하지 않을 수 있다. “이것을 어떻게 구현했을까?” 한번 고민하게 된다는 것이다.

 

매우 단순한 컴포넌트임에도 불구하고 아래 내용을 알지 못하면 이런 컴포넌트를 만드는데 삽질할 수 있다.

 

1. Shields를 사용하지 않기

Shield란 무엇인가? 방패, 보호물 뜻을 가지고 있다. 방금 보여준 Flex 프로그램에서 Thumb을 마우스로 잡고 움직일 때 Thumb의 범위를 벗어나도 잘 움직였던 것은 Shield가 있기 때문이다. Shield가 때문에 Thumb의 움직임이 보호받는 것이라 생각하면 되겠다.

 

어떻게 구현 했을까 단순하게 생각한다면 Thumb 주변에 안보이는 Shield가 존재해서 Thumb의 범위를 연장했다고 보면 된다. Thumb의 범위가 연장되면 그 영역의 마우스 이벤트를 받을 수 있기 때문에 위에서 보여준 프로그램과 같이 구현할 수 있을 것이다.

위 프로그램의 소스를 살펴보겠다.

 

 

위 소스를 다운로드 받아 실행해봐도 되겠다. Flex SDK 3.2 환경이다.


<?xml version=“1.0″ encoding=“utf-8″?>
<mx:Application
    xmlns:mx=“http://www.adobe.com/2006/mxml”
    xmlns:local=“*”
    layout=“vertical”>
    <local:MySlider width=“200″ height=“5″/>
    <mx:Label text=“jidolstar.com”/>
</mx:Application>

 

Flex Application 은 매우 단순하다.  MySlider 컴포넌트를 자식으로 추가했을 뿐이다. 그럼 MySlider를 살펴보자.


package
{
    import flash.display.DisplayObject;
    import flash.events.Event;
    import flash.events.MouseEvent;
   
    import mx.controls.Button;
    import mx.core.UIComponent;
    import mx.events.ResizeEvent;
    import mx.events.SandboxMouseEvent;

    /**
     * 슬라이더 예제
     * systemManager.deployMouseShields() 함수를 활용하는 예제이다.
     * @author Yongho Ji
     * @since 2009.02.11
     */
    public class MySlider extends UIComponent
    {
        /**
         * @private
         * thumb으로 이용할 버튼
         */
        private var thumb:Button;
       
        /**
         * @private
         * thumb의 위치
         */
        private var thumbPos:Number = 0;
       
        /**
         * @private
         * thumb의 한계 위치
         */
        private var maxThumbPos:Number;
       
        /**
         * @private
         * thumb 위치가 바뀌었을 때 true 설정
         */
        private var thumbPosChanged:Boolean = true;
       
        /**
         * @private
         * Track 그리기를 요청할때 true 설정
         */
        private var drawTrackRequest:Boolean = true;
       
        /**
         * 생성자
         */
        public function MySlider()
        {
            super();
            this.addEventListener( ResizeEvent.RESIZE, resizeHandler );
        }
       
     /**
     * @private
     */
        protected override function createChildren():void
        {
            super.createChildren();
           
            //thumb 생성
            if( !thumb )
            {
                thumb = new Button();
                thumb.addEventListener(MouseEvent.MOUSE_OVER,
                                        thumb_mouseOverHandler );
                thumb.setActualSize(20,20);
                addChild( thumb );    
            }
        }

     /**
     * @private
     */
        protected override function measure():void
        {
            super.measure();
           
            //기본 사이즈 설정
            this.minWidth = 100;
            this.minHeight = 3;
        }

     /**
     * @private
     */
        protected override function updateDisplayList(uw:Number, uh:Number):void
        {
            super.updateDisplayList( uw, uh );
           
            //Track그리기
            if( drawTrackRequest )
            {
                //최대 위치값 조절
                maxThumbPos = uw - thumb.width;
                setThumbPos( thumbPos );
                drawTrackRequest = false;
                graphics.clear();
                graphics.beginFill( 0×000000, 1 )
                graphics.drawRect(0,0,uw,uh);
                graphics.endFill();
            }
           
            //Thumb위치조절
            if( thumbPosChanged )
            {
                thumbPosChanged = false;
                thumb.move( thumbPos, uh/2-thumb.height/2 );
            }
        }
       
        /**
         * @private
         * thumb의 위치를 지정한다.
         */
        private function setThumbPos( pos:Number ):void
        {
            if( thumbPos == pos ) return;
           
            thumbPos = pos-thumb.width/2;
            if( thumbPos < 0 )
            {
                thumbPos = 0;
            }
            else if( thumbPos > maxThumbPos )
            {
                thumbPos = maxThumbPos;
            }
            this.thumbPosChanged = true;
            this.invalidateDisplayList();
        }
       
        /**
         * @private
         * track을 다시 그리도록 요청
         */
        private function resizeHandler(event:ResizeEvent ):void
        {
            drawTrackRequest = true;
            this.invalidateDisplayList();
        }

        /**
         * @private
         */
        private function thumb_mouseOverHandler( event:MouseEvent ):void
        {
            thumb.addEventListener( MouseEvent.MOUSE_OUT, thumb_mouseOutHandler );
            thumb.addEventListener( MouseEvent.MOUSE_DOWN, thumb_mouseDownHandler );
        }
       
        /**
         * @private
         */
        private function thumb_mouseOutHandler( event:MouseEvent ):void
        {
            thumb.removeEventListener( MouseEvent.MOUSE_OUT, thumb_mouseOutHandler );
            thumb.removeEventListener( MouseEvent.MOUSE_DOWN, thumb_mouseDownHandler );
        }        
       
        /**
         * @private
         */
        private function thumb_mouseUpHandler( event:MouseEvent ):void
        {
            thumb_mouseLeaveHandler(event);
        }

        /**
         * @private
         */
        private function thumb_mouseMoveHandler( event:MouseEvent ):void
        {
            if( event.buttonDown == false )
            {
                thumb_mouseLeaveHandler(event);
                return;
            }            
            setThumbPos( mouseX );
        }
       
        /**
         * @private
         */
        CONFIG::MOUSE_SHIELDS
        private function thumb_mouseDownHandler( event:MouseEvent ):void
        {
     var sbRoot:DisplayObject = systemManager.getSandboxRoot();
     sbRoot.addEventListener( MouseEvent.MOUSE_UP, thumb_mouseUpHandler, true);
     sbRoot.addEventListener( MouseEvent.MOUSE_MOVE, thumb_mouseMoveHandler, true);
     sbRoot.addEventListener( SandboxMouseEvent.MOUSE_UP_SOMEWHERE, thumb_mouseLeaveHandler); // in case we go offscreen
     systemManager.deployMouseShields(true);    
     setThumbPos( mouseX );                
        }
               
        /**
         * @private
         */
        CONFIG::MOUSE_SHIELDS
        private function thumb_mouseLeaveHandler(event:Event):void
        {
            var sbRoot:DisplayObject = systemManager.getSandboxRoot();
            sbRoot.removeEventListener( MouseEvent.MOUSE_UP, thumb_mouseUpHandler, true);
            sbRoot.removeEventListener( MouseEvent.MOUSE_MOVE, thumb_mouseMoveHandler, true);
            sbRoot.removeEventListener( SandboxMouseEvent.MOUSE_UP_SOMEWHERE, thumb_mouseLeaveHandler); // in case we go offscreen
            systemManager.deployMouseShields(false);            
        }
       
        /**
         * @private
         */
        CONFIG::THUMB_SHIELDS
        private function thumb_mouseDownHandler( event:MouseEvent ):void
        {
            thumb.graphics.clear();
            thumb.graphics.beginFill( 0xff0000, 0.4 );
            thumb.graphics.drawRect( -100, -100, thumb.width+200, thumb.height+200 );
            thumb.graphics.endFill();
            thumb.addEventListener( MouseEvent.MOUSE_UP, thumb_mouseUpHandler );
            thumb.addEventListener( MouseEvent.MOUSE_MOVE, thumb_mouseMoveHandler );
            setThumbPos( mouseX );                
        }
               
        /**
         * @private
         */
        CONFIG::THUMB_SHIELDS
        private function thumb_mouseLeaveHandler(event:Event):void
        {
            thumb.graphics.clear();        
            thumb.removeEventListener( MouseEvent.MOUSE_UP, thumb_mouseUpHandler );
            thumb.removeEventListener( MouseEvent.MOUSE_MOVE, thumb_mouseMoveHandler );
        }    
       
        /**
         * @private
         */
        CONFIG::NONE_SHIELDS
        private function thumb_mouseDownHandler( event:MouseEvent ):void
        {
            thumb.addEventListener( MouseEvent.MOUSE_UP, thumb_mouseUpHandler );
            thumb.addEventListener( MouseEvent.MOUSE_MOVE, thumb_mouseMoveHandler );
     setThumbPos( mouseX );                
        }
               
        /**
         * @private
         */
        CONFIG::NONE_SHIELDS
        private function thumb_mouseLeaveHandler(event:Event):void
        {
            thumb.removeEventListener( MouseEvent.MOUSE_UP, thumb_mouseUpHandler );
            thumb.removeEventListener( MouseEvent.MOUSE_MOVE, thumb_mouseMoveHandler );
        }                
    }
}

 

Flex로 컴포넌트를 제작해보신 분들이라면 위 코드를 어렵지 않게 해석할 수 있으리라 판단한다.

 

이벤트 핸들러에 “CONFIG::~” 가 있는데 이것은 조건부 컴파일을 위해 존재한다. 이 예제는 3가지 경우에 따라 컴파일을 해서 테스트 하기 때문에 이 부분을 추가했다. 가령 CONFIG::NONE_SHIELDS가 있는 함수는 컴파일 하되 CONFIG::THUMB_SHIELD, CONFIG::MOUSE_SHIELD 가 붙은 함수는 컴파일에 제외하려면 컴파일 옵션에 다음을 추가하면 된다. C언어에서 #if, #endif와 비슷한 것이라 생각하면 되겠다.

-define=CONFIG::MOUSE_SHIELDS,false
-define=CONFIG::THUMB_SHIELDS,false
-define=CONFIG::NONE_SHIELDS,true

여기서는 Shield를 사용하지 않는 예이므로 위처럼 컴파일 옵션을 지정하면 되겠다. 프로그램을 실행해 보면 아래와 같다.

 

 

처음에 보여준 것과 겉모습으로는 차이가 없다.  하지만 Thumb을 움직여보면 바로 어떤 문제가 있는지 확인할 수 있을 것이다. Thumb을 잡고 움직이되 빨리 움직여보자. 제대로 움직이는가? 마우스 커서가 Thumb을 벗어나면 더 이상 움직이지 않는다. 왜 그럴까? 바로 Thumb이 받을 수 있는 마우스 이벤트 영역은 Thumb크기에 제한되어 있기 때문이다. Thumb만 벗어나면 MouseOut 이벤트가 발생해서 더 이상 MouseMove에 대응하는 움직임을 수행할 수 없는 것이다.

 

이것이 바로 Shield를 만들지 않고 사용한 결과이다.

 

이렇게 컴포넌트를 만들면 당연히 혼난다. ^^;;

 

2. Thumb에 Shield를 만들기

첫번째 예제에서 Shield의 필요성을 느꼈을 것이다. 그래서 이번에는 Thumb을 움직이기 위해 MouseDown했을 때, Thumb보다 어느 정도 크게 Shield를 만들어 Thumb을 약간 벗어난다 하더라도 지속적으로 Thumb을 움직일 수 있게 해보겠다.

이번에는 아래처럼 컴파일 옵션을 줘보자.

-define=CONFIG::MOUSE_SHIELDS,false
-define=CONFIG::THUMB_SHIELDS,true
-define=CONFIG::NONE_SHIELDS,false

아래는 컴파일한 결과물을 실행한 모습이다.

 

 

위 프로그램에서 Thumb를 움직여보자. 빨간색으로 나오는 것은 Thumb에 그려준 Shield이다. 이 영역이 Thumb을 벗어나도 계속 Thumb을 움직일 수 있게 마우스 감지 영역을 잡는 역할을 해준다. 필요하다면 더 크게 그려도 되며 실제 만들때는 alpha속성을 0으로 주어 보이지 않게 해야할 것이다.

 

Shield영역을 벗어나지 않는 이상 잘 움직인다. 이제 Shield영역만 좀 크게 잡아주면 문제가 없어보인다.

 

하지만….

문제는 아직도 있다. Thumb을 움직이면서 아래 “jidolstar.com”  Label위로 올려보자. 더이상 Thumb이 움직이지 않는다. 이 문제는 MySlider와 Label의 객체가 그의 부모 위에 붙은 순서가 MySlider-Label순이기 때문이다. 만약 Label-MySlider 순으로 붙었다면 아무 상관없을 것이지만 그것을 어찌 알 수 있겠는가? 만약 이대로 MySlider를 만들어 배포하면 다른 컴포넌트들과 함께 붙여 사용할 것인데 위와 같은 문제를 발생시키지 않으려면 MySlider가 가장 높은 위치에 있어야 할 것이다. 그러므로 사용하기에는 여전히 문제가 많다!!!

 

아마도 Flex SDK를 분석해보지 않은 사람이라면 위와 같이 프로그래밍을 할 가능성이 크다. 본인도 그랬으니깐… ^^;;

 

3. SystemManager.deployMouseShields()를 이용한 Shield를 만들어 사용하기

이 부분이 첫번째, 두번째 예제에서 보여준 문제점을 깔끔하게 해결하는 방법이다. 먼저 아래와 같이 컴파일 옵션을 설정하자.

-define=CONFIG::MOUSE_SHIELDS,true
-define=CONFIG::THUMB_SHIELDS,false
-define=CONFIG::NONE_SHIELDS,false

실행해보면 아주 잘된다!

 

 

이쯤되면 “이거 어떻게 한거야?” 궁금할것이다.

 

본인은 첫번째, 두번째 예제와 같은 문제점을 겪으면서 해결방법이 없을까 생각하다가 Flex SDK에 있는 Scrollbar 컴포넌트에서 답을 얻었다. 이 문제점은 검색을 해도 해결방법이 안나온다. Flex SDK를 만든 사람과 경험해 본 사람만 알 수 있다.

 

위 코드에서 CONFIG::MOUSE_SHIELDS 를 부분이 있는 thumb_mouseDownHandler()와 thumb_mouseLeaveHandler()를 보자. 아래처럼 낯선 코드가 있다.

var sbRoot:DisplayObject = systemManager.getSandboxRoot();

sbRoot.addEventListener( MouseEvent.MOUSE_UP, thumb_mouseUpHandler, true);

sbRoot.addEventListener( MouseEvent.MOUSE_MOVE, thumb_mouseMoveHandler, true);

sbRoot.addEventListener( SandboxMouseEvent.MOUSE_UP_SOMEWHERE, thumb_mouseLeaveHandler); // in case we go offscreen

systemManager.deployMouseShields(true);

Flex는 SystemManager가 모든 운영의 중심에 있다. Application이 있다고 생각하겠지만 사실은 아니다. SystemManager가 Application을 생성하고 그외 중요한 모든 핵심기능을 가지고 있다. Application은 단지 컴포넌트를 담는 그릇 정도라고 생각하면 된다. 이 SystemManager를 잘 활용하면 해결하기 어려운 것도 해결하기 쉬워질 수 있다.

 

위 코드처럼 모든 UIComponent를 상속한 컴포넌트는 systemManager를 참조할 수 있다. 여기서 사용한 코드는 SystemManager에 정의된 getSandboxRoot()와 deployMouseShields() 함수이다. getSandboxRoot()는 샌드박스내에 최상위 systemManager를 취득하는데 쓰인다. 즉 지금 보고 있는 애플리케이션의 root이다. 그리고 deployMouseShields()는 Mouse 이벤트를 탐지할 Shield를 만들어주는 역할을 한다. 이 Shield가 getSandboxRoot()로 부터 얻은 systemManager에 등록된 마우스 이벤트 핸들러가 동작할 수 있는 마우스 이벤트 감지 영역인 것이다. addEventListener로 이벤트를 등록시 3번째 인자인 caputure에 true를 붙인 이유는 bubble과정에서 이벤트를 감지는 자식들을 통과해 systemManager로 올라오기 때문에 무의미하므로 바로 systemManager에서 마우스 이벤트를 첫번째로 받기 위함이다.

정리하며

Flex SDK에 있는 기존 컴포넌트가 어떻게 만들어졌는가 분석하는 것은 커스텀 컴포넌트를 제대로 제작하는 가이드 라인이 될 수 있다. 이러한 부분들이 모두 Adobe Livedocs에 잘 설명되어 있으면 좋겠지만 그렇지 않은 부분도 상당히 많다. 그 부분은 개발자들이 알아서 찾아내야한다. Flex/AIR 개발을 하시는 분이라면 어떤 부분을 구현하기 위해 너무 자신의 생각으로 개발하려하지 말자. 대신 Flex SDK에 있는 컴포넌트에서 그 구현을 할 법만한 부분을 찾아 분석한 후 적용하는 편이 훨씬 좋은 코드를 만들 수 있다. 한마디로 Flex SDK를 까보라는 말이다. ^^

관련 사이트

ActionScript 3.0은 객체지향언어이다. 객체 지향의 일반적인 특징은 캡슐화, 상속, 다형성일 것이다.

 

Flex/AIR가 MXML이라는 특수한 XML형태의 언어를 사용하는 것은 순수한 ActionScript 3.0 프로그래밍보다 더 쉽게 개발 하기 위한 일종의 도구인 것이다. Flex 컴파일러는 MXML을 ActionScript 3.0로 변환한뒤 컴파일한다. 컴파일 옵션으로 -keep-generated-actionscript를 입력하면 generated폴더에 MXML이 ActionScript 3.0으로 변환된 것을 확인할 수 있다. 결국 필연적으로 Flex 또는 AIR를 제대로 개발하려면 ActionScript 3.0을 이해해야한다.

 

ActionScript 3.0는 객체지향언어이므로 객체지향을 얼마나 잘 이해하고 활용하느냐에 따라 프로그램의 설계에 큰 영향을 미치게 된다. 덧붙여 Flex와 AIR의 Data Binding 이라는 데이터-UI의 연동을 아주 쉽게 해주는 기능이 있어서 객체지향으로만은 느끼지 못하는 또 다른 새로운 감동을 준다.

 

결론적으로 Flex,AIR로 개발할 때는 ActionScript 3.0의 객체지향 방식을 잘 활용하고 Flex,AIR만의 데이터 바인딩과 같은 유용한 점을 잘 활용하여 개발해야 개발의 능률이 오르고 훨씬 보기 좋은 코드와 UI을 만들어 낼 수 있다.

 

여기서는 두가지 개념을 가지고 이야기 한다. 객체지향 개념중 하나인 다형성, 그리고 Data Binding(데이터 바인딩)이다.

 

다형성이라는 것은 여러형태로 쓸 수 있다는 말인데 이를 지원하는 대표적인 것인 바로 인터페이스(Interface)이다. 인터페이스는 매우 유용한 도구로 이것을 잘 사용하느냐 안하느냐에 따라 설계가 엉터리가 되느냐 안되느냐를 결정할 정도로 중요한 개념이다.

 

인터페이스가 다형성을 지원한다는 것은 인터페이스 하나를 만들어 그 인터페이스를 구현한 클래스를 여러개 접근이 가능해진다는데 있다. 가령, IShape라는 인터페이스가 있다고 하자. IShape는 Rectangle, Triangle, Circle등의 클래스로 구현되어 질 수 있다. 이들 클래스는 IShape를 사용하는 MyCanvas에 사용할 수 있는데 MyCanvas는 IShape덕분에 Rectangle, Triangle, Circle에 대해서 알지 못해도 IShape만 가지고 사용이 가능한 것이다.

 

다형성은 인터페이스로부터 출발해서 추상클래스, 구현클래스로 확장하지만 ActionScript 3.0에서는 Java와 같이 추상클래스 개념이 없다. 그러므로 필요하다면 구현클래스를 가지고 추상클래스처럼 사용하는 경우도 있게 된다. 만약 ActionScript 3.0에도 추상클래스와 private 생성자를 지원해준다면 프로그램 디자인 하는데 꽤 유용해질 것이라 생각이 든다.

 

또 한가지 논할 것은 데이터 바인딩(Data Binding)이다. 데이터 바인딩은 말그대로 데이터를 묶어준다는 의미이다. 무엇과 묶는 것인가? 보통 데이터와 그 데이터 결과를 출력하는 화면(View)과 묶어주는 것이다. 그래서 데이터가 변하면 자동적으로 화면이 능동적으로 변하도록 하는 것이다. 만약 데이터 바인딩 기술을 사용하지 않는다면 데이터 변하는 시점을 따로 알아야 하고 그 시점에 화면(View)에 출력하는 코드가 따로 작성되어야 한다. Flex는 [Bindable] 메타데이터나 <mx:Binding>과 같은 훌륭한 클래스들을 만들어놔서 데이터 바인딩을 쉽고 간편하게 할 수 있도록 지원해주고 있다. 만약 ActionScript 3.0으로만 엔터프라이즈급 프로젝트를 수행해야 하는데 데이터 바인딩 기술이 없다면 참 답답할 것이다. 이 말은 Flex/AIR 설계 및 개발시에 반드시 데이터 바인딩을 고려해야한다는 것을 의미한다.

 

이렇게 ActionScript 3.0의 객체지향의 다형성과 Flex/AIR의 데이터 바인딩 기술은 매우 유용하고 필수적으로 이해하고 응용할 줄 알아야 한다.

 

그럼 결국 오늘 언급하고자 하는게 뭐냐?! ㅋㅋ

 

그건 인터페이스에서 데이터 바인딩이다.


아래 ISimpeModel 이름을 가진 인터페이스와 그것을 구현한 SimpeModle 클래스가 있다.

//ISimpleModle.as
package
{
	public interface ISimpleModel 
	{
		function get name():String;
		function set name(value:String):void;
		function get age():int;
		function set age(value:int):void;
	}
}
//SimpleModel.as
package 
{
	public class SimpleModel implements ISimpleModel 
	{
		private var _name:String = "";
		private var _age:int = 0;

		public function SimpleModel() 
		{
		}

		public function get name():String 
		{ 
			return _name; 
		}
		public function set name(value:String):void 
		{
	  		_name = value;
		}

		public function get age():int
		{
			return _age;
		}

		public function set age(value:int):void
		{
			_age = value;
		}		
	}
}

 


위 클래스를 사용하는 Flex Main Application은 아래와 같다.
<!—- Main Application -->
<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="vertical" creationComplete="init()">
	<mx:Script>
		<![CDATA[
			[Bindable]
			private var model:ISimpleModel;
			
			private function init():void
			{
				model = new SimpleModel();
				model.name = "지돌스타";
				model.age = 30;
			}
		]]>
	</mx:Script>
	<mx:TextInput text="{model.name}"/>
	<mx:TextInput text="{model.age}"/>
</mx:Application>

 

Case 1.

위 코드를 바로 실행하면 아래와 같은 경고 문구가 나올 것이다.

Data binding will not be able to detect assignments to "age".

Data binding will not be able to detect assignments to "name".

Flex 컴파일러는 데이터 바인딩이 될 수 없는 곳에 데이터 바인딩 처럼 사용했다고 해서 경고를 띄울 것이다. 프로그램은 정상적으로 실행되긴 하지만 TextInput에 “지돌스타”와 “30”이 표시가 되어지지 않는다. 즉 SimpeModel의 속성인 name, age자체가 데이터 바인딩 처리가 안되었기 때문에 이들 데이터가 바뀌어도 TextInput에 적용되지 않는 것이다.

 

Case 2.

보니깐 우리는 model로 ISimpleModel 인터페이스를 사용했다. 그러므로 데이터 바인딩은 여기서 처리해야할 듯 싶다. 그래서 ISimpleModel에 선언된 getter에 함수 위에 [Bindable] Metadata tag를 붙여본다.

 

그러나 아래와 같은 에러가 뜬다. 경고도 아니다.

[Bindable] not allowed on global or package-level functions.

Case 3.

조금 어이없어 보인다. 그래서 ISimpleModel에 붙은 [Bindable] Metadata tag를 떼고 SimpleModel.as의 getter 함수에 같은 방법으로 [Bindable]을 붙여준다. 첫번째와 동일하게 아래와 같이 데이터 바인딩할 수 없다고 나온다.

Data binding will not be able to detect assignments to "age".

Data binding will not be able to detect assignments to "name".

Case 4.

결국 인터페이스인 ISimpleModel에서 해결해야한다.

이번에는 SimpleModel에 붙인 [Metadata]는 그대로 두고 ISimpleModel의 클래스 위에 [Metadata]를 붙여본다. 경고문구는 없어지고 프로그램은 정상적으로 “지돌스타”, “30”을 TextInput에 출력해준다. 음 됐다… ㅡㅡ;

 

Case 5.

이번에는 ISimpleModel의 [Bindable]을 그대로 둔채, SimpleModel의 getter위에 붙은 [Bindable]을 떼어보자.  에러,경고 모두 없지만 실행해보면 “지돌스타”, “30”을 TextInput에 출력하지 못한다. 컴파일러가 여기까지 미치지 못하는구나. ^^;;;

 

Case 6.

지금까지 결과를 놓고 보자면 SimpleModel의 클래스 이름 위나 getter 함수에 Bindable을 붙여주고 ISimpleModel에는 Classs 이름 위에만 붙여야 하는 것이다.

희한하다… 왜 인터페이스의 getter 함수 위에는 붙일 수 없는 것일까?

이번에는 ISimpleModel의 클래스 이름위에 [Bindable]은 떼어내고 getter에 [Bindable(event=”propertyChange”)]을 붙여보자.

그리고 컴파일하고 실행해보자!

잘된다… ㅡㅡ;

 

정리

 

정리하자면, 인터페이스 바인딩을 위해서 구현체인 SimpleModel의 클래스 이름 위 또는 getter 함수 위에 [Bindable]을 붙인다. 그리고 그의 인터페이스인 ISimpleModel의 클래스 이름 위에는 [Bindable] 또는 getter 함수위에는 [Bindable(event="propertyChange")]를 붙이면 데이터 바인딩이 잘되는 것이다. ^^;

 

사실 [Bindable(event="propertyChange")]은 [Bindable]과 동일한 표현이다. event="propertyChange"를 생략한 [Bindable]만 있어도 Flex는 propertyChange이벤트에 대응하는 데이터 바인딩이 된다. 하지만 Flex 컴파일러 버그인가? 인터페이스의 getter위에는 [Bindable]이 붙일 수 없다. 버그인지 아닌지 모르겠지만 해결방법은 알았으니 됐다. Flex 4부터는 이 문제를 해결하겠지?

 

좋은 글

땡굴이님의 네이버 오픈캐스트를 보다가 정말 멋진 것을 보게 되었다.

아래 동영상은 웹캠 ActionScript 3.0을 가지고 구현한 것이다. 놀라운 구현력도 있지만 아이디어가 멋지다는 생각이 들었다.


 

이것을 구현하기 위해 아래 3가지 ActionScript 3.0 라이브러리를 이용했다고 한다.

FLARToolKit  : 웹캠을 통해 얻어온 화상의 마커를 인식해주는 라이브러리이다. 이것으로 3차원 공간의 카메라 위치를 계산하도록 한다. 소스코드사용법(Papervision 3D 예)이 공개되어 있다. 아래와 같이 만들어진 판을 들고 화상카메라 앞에서 움직이면 3D 좌표값을 던져주게 되는 것이다.
일본 사람이 만든건데 정말 대단하다. ㅡㅡ;

 

 

Away3D : Flash 상에서 3D를 구현하기 위한 라이브러리이다. Papervision3D 같은 거라 생각하면 되겠다.

WOW-Engine : 3D 물리엔진 라이브러리이다. 이 라이브러리를 이용해 현실감 있게 만들 수 있다.

 

일본 사람들을 보면 인터렉티브한 Flash 컨텐츠를 정말 잘 만들어내는 것 같다. 이러한 연구가 활발해야하는데 난 뭘할 수 있을까? 천문학 관련 ActionScript 3.0 라이브러리 같은 것을 만들어야겠다.

다른 작품도 구경해보자.

 

 

 


웹서핑중에 재미있는 사이트가 있어서 소개한다.

 

사이트 이름은 wonderfl이다. 부제로 build flash online 로 되어 있다.


부제만 보면 알 수 있듯이 사이트는 ActionScript 3.0 코드를 온라인상에 입력하면 이 코드를 서버에서 컴파일해주고 바로 그 결과를 보여주는 사이트이다.

 

이름도 참 독특하다. wonderful을 보방해서 wonderfl로 했다.  뒤에 fl은 Adobe Flash의 Fl인듯 싶다.

 

이런 컨셉과 비슷한 사이트는 몇번 본 것 같은데 ActionScript 3.0을 컴파일해서 서비스하는 것은 처음본다.


서버에서는 Flex의 mxmlc를 이용해 컴파일하는 것으로 보인다.

각종 filterbitmap 처리를 통한 Effect, pixel manipulation, pixel bender 그리고 papervision3D등의 예제가 여러개 올라와 있다.

 

본인도 가입해서 한번 만들어보았다. Papervision3D가 지원되기 때문에 그에 알맞은 코드를 찾아다가 아래 그림과 같이 코딩해보았다. 훌륭하다. ^^ 또한 Flash Player 10 기반으로도 만들 수 있기 때문에 Matrix3D, Vector3D, Vector와 같은 클래스나 연산자를 사용하여 코드를 테스트 해볼 수 있다.

 

샘플코드는 Clockmaker 블로그에서 얻어왔다.

 

아쉬운 점은 블로그에 결과물을 붙힐 수 없다는 것이다(적어도 내 블로그에는…). 하지만 링크를 통해 만든 결과물을 보여줄 수 있다. 아래 링크로 확인하기 바란다.

이 사이트가 어떤 사업성을 가질지는 의문(그냥 개인이 심심해서 만든 것 같음)이지만 매우 재미있는 있고 독특한 사이트라고 느꼈다.

빵집 개발자 양병규님께서 이 글의 이전 글인 Adobe Alchemy 속도 테스트에 대해 이런 답변을 달아주셨다.


컴파일러에 대해서 조금 관심을 가져보시면 알 수 있을텐데요…
음… 뭐랄까…
그 최적화라는 것이 불필요한 일은 안하고 같은 기능이라면 더 빠른 기능으로 대치하고.. 그런일을 하는 것이 컴파일러의 최적화인데요..
제가 보기에는
for( i=0; i < 1000000; i++ )
{
result = sin( 0.332 ) + cos( 0.123 ) + tan(0.333) * log(3);
}
이 부분이.. 최적화를 하면 루프를 1000000번 돌지 않고 한방에 끝낼것같습니다. 최적화알고리즘중에서 루프문안에서의 내용이 반복을 해도 변화가 없는 경우에는 루프를 한방에 끝내도록 하는 알고리즘이 있습니다. 이 예가 그런 예일것 같은데요..

암튼.. ActionScript에는 그런 최적화가 없는데 이미 오랜 역사동안 만들어진 C/C++언어의 최적화 기술이 적용되면 분명히 나은 결과를 보일 것 으로 예상됩니다. ^^;(물론 코딩하는 사람의 실력이 좋을 수록 최적화의 효과가 떨어지겠지만)

그리고..
저는 아무리 봐도 C/C++로 코딩한 것이 결국 몽땅 ActionScript로 만들어지는 것이 맞는 것 같습니다. SWF에는 ActionScript 이 외에 다른 실행코드는 존재하지 않습니다. 최종적으로 만들어진 결과가 *.SWF(*.SWC)이지 않습니까? SWF File Format Specification Version 10 문서 278페이지를 다 훑어봐도 ActionScript 이외에는 실행코드가 없습니다. 머.. DLL을 임포트한다거나 별도의 실행코드용 외부 라이브러리가 있다거나… 그런건 전혀 없습니다.

malloc 함수 같은 경우의 예를 들어보면 ActionScript에는 malloc과 동일한 역할을 하는 어셈블명령은 없습니다. 하지만 ActionInitArray와 같이 긴 변수를 사용할 수 있는 명령으로 ‘구현’은 할 수 있을겁니다. 아마도 그런 방법으로 구현되지 않았나싶습니다.

어쨋든 Alchemy는 C/C++을 100% ActionScript로 변환해 주는게 맞고.. 함수사용, 변수사용, 최적화, 수학함수의 구현, 각종 기초 알고리즘등 기초적인 부분에서 훨씬 우월하기 때문에 좋은 효과를 나타내고 있는 것으로 생각됩니다.

시간이 나면 Alchemy에서 만들어진 SWF 파일을 분석해 보면 아주 좋은 공부가 될것같습니다. ^^;

양병규님의 의견중 최적화에 대한 것을 다시 살펴보자면 -O3 옵션으로 인해 최적화 알고리즘이 적용되어 for문이 없어지고 result = sin( 0.332 ) + cos( 0.123 ) + tan(0.333) * log(3); 만 남는다는 것을 지적하고 ActionScript 3.0에는 이런 최적화 기능이 없기 때문에 실제로 돌려보면 C코드는 1번만 수행하는 반면 ActionScript 3.0에서는 원래대로 1백만번 루프를 돈다는 것이다.

 

또한 여러 상황에 비추어볼때 Alchemy는 C/C++ 코드를 100% ActionScript 3.0으로 변경하는게 맞다고 말씀해 주었다.

 

이 의견을 읽고 맞는 말인 것 같다고 생각해서 코드를 다음과 같이 수정했다.

 

1. C 코드 최적화를 방지해서 다시 속도 테스트

speedtest.c

#include <math.h>
#include <stdlib.h>
#include "AS3.h"

static AS3_Val c_speed_test(void* self, AS3_Val args)
{
    int i;
    double result = 0;
    for( i=0; i < 10000000; i++ )
    {
        result += sin( i ) * cos( i );
    }
    return AS3_Number(result);
}

int main()
{
    AS3_Val method = AS3_Function( NULL, c_speed_test );
    AS3_Val result = AS3_Object( "c_speed_test: AS3ValType", method );
    AS3_Release( method );
    AS3_LibInit( result );
    return 0;
}

 

AlchemySpeedTest.as

package {
    import cmodule.speedtest.CLibInit;

    import flash.display.Sprite;
    import flash.display.StageAlign;
    import flash.display.StageScaleMode;
    import flash.text.TextField;
    import flash.text.TextFieldAutoSize;
    import flash.text.TextFormat;
    import flash.utils.getTimer;

    public class AlchemySpeedTest extends Sprite
    {
        public function AlchemySpeedTest()
        {
            stage.scaleMode = StageScaleMode.NO_SCALE;
            stage.align = StageAlign.TOP_LEFT;

            var loader:CLibInit = new CLibInit();
            var lib:Object = loader.init();
            var startTime:Number;

            startTime = getTimer();
            var cResult:String = "c returned value : " + lib.c_speed_test(null) + " delay time(ms) : " + (getTimer()-startTime);

            startTime = getTimer();
            var asResult:String = "as3 returned value : " + as3_speed_test() + " delay time(ms) : " + (getTimer()-startTime);

            var textField:TextField = new TextField();
            textField.text = cResult + "\n" + asResult;
            textField.width = 500;
            textField.autoSize = TextFieldAutoSize.LEFT;
            textField.wordWrap = true;
            textField.defaultTextFormat = new TextFormat( null, 12 );
            addChild(textField);

        }
        public function as3_speed_test():Number
        {
            var i:int;
            var result:Number = 0;
            for (i = 0; i < 10000000; i++)
            {
                result += Math.sin( i ) + Math.cos( i );
            }
            return result;
        }
    }
}

 

이 코드에서 달라진 것은 gcc로 c를 컴파일 할 때 -O3와 같은 최적화 알고리즘 적용을 하더라도 전과 같이 for문이 삭제되는 것을 방지하도록 했다.

 

이전 코드

for( i=0; i < 1000000; i++ )
{
        result = sin( 0.332 ) + cos( 0.123 ) + tan(0.333) * log(3);
}

 

새로 바꾼 코드

for( i=0; i < 10000000; i++ )
{
        result += sin( i ) * cos( i );
}

새로 바꾼 코드는 sin(), cos()함수에 계속 변화되는 값을 넣어주었고 그 결과가 누적되도록 했다. 이로써 최적화를 하더라도 for문이 적용되지 않는 경우는 없을 것이다.

컴파일은 아래와 같이 한다.

gcc speedtest.c –03 –Wall –swc –o speedtest.swc
mxmlc.exe –library-path+=speedtest.swc –target-player=10.0.0 AlchemySpeedTest.as

결과는 다음과 같다.

c returned value : 0.24755567269650028 delay time(ms) : 3425
as3 returned value : 2.873882594355213 delay time(ms) : 3184

예상대로 gcc –O3 로 인해 최적화가 적용되었던 것이다. 양병규 님의 말에 힘이 실리는 듯하다.

 

2. ActionScript 3.0 Native Vector3D, Matrix3D와 C++의 Vector3D, Matrix3D 속도테스트

 

Flash Player 10부터 추가된 ActionScript 3.0 API중에 Vector3D, Matrix3D가 있다. 이 클래스들은 3D 표현을 위한 다양한 기능을 제공하고 있다. Native 코드이므로 소스코드는 직접 볼 수 없다.

 

ActionScript 3.0의 Vector3D와 Matrix3D 대신 C++로 Vector3D, Matrix3D를 만들어서 서로 수행속도를 비교해보도록 하겠다.

 

아래는 Vector3D와 Matrix3D C++코드, 그리고 이들을 속도 테스트 할 수 있는 C++코드, ActionScript 3.0과 C++과 속도 테스트 할 수 있도록 제작한 ActionScript 3.0 코드가 나열되어 있다.

 

Vector3D.h

#ifndef VECTOR3D_H_
#define VECTOR3D_H_

#include "AS3.h"

class Vector3D {

public :
  Vector3D();
  Vector3D(double x, double y, double z);
  Vector3D(const AS3_Val& as3Vector);
  virtual ~Vector3D();

  double dot(const Vector3D& v) const;
  Vector3D cross(const Vector3D& v) const;
  double modulus() const;
  Vector3D normalise() const;

  void setX(double x);
  void setY(double y);
  void setZ(double z);
  double getX() const;
  double getY() const;
  double getZ() const;

private :
  double _x;
  double _y;
  double _z;

};

#endif /*VECTOR3D_H_*/

 

Matrix3D.h

#ifndef MATRIX3D_H_
#define MATRIX3D_H_

#include "Vector3D.h"

class Matrix3D {

public :
  Matrix3D();
  virtual ~Matrix3D();

  void setRotationX(double degrees);
  void setRotationY(double degrees);
  void setRotationZ(double degrees);

  void setIdentity();

  Vector3D transformVector(const Vector3D& vector) const;

private :
  double _M00;
  double _M01;
  double _M02;
  double _M10;
  double _M11;
  double _M12;
  double _M20;
  double _M21;
  double _M22;

};

#endif /*MATRIX3D_H_*/

 

Vector3D.cpp

#include "Vector3D.h"
#include <cmath>

Vector3D::Vector3D() :
  _x(0),
  _y(0),
  _z(0) {
}

Vector3D::Vector3D(const AS3_Val& as3Vector) {
  AS3_ObjectValue(as3Vector, "x:DoubleType, y:DoubleType, z:DoubleType", &_x, &_y, &_z);
}

Vector3D::Vector3D(double x, double y, double z) :
  _x(x),
  _y(y),
  _z(z) {
}

Vector3D::~Vector3D() {
}

double Vector3D::dot(const Vector3D& v) const {
  return v._x*_x + v._y*_y + v._z*_z;
}

Vector3D Vector3D::cross(const Vector3D& v) const {

  Vector3D result;
  result._x = _y*v._z - _z*v._y;
  result._y = _z*v._x - _x*v._z;
  result._z = _x*v._y - _y*v._x;
  return result;
}

double Vector3D::modulus() const {
  return std::sqrt(_x*_x + _y*_y + _z*_z);
}

Vector3D Vector3D::normalise() const {
  double mod = modulus();
  return Vector3D(_x/mod, _y/mod, _z/mod);
}

void Vector3D::setX(double x) {
  _x = x;
}

void Vector3D::setY(double y) {
  _y = y;
}

void Vector3D::setZ(double z) {
  _z = z;
}

double Vector3D::getX() const {
  return _x;
}

double Vector3D::getY() const {
  return _y;
}

double Vector3D::getZ() const {
  return _z;
}

Matrix3D.cpp

#include "Matrix3D.h"
#include <cmath>

Matrix3D::Matrix3D() :
  _M00(1),
  _M01(0),
  _M02(0),
  _M10(0),
  _M11(1),
  _M12(0),
  _M20(0),
  _M21(0),
  _M22(1) {
}

Matrix3D::~Matrix3D() {
}

void Matrix3D::setIdentity() {
  _M00 = 1;
  _M01 = 0;
  _M02 = 0;
  _M10 = 0;
  _M11 = 1;
  _M12 = 0;
  _M20 = 0;
  _M21 = 0;
  _M22 = 1;
}

void Matrix3D::setRotationX(double degrees) {
  setIdentity();
  double radians = degrees / 180 * M_PI;
  _M11 = cos(radians);
  _M12 = -sin(radians);
  _M21 = sin(radians);
  _M22 = cos(radians);
}

void Matrix3D::setRotationY(double degrees) {
  setIdentity();
  double radians = degrees / 180 * M_PI;
  _M00 = cos(radians);
  _M02 = sin(radians);
  _M20 = -sin(radians);
  _M22 = cos(radians);
}

void Matrix3D::setRotationZ(double degrees) {
  setIdentity();
  double radians = degrees / 180 * M_PI;
  _M00 = cos(radians);
  _M01 = -sin(radians);
  _M10 = sin(radians);
  _M11 = cos(radians);
}

Vector3D Matrix3D::transformVector(const Vector3D& vector) const {
  Vector3D result;
  result.setX(_M00*vector.getX() + _M01*vector.getY() + _M02*vector.getZ());
  result.setY(_M10*vector.getX() + _M11*vector.getY() + _M12*vector.getZ());
  result.setZ(_M20*vector.getX() + _M21*vector.getY() + _M22*vector.getZ());
  return result;
}

alchemy_speed_test.cpp

#include "AS3.h"
#include "Vector3D.h"
#include "Matrix3D.h"

AS3_Val speedTest1(void* self, AS3_Val args) {

  // Declare AS3 variables
  AS3_Val as3Vector1;
  AS3_Val as3Vector2;
  // Extract variables from arguments array
  AS3_ArrayValue(args, "AS3ValType, AS3ValType", &as3Vector1, &as3Vector2);
  // Create native C++ objects with AS3 parameters
  Vector3D vector1(as3Vector1);
  Vector3D vector2(as3Vector2);
  Vector3D vector3;
  // Speed test : calculate cross products and normalise
  for (int i = 0; i < 1000000; i++) {
    vector3 = vector1.cross(vector2);
    vector3 = vector3.normalise();
    vector1 = vector2;
    vector2 = vector3;
  }

  // Obtain a class descriptor for the AS3 Vector3D class
  AS3_Val vector3DClass = AS3_NSGet(AS3_String("flash.geom"), AS3_String("Vector3D"));
  AS3_Val params = AS3_Array("");
  // Construct a new AS3 Vector3D object with empty parameters
  AS3_Val result = AS3_New(vector3DClass, params);
  // Set the x, y and z properties of the AS3 Vector3D object, casting as appropriate
  AS3_Set(result, AS3_String("x"), AS3_Number(vector3.getX()));
  AS3_Set(result, AS3_String("y"), AS3_Number(vector3.getY()));
  AS3_Set(result, AS3_String("z"), AS3_Number(vector3.getZ()));

  // Release what’s no longer needed
  AS3_Release(params);
  AS3_Release(vector3DClass);
  // return the AS3 Vector
  return result;
}

AS3_Val speedTest2(void* self, AS3_Val args) {

  // Declare AS3 variable
  AS3_Val as3Vector;
  // Extract variables from arguments array
  AS3_ArrayValue(args, "AS3ValType", &as3Vector);

  // Create native C++ object with AS3 parameters
  Vector3D vector(as3Vector);
  Vector3D copy = vector;
  Matrix3D rotationX;
  Matrix3D rotationY;
  Matrix3D rotationZ;
  // Speed test : calculate rotation matrices and transform vector
  for (int i = 0; i < 1000; i++) {
    vector = copy;
    for (double ang = 0; ang < 180; ang++) {
      rotationX.setRotationX(ang);
      rotationY.setRotationY(ang);
      rotationZ.setRotationZ(ang);
      vector = rotationX.transformVector(vector);
      vector = rotationY.transformVector(vector);
      vector = rotationZ.transformVector(vector);
    }
  }

  // Obtain a class descriptor for the AS3 Vector3D class
  AS3_Val vector3DClass = AS3_NSGet(AS3_String("flash.geom"), AS3_String("Vector3D"));
  AS3_Val params = AS3_Array("");
  // Construct a new AS3 Vector3D object with empty parameters
  AS3_Val result = AS3_New(vector3DClass, params);
  // Set the x, y and z properties of the AS3 Vector3D object, casting as appropriate
  AS3_Set(result, AS3_String("x"), AS3_Number(vector.getX()));
  AS3_Set(result, AS3_String("y"), AS3_Number(vector.getY()));
  AS3_Set(result, AS3_String("z"), AS3_Number(vector.getZ()));

  // Release what’s no longer needed
  AS3_Release(params);
  AS3_Release(vector3DClass);
  // return the AS3 Vector
  return result;
}

/**
* Main entry point for Alchemy compiler. Declares all functions available
* through the Alchemy bridge.
*/
int main() {
  // Declare all methods exposed to AS3 typed as Function instances
  AS3_Val speedTest1Method = AS3_Function(NULL, speedTest1);
  AS3_Val speedTest2Method = AS3_Function(NULL, speedTest2);

  // Construct an object that contains references to all the functions
  AS3_Val result = AS3_Object("speedTest1:AS3ValType, speedTest2:AS3ValType", speedTest1Method, speedTest2Method);

  // Release what’s no longer needed
  AS3_Release(speedTest1Method);
  AS3_Release(speedTest2Method);

  // Notify the bridge of what has been created — THIS DOES NOT RETURN!
  AS3_LibInit(result);

  // Should never get here!
  return 0;
}

AlchemySpeedTest.as

package {

  import cmodule.alchemy_speed_test.CLibInit;
  import flash.display.Sprite;
  import flash.display.StageAlign;
  import flash.display.StageScaleMode;
  import flash.geom.Matrix3D;
  import flash.geom.Vector3D;
  import flash.text.TextField;
  import flash.text.TextFieldAutoSize;
  import flash.utils.getTimer;

  public class AlchemySpeedTest extends Sprite {

    private var vectorUtils:Object;

    public function AlchemySpeedTest() {

      // Set up the stage
      stage.align = StageAlign.TOP_LEFT;
      stage.scaleMode = StageScaleMode.NO_SCALE;

      // Create the Alchemy bridge to C++ methods
      var loader:CLibInit = new CLibInit;
      vectorUtils = loader.init();

      // Create a text field          
      var timerText1:TextField = new TextField();
      var timerText2:TextField = new TextField();
      var timerText3:TextField = new TextField();
      var timerText4:TextField = new TextField();
      timerText1.autoSize = TextFieldAutoSize.LEFT;
      timerText2.autoSize = TextFieldAutoSize.LEFT;
      timerText3.autoSize = TextFieldAutoSize.LEFT;
      timerText4.autoSize = TextFieldAutoSize.LEFT;
      timerText1.y = 0;
      timerText2.y = 30;
      timerText3.y = 60;
      timerText4.y = 90;
      addChild(timerText1);
      addChild(timerText2);
      addChild(timerText3);
      addChild(timerText4);
      var time0:int;
      var totalTime1:int;
      var totalTime2:int;
      var totalTime3:int;
      var totalTime4:int;

      // Perform the speed test
      time0 = getTimer();
      var vector1:Vector3D = speedTest1();
      totalTime1 = getTimer() - time0;

      time0 = getTimer();
      var vector2:Vector3D = speedTest2();
      totalTime2 = getTimer() - time0;

      time0 = getTimer();
      var vector3:Vector3D = nativeSpeedTest1();
      totalTime3 = getTimer() - time0;

      time0 = getTimer();
      var vector4:Vector3D = nativeSpeedTest2();
      totalTime4 = getTimer() - time0;
      // Display elapsed time and final vector
      timerText1.text = "1. Alchemy Time taken = " + totalTime1 + " vector = (" + vector1.x + ", " + vector1.y + ", " + vector1.z + ")";
      timerText2.text = "1. Native  Time taken = " + totalTime3 + " vector = (" + vector3.x + ", " + vector3.y + ", " + vector3.z + ")";
      timerText3.text = "2. Alchemy Time taken = " + totalTime2 + " vector = (" + vector2.x + ", " + vector2.y + ", " + vector2.z + ")";
      timerText4.text = "2. Native  Time taken = " + totalTime4 + " vector = (" + vector4.x + ", " + vector4.y + ", " + vector4.z + ")";
    }

    /**
     * Speed test using C++ to iteratively calculate the cross products of two vectors
     */
    private function speedTest1():Vector3D {
      var vector1:Vector3D = new Vector3D(0.123, 0.456, 0.789);
      var vector2:Vector3D = new Vector3D(0.987, 0.654, 0.321);

      return vectorUtils.speedTest1(vector1, vector2);
    }

    /**
     * Speed test using C++ to iteratively calculate rotation matrices and apply these to a vector
     */
    private function speedTest2():Vector3D {
      var vector:Vector3D = new Vector3D(0.123, 0.456, 0.789);
      return vectorUtils.speedTest2(vector);    
    }

    /**
     * Speed test using AS3 to iteratively calculate the cross products of two vectors
     */
    private function nativeSpeedTest1():Vector3D {
      var vector1:Vector3D = new Vector3D(0.123, 0.456, 0.789);
      var vector2:Vector3D = new Vector3D(0.987, 0.654, 0.321);
      var vector3:Vector3D;
      var time0:int = getTimer()
      for (var i:int = 0; i < 1000000; i++) {
        vector3 = vector1.crossProduct(vector2);
        vector3.normalize();
        vector1 = vector2;
        vector2 = vector3;
      }
      return vector3;
    }

    /**
     * Speed test using AS3 to iteratively calculate rotation matrices and apply these to a vector
     */
    private function nativeSpeedTest2():Vector3D {
      var vector:Vector3D = new Vector3D(0.123, 0.456, 0.789);

      var copy:Vector3D = vector.clone();

      var rotationX:Matrix3D = new Matrix3D();
      var rotationY:Matrix3D = new Matrix3D();
      var rotationZ:Matrix3D = new Matrix3D();

      for (var i:int = 0; i < 1000; i++) {
        vector = copy.clone();
        for (var ang:Number = 0; ang < 180; ang++) {
          rotationX.identity();
          rotationX.appendRotation(ang, Vector3D.X_AXIS);
          rotationY.identity();
          rotationY.appendRotation(ang, Vector3D.Y_AXIS);
          rotationZ.identity();
          rotationZ.appendRotation(ang, Vector3D.Z_AXIS);
          vector = rotationX.transformVector(vector);
          vector = rotationY.transformVector(vector);
          vector = rotationZ.transformVector(vector);
        }
      }
      return vector;
    }

  }
}

위 코드에서 alchemy_speed_test.cpp에 speedTest1(), speedTest2()가 만들어져 있고 AlchemySpeedTest.as 안에는 C++ 코드를 호출할 수 있는 함수와 ActionScript 3.0 Native Vector3D와 Matrix3D 속도를 테스트해보는 nativeSpeedTest1(), nativeSpeedTest2() 함수가 정의되어 있다.

 

speedTest1()과 nativeSpeedTest1()은 2개의 Vector간에 dot product 연산과 Normalisation을 1백만번 반복해서 결과를 반환하다. 반면 speedTest2()와 nativeSpeedTest2()는 x,y,z축 회전에 대한 Matrix를 3개 만들어 Vector를 회전을 반복한 다음 결과를 반환한다.

 

C++파일들을 컴파일 하기 위해 Makefile을 만들었다.

.SUFFIXES: .cpp .c .o

NAME=alchemy_speed_test
PATH:=${ALCHEMY_HOME}/achacks:${PATH}
CC = gcc
CCPP = g++
FLAGS = -O3 -Wall
OBJS =  $(NAME).o Matrix3D.o Vector3D.o

.cpp.o:
    $(CCPP) $(FLAGS) -c $<
.c.o:
    $(CC) $(FLAGS) -c $<

$(NAME) : $(OBJS)
    $(CC) $(FLAGS) -swc -o $(NAME).swc $(OBJS)

clean:
    rm $(OBJS) $(NAME).swc
    rm swfbridge.log
    rm *achacks*
    rm -r _sb_*

Window사용자중 Cygwin을 사용하시는 분은 make 패키지를 설치해야 이 Makefile을 구동할 수 있다.

 

프롬프트 상에서 make 명령을 하면 Makefile을 구동하여 C++파일을 컴파일 할 수 있다.  

 


 

만약 make를 사용하지 않으면 아래와 같이 컴파일해도 된다.

g++ –O3 –Wall –c alchemy_speed_test.cpp
g++ –O3 –Wall –c Matrix3D.cpp
g++ –O3 –Wall –c Vector3D.cpp
gcc –O3 –Wall –swc –o alchemy_speed_test.swc alchemy_speed_test.o Matrix3D.o Vector3D.o

 

마지막으로 컴파일된 alchemy_speed_test.swc 을 AlchemySpeedTest.as에 추가해서 컴파일 한다.

mxmlc.exe –library-path+=alchemy_speed_test.swc –target-player=10.0.0 AlchemySpeedTest.as

 

생성된 AlchemySpeedTest.swf 를 Flash Player 10에서 구동하면 다음과 같은 결과가 나온다.

1. Alchemy Time taken = 200 vector = (-0.40824829046386324, 0.8164965809277259, -0.4082482904638632)

1. Native  Time taken = 1065 vector = (-0.4082482904638631, 0.816496580927726, -0.4082482904638629)

2. Alchemy Time taken = 290 vector = (-0.6365049502756422, 0.662044570582514, -0.04630804289556738)

2. Native  Time taken = 893 vector = (-0.6365604400634766, 0.6619919538497925, -0.04631434381008148)

 

결국 결과는 다음과 같다.

Vector dot Product 연산과 Normalisation의 경우
- Alchemy : 201 ms
- Native : 1090 ms

회전행렬 생성과 Vector 변환의 경우
- Alchemy : 301 ms
- Native : 908 ms

이전과 다르게 수행속도의 차이를 보이고 있다. 이 결과만 놓고 볼 때 필요할 때는 C/C++로 만들어 최적화 할 수 있도록 만들 수 있겠다는 생각을 가진다. 하지만 많은 차이를 보이지 않아 실제 속도해결에 있어서 필요성에 대해서는 의문을 가질 수 있겠다. 아래서부터 Alchemy를 더욱 이해해 보도록 하겠다.

 

Alchemy를 깊게 이해하자.

 

Adobe labs에서 Alchemy에 대한 소개를 보면 ActionScript 3.0 코드보다 더욱 빠를 것이다라고 언급하고 있다. 하지만 지금까지의 결과를 볼 때 실제로는 기대만큼 효과적으로 빠른 것 같지는 않다. Alchemy를 사용해서 수행속도에 대한 이점을 이끌어내기 위해서는 Alchemy 본질을 어느 정도 이해할 필요가 있다고 생각했다.

 

아래 Alchemy에 대한 내용은 나름대로 자료를 찾아서 안되는 영어로 번역해보고 분석해봐서 정리한 내용이다. 혹시 잘못된 내용이 있을 수 있다. 따끔한 지적도 마다하지 않겠다. ^^

 

Alchemy를 이용해 C/C++ 코드를 SWF로 컴파일 하는 과정은 다음과 같다.

  1. C/C++ 코드를 LLVM(Low Level Virtual Machine) 바이트 코드로 변환한다.(확장자 .bc)
  2. LLVM 코드를 ActionScript 3.0 코드로 변환한다. (확장자 .as)
  3. ActionScript 3.0 코드를 ActionScript Byte Code로 변환한다.(확장자 .abc)
  4. 마지막으로 swc를 만든다. (확장자 .swc)

Alchemy의 중요 요소중 하나는 LLVM(Low Level Virtual Machine)이다. LLVM은 컴파일러 컴포넌트로 여러 언어를 중간코드로 변경하여 언어와 플랫폼에 독립적이고 컴파일, 링크시 런타임등 각 시점에서 최적화를 시켜준다. 자체적인 중간코드(intermediate representation)을 바탕으로 프로그래밍 언어와 CPU에 독립적인 최적화기, GCC기반의 C/C++ 프론트엔드(front-end), 그리고 X86, X86-64, PowerPC, ARM, IA-64, SPARC, Mips, Alpha, c 소스 백엔드(back-end), 또한 메모리에 코드를 생성해 실행할 수 있는 JIT 백엔드 등을 포함하고 있다.

 

Alchemy 설치 디렉토리를 $ALCHEMY_HOME 이라고 한다면 $ALCHEMY_HOME/achacks 내에 gcc가 있다. gcc는 sh코드로 만들어져 있고 이 안에서 C/C++코드를 SWC로 컴파일하기 위한 모든 과정이 들어간다. gcc 파일에서 첫번째 하는 과정은 해당 C/C++코드를 LLVM 바이트 코드(.bc)로 바꾸는 일이다. 이때 사용되는 도구가 $ALCHEMY_HOME/bin/llvm-gcc와 llvm-ld이다. 예를 들어 아래와 같은 코드가 실행된다.

$ llvm-gcc -v -emit-llvm -nostdinc -I$ALCHEMY_HOME/avm2-libc/include -I/usr/local/include –include $ALCHEMY_HOME/avm2-libc/avm2/AVM2Env.h echotest.c -c -o echo.o

$ llvm-ld -o=echotest -O5 -internalize-public-api-list=_start,malloc,free,__adddi3,__anddi3,__ashldi3,__ashrdi3,__cmpdi2,__divdi3, __fixdfdi,__fixsfdi,__fixunsdfdi,__fixunssfdi,__floatdidf,__floatdisf,__floatunsdidf, __iordi3,__lshldi3,__lshrdi3,__moddi3,__muldi3,__negdi2,__one_cmpldi2,__qdivrem, __adddi3,__anddi3,__ashldi3,__ashrdi3,__cmpdi2,__divdi3,__qdivrem,__fixdfdi, __fixsfdi,__fixunsdfdi,__fixunssfdi,__floatdidf,__floatdisf,__floatunsdidf,__iordi3, __lshldi3,__lshrdi3,__moddi3,__muldi3,__negdi2,__one_cmpldi2,__subdi3, __ucmpdi2,__udivdi3,__umoddi3,__xordi3,__subdi3,__ucmpdi2,__udivdi3, __umoddi3,__xordi3,__error $ALCHEMY_HOME/avm2-libc/lib/avm2-libc.l.bc echotest.o

위 과정은 llvm-gcc로 echotest.c를 echotest.o로 만들고 llvm-ld로 echotest.o를 avm2-libc.l.bc와 링크해서 echotest와 echotest.bc를 만들어준다. echotest는 echotest.bc를 실행하는 sh파일이다.

 

참고로 LLVM 바이트 코드인 echotest.bc는 $ALCHEMY_HOME/bin/llvm-dis를 이용해 이 도구는 disassemble하는 역할을 한다.  LLVM 홈페이지에 online demo를 보면 쉽게 이런 과정을 볼 수 있다. online demo에서 LLVM disassemble를 보면 다음 c코드가 어떻게 LLVM 바이트 코드로 바뀌는지 확인해볼 수 있다.

#include <stdio.h>
int main(int argc, char **argv) {
    printf("%d\n", 1);
}

다음은 위 코드를 LLVM 바이트 코드로 바꾼 것은 disassemble 한 것이다.

; ModuleID = ‘/tmp/webcompile/_5616_0.bc’
target datalayout = "e-p:32:32:32-i1:8:8-i8:8:8-i16:16:16-i32:32:32-i64:32:64-f32:32:32-f64:32:64-v64:64:64-v128:128:128-a0:0:64-f80:32:32"
target triple = "i386-pc-linux-gnu"
@.str = internal constant [4 x i8] c"%d\0A\00"        ; <[4 x i8]*> [#uses=1]

define i32 @main(i32 %argc, i8** %argv) nounwind {
entry:
    %0 = tail call i32 (i8*, …)* @printf(i8* noalias getelementptr ([4 x i8]* @.str, i32 0, i32 0), i32 1) nounwind        ; <i32> [#uses=0]
    ret i32 undef
}

declare i32 @printf(i8*, …) nounwind

 

다음으로 $ALCHEMY_HOME/bin에 있는 llc 도구를 이용해 ActionScript 3.0 코드를 만든다.

$ llc -march=avm2 -avm2-use-memuser -o=echotest.as -avm2-package-name=cmodule.echotest echotest.bc

다음 코드가 만들어진 ActionScript 3.0 코드이다.

package cmodule.speedtest {
// Start of file scope inline assembly
import flash.utils.*
import flash.display.*
import flash.text.*
import flash.events.*
import flash.net.*
import flash.system.*

public var gdomainClass:Class;
public var gshell:Boolean = false;

public function establishEnv():void
{
  try
  {
    var ns:Namespace = new Namespace("avmplus");
    gdomainClass = ns::["Domain"];
    gshell = true;
  }
  catch(e:*) {}
  if(!gdomainClass)
  {
    var ns:Namespace = new Namespace("flash.system");
    gdomainClass = ns::["ApplicationDomain"];
  }
}

establishEnv();

……(생략)

public const _c_speed_test:int = regFunc(FSM_c_speed_test.start)

public final class FSM_c_speed_test extends Machine {

    public static function start():void {
            var result:FSM_c_speed_test = new FSM_c_speed_test
        gstate.gworker = result
    }

    public var i0:int

    public static const intRegCount:int = 1
    public var f0:Number, f1:Number, f2:Number, f3:Number

    public static const NumberRegCount:int = 4
    public final override function work():void {
        Alchemy::SetjmpAbuse { freezeCache = 0; }
        __asm(label, lbl("_c_speed_test_entry"))
        __asm(push(state), switchjump(
            "_c_speed_test_errState",
            "_c_speed_test_state0",
            "_c_speed_test_state1"))
    __asm(lbl("_c_speed_test_state0"))
    __asm(lbl("_c_speed_test__XprivateX__BB75_0_F"))
        mstate.esp -= 4; __asm(push(mstate.ebp), push(mstate.esp), op(0×3c))
        mstate.ebp = mstate.esp
        mstate.esp -= 0
        f0 =  (0)
        i0 =  (0)
    __asm(jump, target("_c_speed_test__XprivateX__BB75_1_F"), lbl("_c_speed_test__XprivateX__BB75_1_B"), label, lbl("_c_speed_test__XprivateX__BB75_1_F"));
        f1 = f0
        f2 =  (Number(i0))
        f0 = f2
        //InlineAsmStart
    f0 =  Math.sin(f0);

    //InlineAsmEnd
        f3 = f0
        f0 = f2
        //InlineAsmStart
    f0 =  Math.cos(f0);

    //InlineAsmEnd
        f0 =  (f3 * f0)
        i0 =  (i0 + 1)
        f0 =  (f0 + f1)
        __asm(push(i0==10000000), iftrue, target("_c_speed_test__XprivateX__BB75_3_F"))
    __asm(lbl("_c_speed_test__XprivateX__BB75_2_F"))
        __asm(jump, target("_c_speed_test__XprivateX__BB75_1_B"))
    __asm(lbl("_c_speed_test__XprivateX__BB75_3_F"))
        mstate.esp -= 8
        __asm(push(f0), push(mstate.esp), op(0×3e))
        state = 1
        mstate.esp -= 4;(mstate.funcs[_AS3_Number])()
        return
    __asm(lbl("_c_speed_test_state1"))
        i0 = mstate.eax
        mstate.esp += 8
        mstate.eax = i0
        mstate.esp = mstate.ebp
        mstate.ebp = __xasm<int>(push(mstate.esp), op(0×37)); mstate.esp += 4
        //RETL
        mstate.esp += 4
        mstate.gworker = caller
        return
    __asm(lbl("_c_speed_test_errState"))
        throw("Invalid state in _c_speed_test")
    }
}

……(생략)

 

 

Alchemy로 C 또는 C++ 코드의 컴파일 결과는 기본적으로 ActionScript 와 AVM2 바이트 코드로 만들어진 하나의 클래스가 된다. 이 과정에서 결과물인 SWC는 C와 C++이 C 라이브러리와 POSIX를 지원에 필요할 수 있는 것도 함께 컴파일 되므로 크기가 커진다. 가령 54줄의 C코드를 Alchemy를 통해 컴파일 하면 27415줄의 ActionScript 코드가 만들어진다. 방금 위에서 이러한 현상을 확인할 수 있었다.

 

다음으로 asc.jar를 이용해 만들어진 ActionScript 파일을 swf로 만든다. 이 과정에서 playerglobal.abc와 global.abc를 포함한다.

$ java -Xms16M -Xmx1024M -jar $ALCHEMY_HOME/bin/asc.jar -AS3 -strict -import $ALCHEMY_HOME/flashlibs/global.abc -import $ALCHEMY_HOME/playerglobal.abc -d -config Alchemy::Shell=false -config Alchemy::NoShell=true -config Alchemy::LogLevel=0 -config Alchemy::Vector=true -config Alchemy::NoVector=false -config Alchemy::SetjmpAbuse=false -swf cmodule.echotest.EchoTest,800,600,60 echotest.as

다음으로 만들어진 echotest.swf를 ActionScript 바이트 코드로 만들어주고 라이브러리와

$ GetABC2.pl echotest echotest.swf.abc

$ALCHEMY_HOME/achacks에 있는 swctmpl.swf를 위에서 만든 abc로 대체해 새로운 swf를 만들고 SWF 버전 태그를 10으로 한다.

$ PutABC2.pl $ALCHEMY_HOME/swctmpl.swf temp.swf echotest.swf.abc cmodule/echotest/CLibInit 
$ V10SWF.pl temp.swf library.swf

마지막으로 다음과 같은 catalog.xml과 함께 최종적으로 SWC를 만든다.

<? xml version = "1.0"encoding = "utf-8"?>
<swc xmlns = "http://www.adobe.com/flash/swccatalog/9">
  <versions>
    <swc version = "1.0" />
    <flex version = "2.0"    build = "143452"/>
  </ versions>
  <features>
    <feature-script-deps />
    <feature-files />
  </ features>
  <libraries>
    <library path = "library.swf">
      <script name = "cmodule/echotest/ CLibInit" mod = "1177909560000">
        <def id = "cmodule.hello:CLibInit" /> 
        <dep id = "Date"    type = "e"/> 
        <dep id = "Date"    type = "s"/> 
        <dep id = "flash.utils : Dictionary" type = "e"/> 
        <dep id = "flash.utils : Dictionary" type = "s"/> 
:
(생략)
:
        <dep id = "AS3" type = "n"/> 
        <dep id = "Object" type = "i"/> 
      </ script>
    </ library>
  </ libraries>
  <Files>
  </ files>
</ swc>

$ zip echotest.swc catalog.xml library.swf

 

이처럼 Alchemy를 통해 얻는 결과물은 일반적으로 SWC이다(SWF의 경우 사용가치가 떨어진다.). 이 SWC는 Flex 3(Flash Player 10을 겨냥해서 만들어져야 함), Flex 4(아직 개발중인 “Gumbo”) 그리고 Flash CS4에서 사용할 수 있다.

 

C와 C++로 만들어졌기 때문에 Alchemy로 컴파일 된 SWF가 일반 C/C++의 규칙과 제한을 따른다고 생각하면 곤란하다. C/C++는 일반 SWF와 같이 Flash Player의 보안샌드박스안에 포함되고 모든 화면출력결과물은 display list를 통과하도록 만들어진다. 그리고 네트워크 및 파일엑세스도 이미 존재하는 클래스를 사용하게 된다. Alchemy로부터 컴파일된 라이브러리에서 만들어진 코드는 적절한 메모리 덩어리를 ByteArray로 할당한다. 이것은 Alchemy가 어떻게 포인터, malloc, free등이 ActionScript 에서 어떻게 작업되는가 알려주는 개념이 된다. 이말은 즉 C/C++언어가 Alchemy를 통해 100% ActionScript 3.0으로 변경되어 원천적으로 가지고 있는 SWF의 제약사항을 넘어갈 수는 없다는 것을 의미한다.

 

그럼에도 불구하고 Alchemy의 AVM2 코드가 Flash 와 Flex에서 만들어진 AVM2 코드보다 어떻게 수행속도가 빠를 수 있는지 궁금할 거다. 실제로는 더 빠르지 않다. 그럼 Adobe 에서 주장하는 “수행속도가 빠를 것이다”라는 것은 거짓말인가? 100% ActionScript 3.0으로 바뀌는데 수행속도가 빠를 수 없다고 생각할 수 있다. 하지만 이 부분에서 수행속도의 개선을 찾으면 안된다. 실제로는 Alchemy의 LLVM은 많은 최적화를 이루는 반면 Flash와 Flex의 컴파일러는 최적화 과정이 없다. 또한 Alchemy는 ActionScript 3.0 자체가 가지는 오버헤드의 문제로 인한 수행속도 저하를 극복하는데 쓰일 수 있다.

 

그럼 Alchemy의 수행속도에 대해 기대할 수 있는 부분은 구체적으로 무엇일가? 바로 메모리 엑세스와 함수 호출이다. Alchemy를 통해 컴파일된 코드는 ByteArray를 적용하기 위해 Flash Player 10에 적용된 새로운 ByteArray를 사용한다. 마치 이것은 RAM처럼 이용된다. ActionScript 3.0의 함수를 호출할 때 그들의 인자가 박싱(boxed) 와 언박싱(unboxed)를 하게 된다. 하지만 Alchemy로 만들어진 코드는 그러한 과정이 없다. 이 두가지 이유로 Alchemy로 만들어진 결과물이 수행속도 향상에 도움이 될 수 있다는 것이다.

 

ActionScript 코드와 Alchemy 코드간에 통신과정에서 수행속도가 떨어질 수 있다. 이 과정은 마샬링(marshaling)이 된다. 마샬링은 어떤 한 언어에서 작성된 출력 데이터를 다른 언어의 입력단으로 전달해야하는 경우 데이터 변환과정이 생기는 것을 말하는데 ActionScript 코드와 Alchemy 코드간에 빈번한 상대방의 함수 호출은 이런 과정으로 인해 전체적인 수행속도를 죽이는 일을 발생시킬 수 있다. 그러므로 어떤 처리를 위해 C/C++코드에서 한꺼번에 처리하는 과정이 들어가도록 코딩하고 상대방 함수로 넘겨주는 파라미터의 수를 줄여주는 것이 속도향상에 도움이 될 것이다.

 

Alchemy는 매우 강력하지만 마법사는 아니다. Alchemy의 이점이 무엇인지 잘 파악해서 사용하는 것이 현명하다. 본인이 생각할 때 Alchemy를 사용해야 하는 경우는 다음과 같다.

1. Alchemy로 포퍼먼스 향상에 어느 정도 이득이 있는 경우
2. 기존 C/C++ 라이브러리를 ActionScript 3.0으로 바꾸기 힘들거나 별도의 변경 없이 그래로 Flash/Flex/AIR 프로젝트에서 사용하고 싶은 경우

위 2가지 경우 중 1가지라도 있다고 판단하면 Alchemy를 활용해서 자신의 Adobe RIA 프로젝트에 접목시킬 수 있다고 생각한다. 2번째의 경우 C/C++ 개발자와 Adobe RIA 개발자간에 협업도 기대할 수 있는 부분이다.

 

한가지 더 언급할 것은 Alchemy는 아직 정식 배포상태가 아니라는 점이다. 그래서 약간 개발하기도 불편하고 버그도 간혹 보인다. 하지만 정식 배로가 된다면 그러한 문제점은 해결될 것이라 생각하고 지금까지 보여줬던 것 보다 더욱 큰 포퍼먼스 향상을 기대할 수 있다. 추후에는 Flash Catalyst에 포함되어 개발에 큰 도움이 될 것이다.

 

정리하며

 

Alchemy의 장점은 메모리 엑세스와 함수호출에 따른 수행속도의 개선과 기존 C/C++ 라이브러리를 그대로 활용할 수 있다는 점에 있다. 이 장점을 잘 살려서 현업에 적용할 수 있도록 적절한 예제를 찾아보려한다.

 

이 글을 적으면서도 Alchemy의 컴파일 과정 및 내용에 대해서 확실히 이해할 수 없었다. 하지만 실망하지 않는다. 어쨌던 이러한 노력은 나중에 큰 도움이 될 것이다라고 생각하기 때문이다.

 

이 글을 보신 분 중 공유할 수 있는 내용이나 지적사항이 있다면 언제든지 댓글 환영한다. ^^


한국에도 Alchemy를 활용한 많은 예제와 현업에서 적용 예가 많이 나왔으면 한다.

 

참고내용

 

+ Recent posts