Frame 메타데이터 태그는 Flex 기반에서 만든 Flash Application에 Preloader를 구현하기 위해서 가장 많이 사용합니다. 실제로 Flex를 뜯어보면 [Frame(factoryClass="mx.managers.SystemManager")] 부분이 존재합니다. 이때 이 SystemManager가 바로 Preloader 역할을 합니다. 재미있게도 이렇게 유용한 Frame 메타데이터 태그에 대한 어떠한 공식문서를 저는 본적이 없다는 겁니다. Flex기반이지만 ActionScript 3.0으로만 개발할때 꼭 필요한 요소인데도 말이죠.

각설하고,
지금 말하고자 하는 것은 Flash Builder 4, Flex 4 기반에서 ActionScript 3.0 기반 프로젝트를 만들어 Frame 메타데이터 태그를 사용할때 다음과 같은 에러메시지가 난다는 겁니다.(에러 안날 수 있습니다. 하지만 제 프로젝트에서는 나버리네요.)

1172: mx.skins.spark:BorderSkin 정의를 찾을 수 없습니다.
1202: 정의되지 않은 속성 BorderSkin(mx.skins.spark 패키지)에 액세스했습니다.


이쯤되면 엄청 짜증나죠. 

ActionScript 3.0기반에서는 chart.swc, spark.swc 같은 것은 사용하지 않죠? 여기서 해결 아이디어가 나옵니다. Flash Builder나 Flex Builder에서 프로젝트를 생성하면 .actionScriptProperties 파일이 생성됩니다. 이 안에는 Flex 컴파일러인 mxmlc를 통해 컴파일할때 필요한 설정이 들어가 있지요. 내부를 살펴보면 <excludedEntries>부분이 보입니다. 이 부분은 기본 Flex SDK에 있는 SWC나 SWZ중에 ActionScript 3.0 기반에서 제작시에 필요없는 SWC, SWZ를 포함해서 컴파일하지 않겠다는 것을 명시하는 겁니다. 그런데 Frame 메타데이터 태그를 사용하면 mxmlc컴파일러가 mx.skins.spark:BorderSkin가 들어가는 코드를 만들어주는데 그게 없어서 에러를 던지는 겁니다. 이 클래스는 sparkskin.swc에 포함되어 있는데 이게 <excludeEntries>에 포함되어 있습니다. 그러므로 <excludedEntries>부분을 주석처리를 하거나 삭제하면 sparkskin.swc를 컴파일시 참조할 수 있습니다.  결국 이렇게 하면 깔끔하게 에러가 없어집니다. 명확히 그 이유를 확인하고 싶으시다면 -keep-generated-actionscript 컴파일 옵션을 사용해보세요.

참고로 Flash Builder에서 .actionScriptProperties 파일은 숨겨져 있습니다. 숨김설정을 푸는 방법을 간단히 소개하자면...

1. Package Explorer에서 Filters를 선택합니다.


2. Flex Package Explorer Filters 창이 뜨면 체크되어 있는 모든 항목 풉니다. .actionScriptProperties만 풀어도 됩니다.


3. 이제 자신의 프로젝트에 .actionScriptProperties 파일을 볼 수 있습니다.


추가사항 1
댓글로 hika님께서 Embed 태그문제를 거론해주셨습니다. mxmlc 컴파일러는 Embed 메타데이터 태그를
사용하면 중간에 mx.core.BitmapAsset, mx.core.MovieClipAsset, mx.core.FontAsset과 같은 클래스를 확장한 클래스를 만들어줍니다. 그렇기 때문에 이들 클래스가 있는 swc를 프로젝트 내에 포함해야겠지요. 정확히 알고 싶다면-keep-generated-actionscript 컴파일 옵션을 이용해 bin-debug에 들어간 generated 폴더를 확인해 보시면 됩니다.

