서기님의 블로그를 보다가 너무 멋진 그림을 봐서 따왔다.

 

Life Cycle of the Flex UIComponent Base Class

 

그림의 출처 : http://danorlando.com/?p=122

 

Flex의 모든 비주얼 컴포넌트는 Sprite를 확장한 UIComponent를 기반으로 한다. 생성할 대 호출되는 함수와 발생되는 이벤트에 대해서 하나의 그림으로 표현되어 있다. Flex 커스텀 컴포넌트를 작성해야할 때 Life Cycle을 이해하는 것은 반드시 필요하다. 그러므로 눈에다 팍팍 익혀두면 도움이 될 것이다.

 

글쓴이 : 지돌스타(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 패턴을 구현했었다. 아~ 무식은 용감하다. ㅋ



+ Recent posts