Flash 애플리케이션을 만드는데 사용되는 SWC는 playerglobal.swc 뿐입니다. 하지만 mxmlc를 사용해 Embed, Frame 메타데이터와 같은 편리한 기능을 사용하려면 그외에 swc를 포함해줘야합니다. Flash Builder에서 ActionScript 3.0 프로젝트를 만들면 playerglobal.swc, textLayout.swc, flex.swc, utilities.swc를 포함되는 것은 다 이런 이유 때문입니다. 나머지 swc는 모두 <excludedEntries>로 제외해버리죠. 하지만 Frame을 썼을때 위에서 언급한 에러가 발목을 잡으므로 <excludedEntries>를 제거함으로써 문제를 해결할 수 있었다는 것을 언급한겁니다. 그때그때 상황에 맞게 .actionScriptProperties를 가지고 조절하시면 된다는 말씀~ ^^; (간단한건데.. 왜이렇게 설명을 장황하게 했는지 저도 모르겠습니다. ㅎㅎ)

추가사항 2
CSS파일이 없습니다라는 에러도 <excludedEntries>를 제거하면 없어집니다. -defaults-css-url 컴파일 옵션이나 .actionScriptProperties내에 <!-- --> 빈 주석을 달아 없애는 것 외에 또 하나의 팁!

참고글
Flex에서 순수 AS3로 Preloader 구현하기
AS3로 SystemManager 의 Preloading을 흉내내기
ActionScript 3.0에서 Preloader 구현 및 Default css file not found 경고 메시지 없애기
Preloader의 기본 css 파일이 없습니다(Default css file not found) 경고 문제
프로젝트 템플릿 ant
Preloaders in AS3

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

Flex의 SystemManager는 Flex가 구동될 때 Application이 동작하기전 각종 설정을 하면서 사용자들에게 충분히 그 시간을 기다릴 수 있도록 UI적으로 Preloading 화면을 보여준다. 하지만 Flex가 아닌 ActionScript 3.0 프로젝트로 만들면 이런 UI를 보여주지 않는다. Flex는 되는데 ActionScript라고 못할까? 사실 매우 쉬운 방법으로 이 기능을 추가할 수 있다. 방법은 다음 글을 참고하길 바란다.

 

http://www.diebuster.com/?p=681

http://www.bit-101.com/blog/?p=946

 

ActionScript 3.0도 Flex 컴파일러인 mxmlc로 컴파일하는 것이기 때문에 [Frame] 메타 데이타 태그를 이용해 Preloading 기능을 추가할 수 있는 것이 핵심이다. 이를 이용해 만들어진 Preloading 기능 추가한 클래스에 각종 설정 및 자원관리를 할 수 있는 로직을 만들어 사용하면 좋겠다.

 

한가지 팁을 소개하자면....

 

mxmlc가 Flex 컴파일러라서 내부적으로 [Frame] 메타 데이타 태그를 사용하면 기본 CSS를 포함하게 된다. 그래서 위 글대로 하면 다음과 같은 경고 문구가 나온다.

 

Default css file not found

 

이 경고 문구를 없애려면 내용이 아무 것도 없는 null.css를 하나 만들고 컴파일 옵션으로 -defaults-css-url null.css를 추가하면 경고문구가 사라진다.

 

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

 

Adobe Flex가 아닌 Flash CS3, CS4, ActionScript 3.0로 개발해본 사람이면 Stage를 자주 접하게 된다. 하나의 ActionScript 3.0 애플리케이션은 Stage 하나를 가지고 있다. 이것은 모든 DisplayObject 계열의 객체가 addChild()를 통해 시각화 과정이 완료후에 그 객체의 stage속성을 통해 접근이 가능하다. 모든 DisplayObject 계열의 객체에서 stage속성에 접근할 수 있다는 것은 stage가 매우 중요하다는 것을 암시하고 있다. 그러므로 잘 알고 활용해야 삽질을 방지할 수 있지 않을까?



1. Stage.invalidate()와 Event.RENDER 이벤트의 이해 


Stage 클래스에는 invalidate() 메소드가 있다. 이것을 호출하면 다음 렌더링 시점에 Event.RENDER 이벤트를 발생시킨다. 모든 DisplayObject 객체는 stage에 접근이 가능하므로 Event.RENDER 이벤트를 청취할 수 있다. 이는 애플리케이션 퍼포먼스를 향상시키고자 하는 개발자들이 반드시 알고 있어야할 사항이라고 생각한다. Stage의 invalidate()와 Event.RENDER 이벤트를 이용해 쓸데없는 렌더링을 예방함으로써 퍼포먼스를 향상시킬 수 있기 때문이다. 그 이유를 명확하게 알기 위해 다음 예제를 보자.


필자는 일반 ActionScript 3.0 프로젝트를 만들고 Sprite기반에서 애플리케이션을 만들고자 한다. 이때 두개의 클래스를 만드는데 하나는 Stage.invalidate()를 사용한 클래스이고 하나는 사용하지 않는것이다. 이 두개를 비교하면 Stage.invalidate()의 유용성을 확연히 알 수 있다.


InvalidateTest.as

package {
	import flash.display.Sprite;
	import flash.display.StageScaleMode;
	import flash.display.StageAlign;

	/**
	 * Stage.invalidate() 테스트 메인 
	 * @author Yongho, Ji (jidolstar@gmail.com)
	 * @since 2009.6.1
	 */ 
	public class InvalidateTest extends Sprite
	{
		public function InvalidateTest()
		{
			super();
			
			stage.align = StageAlign.TOP_LEFT;
			stage.scaleMode = StageScaleMode.NO_SCALE;
			
			var button1:NotUseInvalidateButton = new NotUseInvalidateButton();
			button1.setWidth( 100 );
			button1.setHeight( 100 );
			button1.backColor = 0xff0000;
			button1.lineColor = 0x00ff00;
			button1.x = 20;
			button1.y = 20;
			addChild( button1 );
			
			var button2:UseInvalidateButton = new UseInvalidateButton();
			button2.setWidth( 100 );
			button2.setHeight( 100 );
			button2.backColor = 0xff00ff;
			button2.lineColor = 0x0000ff;
			button2.x = 130;
			button2.y = 20;
			addChild( button2 );			
		}

	}
}

위처럼 두개의 버튼 클래스를 만들었다. 2개 모두 폭, 높이, 배경색, 선색을 지정하도록 되어 있다. 결과는 비슷하다. 하지만 내부적으로 NotUseInvalidateButton은 내부에 Stage.invalidate()를 사용하지 않았고 UseInvalidateButton은 사용했다. 소스를 보자.


NotUseInvalidateButton.as

package
{
	import flash.display.Sprite;

	/**
	 * Stage.invalidate()를 사용하지 않는 Button
	 * @author Yongho, Ji (jidolstar@gmail.com)
	 * @since 2009.6.1
	 */ 
	public class NotUseInvalidateButton extends Sprite
	{
		private var _height:Number = 50;
		private var _width:Number = 50;
		private var _backColor:Number = 0xffffff;
		private var _lineColor:Number = 0x000000;
		
		public function NotUseInvalidateButton()
		{
			super();
		}
		
		public function get backColor():Number
		{
			return _backColor;
		}
		
		public function set backColor( value:Number ):void
		{
			_backColor = value;
			drawNow();
		}
		
		public function get lineColor():Number
		{
			return _lineColor;
		}
		
		public function set lineColor( value:Number ):void
		{
			_lineColor = value;
			drawNow();
		}
		
		public function getWidth():Number
		{
			return _width;
		}
		
		public function setWidth( value:Number ):void
		{
			_width = value;
			drawNow();
		}
		
		public function getHeight():Number
		{
			return _height;
		}
		
		public function setHeight( value:Number ):void
		{
			_height = value;
			drawNow();
		}
		
		public function drawNow():void
		{
			trace( "[NotUseInvalidateButton] drawNow" );			
			graphics.clear();
			graphics.beginFill( backColor, 1 );
			graphics.lineStyle( 1, lineColor, 1 );
			graphics.drawRect( 0, 0, getWidth(), getHeight() );
			graphics.endFill();
		}
		
	}
}


UseInvalidateButton.as

package
{
	import flash.display.Sprite;
	import flash.events.Event;

	/**
	 * Stage.invalidate()를 사용하는 Button
	 * @author Yongho, Ji (jidolstar@gmail.com)
	 * @since 2009.6.1
	 */ 
	public class UseInvalidateButton extends Sprite
	{
		private var _height:Number = 50;
		private var _width:Number = 50;
		private var _backColor:Number = 0xffffff;
		private var _lineColor:Number = 0x000000;
		
		private var isDrawNow:Boolean = false;
		private var isInvalidated:Boolean = false;
				
		public function UseInvalidateButton()
		{
			super();
			
			this.addEventListener(Event.ADDED_TO_STAGE, onAddedToStage);
		}		
		
		private function onAddedToStage( event:Event ):void
		{
			this.removeEventListener(Event.ADDED_TO_STAGE, onAddedToStage);
			
			if( isInvalidated )
			{
				isInvalidated = false;
				this.stage.addEventListener(Event.RENDER, onRender, false, 0, true );
				this.stage.invalidate();
			}
		}
		
		private function onRender(event:Event):void
		{
			if( this.stage )
			{
				this.stage.removeEventListener( Event.RENDER, onRender, false );
			}
			if( isDrawNow )
			{
				isDrawNow = false;
				drawNow();
			}
		}
		
		private function invalidate():void
		{
			if( this.stage )
			{
				this.stage.addEventListener(Event.RENDER, onRender, false, 0, true );
				this.stage.invalidate();
			}
			else
			{
				isInvalidated = true;	
			}
		}
		
		
		public function get backColor():Number
		{
			return _backColor;
		}
		
		public function set backColor( value:Number ):void
		{
			_backColor = value;
			isDrawNow = true;
			invalidate();
		}
		
		public function get lineColor():Number
		{
			return _lineColor;
		}
		
		public function set lineColor( value:Number ):void
		{
			_lineColor = value;
			isDrawNow = true;
			invalidate();
		}
		
		public function getWidth():Number
		{
			return _width;
		}
		
		public function setWidth( value:Number ):void
		{
			_width = value;
			isDrawNow = true;
			invalidate();
		}
		
		public function getHeight():Number
		{
			return _height;
		}
		
		public function setHeight( value:Number ):void
		{
			_height = value;
			isDrawNow = true;
			invalidate();
		}
		
		public function drawNow():void
		{
			trace( "[UseInvalidateButton] drawNow" );
			graphics.clear();
			graphics.beginFill( backColor, 1 );
			graphics.lineStyle( 1, lineColor, 1 );
			graphics.drawRect( 0, 0, getWidth(), getHeight() );
			graphics.endFill();
		}		
		
	}
}

디버깅 모드로 프로그램을 실행해보면 콘솔창에 다음과 같이 출력된다.


[NotUseInvalidateButton] drawNow

[NotUseInvalidateButton] drawNow

[NotUseInvalidateButton] drawNow

[NotUseInvalidateButton] drawNow

[UseInvalidateButton] drawNow 


NotUseInvalidateButton은 drawNow()메소드가 4번 호출되고 UseInvalidateButton은 1번만 호출된다. 이것만 보더라도 쓸데없는 렌더링을 줄여준 UseInvalidateButton 클래스가 더 잘 설계되었다는 것을 확인할 수 있다. 


NotUseInvalidateButton를 잘 살펴보면 setWidth(), setHeight(), set backColor, set lineColor 를 호출할 때마다 drawNow() 메소드를 호출한다. 개발자라면 이 4가지 속성이 모두 적용된 다음에 drawNow()를 호출하여 한번만 그려주고 싶어질 것이다. 이때 유용하게 사용할 수 있는 것이 Stage.invalidate() 이다.


UseInvalidateButton을 보자 NotUseInvalidateButton보다 약간 복잡해보이지만 잘 따라가보면 어렵지 않게 분석이 가능할 것이다. 결국 4개의 속성이 설정되면 invalidate() 메소드가 호출되고 Event.RENDER 이벤트를 받는 onRender()에서 drawNow()가 호출되도록 한다. 이렇게 되면 4개든 여러개든 할 것 없이 렌더링에 영향을 주는 다중 속성을 설정할 때마다 drawNow()를 호출하여 그림을 그려주는 부담을 줄일 수 있다. 


그림을 그리는 행위는 일반 속성을 설정하는 것보다 더 비싼 비용을 가진다. 그러므로 그림을 그리는 것은 되도록 한번에 처리할 수 있도록 하는 것이 애플리케이션 퍼포먼스 향상의 중요한 요소가 될 수 있는 것이다. 



2. Stage.invalidate()의 버그와 해결방법 


하지만 이렇게 좋은 Stage.invalidate() 가 있음에도 불구하고 실제로 이 메소드를 호출해도Event.RENDER 이벤트가 발생하지 않아 당황스러운 경우가 필자에게 있었다. 그래서 어떤 것은 drawNow()가 호출되고 또 어떤 것은 호출안되는 상황이 발생한 것이다. 너무도 어이가 없지만 Flash Player 10으로 넘어오면서도 이 버그는 아직까지도 Fix가 되지 않은 모양이다. 



위 링크는 모두 Adobe Bugs 관리 시스템에 등록된 것이다.(여러분도 가입해서 투표하길 바란다. 버그 시스템을 잘 이용하면 해결하지 못하는 문제도 쉽게 해결할 수 있을지 모른다. ^^)


Event.RENDER가 발생한 중간에 stage.invalidate()가 호출되면 이게 무시가 되나보다. 그래서 동작상으로는 문제없지만 개발자에게는 버그처럼 느껴질 수 있을지 모르겠다. 어쨌든 필자도 이 문제로 고민을 하다가 한가지 해결방법을 찾았는데 그것은 Event.ENTER_FRAME 이벤트를 이용하는 방법이다. 


Event.ENTER_FRAME을 이용해서 stage.invalidate()의 버그를 말끔히 해결할 수 있다. 다음 코드는NotUseInvalidateButton을 Event.ENTER_FRAME을 이용하는 것으로 바꿔본 것이다.


NotUseInvalidateButton.as



package
{
	import flash.display.Sprite;
	import flash.events.Event;

	/**
	 * Stage.invalidate()를 사용하는 Button.
	 * Event.ENTER_FRAME 로 Stage.invalidate() 버그 우회 
	 * @author Yongho, Ji (jidolstar@gmail.com)
	 * @since 2009.6.1
	 */ 
	public class UseInvalidateButton extends Sprite
	{
		private var _height:Number = 50;
		private var _width:Number = 50;
		private var _backColor:Number = 0xffffff;
		private var _lineColor:Number = 0x000000;
		
		private var isDrawNow:Boolean = false;
		private var isInvalidated:Boolean = false;
				
		public function UseInvalidateButton()
		{
			super();
			
			this.addEventListener(Event.ADDED_TO_STAGE, onAddedToStage);
		}		
		
		private function onAddedToStage( event:Event ):void
		{
			this.removeEventListener(Event.ADDED_TO_STAGE, onAddedToStage);
			
			if( isInvalidated )
			{
				isInvalidated = false;
				this.stage.addEventListener(Event.RENDER, onRender, false, 0, true );
				this.stage.addEventListener(Event.ENTER_FRAME, onRender, false, 0, true );
				this.stage.invalidate();
			}
		}
		
		private function onRender(event:Event):void
		{
			if( this.stage )
			{
				this.stage.removeEventListener( Event.RENDER, onRender, false );
				this.stage.removeEventListener( Event.ENTER_FRAME, onRender, false );
			}
			if( isDrawNow )
			{
				isDrawNow = false;
				drawNow();
			}
		}
		
		private function invalidate():void
		{
			if( this.stage )
			{
				this.stage.addEventListener(Event.RENDER, onRender, false, 0, true );
				this.stage.addEventListener(Event.ENTER_FRAME, onRender, false, 0, true );
				this.stage.invalidate();
			}
			else
			{
				isInvalidated = true;	
			}
		}
		
		
		public function get backColor():Number
		{
			return _backColor;
		}
		
		public function set backColor( value:Number ):void
		{
			_backColor = value;
			isDrawNow = true;
			invalidate();
		}
		
		public function get lineColor():Number
		{
			return _lineColor;
		}
		
		public function set lineColor( value:Number ):void
		{
			_lineColor = value;
			isDrawNow = true;
			invalidate();
		}
		
		public function getWidth():Number
		{
			return _width;
		}
		
		public function setWidth( value:Number ):void
		{
			_width = value;
			isDrawNow = true;
			invalidate();
		}
		
		public function getHeight():Number
		{
			return _height;
		}
		
		public function setHeight( value:Number ):void
		{
			_height = value;
			isDrawNow = true;
			invalidate();
		}
		
		public function drawNow():void
		{
			trace( "[UseInvalidateButton] drawNow" );
			graphics.clear();
			graphics.beginFill( backColor, 1 );
			graphics.lineStyle( 1, lineColor, 1 );
			graphics.drawRect( 0, 0, getWidth(), getHeight() );
			graphics.endFill();
		}		
		
	}
}


위에서 stage.addEventListener( Event.ENTER_FRAME )과 stage.removeEventListener( Event.ENTER_FRAME ) 이 추가된 것을 확인하자. 무작정 이벤트를 청취하고 있으면 안되므로 적절하게 삭제도 하고 있다. 


이렇게 처리함으로써 stage.invalidate()가 제대로 작동안하더라도 drawNow()메소드가 호출이 안되는 경우는 없게 된다.



3. fl.core.UIComponent 수정하기 


Flash의 UIComponent 를 보면 이것 stage.invalidate() 때문에 렌더링이 안되는 경우가 종종 발생한다고 한다. 필자는 Flash는 해본적이 없기 때문에 확실하게 모르지만 이것도 Event.ENTER_FRAME 을 이용해 해결할  수 있다. 그 방법은 다음 링크를 참고한다.


How to Fix the Flash CS3 Components


UIComponent의 callLater()가 어떻게 동작하는 것인지 이것을 보고 감을 잡을 수 있을 것이다. 결국  Event.RENDER, Event.ENTER_FRAME 으로 한다!


4. mx.core.UIComponent의 무효화 메소드들


Flex의 UIComponent에는 invalidateDisplayList(), invalidateProperties(), invalidateSize()와 같은 invalidate() 메소드가 존재한다. 이는 앞선 메소드들의 호출에 대해 각각 updateDisplayList(), commitProperties(), measure() 메소드가 다음 렌더링 시점에 호출되도록 하고 있다. 이는 invalidate/validate 패턴이다. 


재미있게도 Flex에서는 Stage.invalidate()에서 처럼 동작하지 않는 버그는 존재하지 않는다. 어떻게 된 것일까?


사실 Flex 내부를 살펴보면 Stage.invalidate() 와 Event.ENTER_FRAME을 이용해 invalidate 계열의 메소드를 처리하도록 되어 있다. UIComponent 부터, LayoutManager, SystemManager 를 들춰보면 Event.RENDER와 Event.ENTER_FRAME을 가지고 invalidate/validate 패턴을 구현하고 있다. (Flex 개발자도 어쩔 수 없었나보다. ㅡㅡ;;) 


이에 대한 자료는 아래 링크를 읽어보면 되겠다.


Flex internals: Setting a button label



Flex만 접해보고 ActionScript 3.0 학습에 소홀히 했다면 절대 이런 문제를 발견할 수 없었을 것이다. 




정리하며


Stage.invalidate()가 호출할 때 때떄로 Event.RENDER가 호출되지 않는 문제는 버그가 아닐 수 있다. 원래 그렇게 만들어졌을지도 모른다. 하지만 개발자는 이것을 버그로 여긴다. 왜냐하면 보통 개발할때는 그런 것은 생각안하고 메뉴얼만 보고 당연히 그러겠지 생각하기 때문이다. 좀 황당하지만 버그 아닌 버그로 Flex 까지 다 까보게 되었다 ㅡㅡ;


Event.RENDER, Stage.invalidate() 모를때는 setTimeout()과 Timer를 이용해 invalidate/validate 패턴을 구현했었다. 아~ 무식은 용감하다. ㅋ



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를 까보라는 말이다. ^^

관련 사이트

+ Recent posts