본문 바로가기

Blog/Flex

Flex 4 Component (3) – Custom Component Sample

이전 글에서는 Flex4에서 달라졌거나 새로 추가된 여러 기능을 알아 보았습니다. 이번 글에서는 추가된 여러 기능들 중에서 skin과 component에 대하여 Helo Component를 Spark Component로 리펙토링하는 예제을 통해 일단 경험해 본 뒤, (다음 글에서) lifecycle에서 추가된 주기나 메서드에 대해서 구체적으로 알아보도록 하겠습니다. 이 글은 Frank Sommers의 Creating a Custom Look and Feel for Flex 4 Components 에 대한 내용입니다. 이 글을 읽기 전에 MVC 패턴에 대한 기초 지식이 있으면 좋습니다.

flex4 Spark architecture에서 중요한 점은 컴포넌트 view와 display-logic의 완전히 분리시킨다는 점입니다. 이 글은 이런 Spark architecture의 특징을 살려 skin 을 이용한 custom 컴포넌트를 제작하는 방법을 소개하는 것입니다. 이전 Flex버전의 컴포넌트는 외부 stylesheet파일을 통해 이런 logic (controller)와 view를 분리하는 방법을 제시하긴 했지만, style이나 data에 따라 어떻게 작동해야 하는지에 대한 logic, 컴포넌트의 layout과 child(subcomponent) 등을 하나의 MXML파일이나 ActionScript 파일에 모두 기술해야 했습니다. Flex 4 SDK의 핵심은 방금 말했던 component가 가지는 모양(visual)을 그것을 나타내는 방식(display-logic) 으로부터 완전히 분리시키는 것입니다. 그렇다고 해서 Flex4 application에서 사용되는 모든 컴포넌트가 이런 새로운 architecture에 따라 제작되어야 할 필요는 없습니다. Spark 컴포넌트는 이전 버전의 컴포넌트(Halo)의 구조를 기반으로 설계되었기 때문에 같은 Flex application내에서 얼마든지 Halo와 Spark 컴포넌트를 섞어 쓸 수 있습니다.

이 글에서 사용할 예제는 화씨와 섭씨온도를 서로 변환해주는 간단한 컴포넌트를 제작하는 것입니다. 이 예제는 Artima article : Two-Way Data Binding in Flex 4 (양방향 데이터 바인딩을 표현하는 방법에 대해 flex4에서 "@"로 지원된다는 점을 설명한 글)에 소개된 것입니다. 잠깐 살펴보자면…

AS3에서 구현

        private var fahrenheitDegree: Number;
        
private var celsiusDegree: Number;
        
        
public function set fahrenheit(n: Number): void {
            
fahrenheitDegree = n;
            
celsiusDegree = (fahrenheitDegree - 32) * 5/9;
            
dispatchEvent(new Event("celsiusChanged", true, true));
        }
        
        [
Bindable(event="fahrenheitChanged")]
        
public function get fahrenheit(): Number {
            
return fahrenheitDegree;
        }
        
        
public function set celsius(n: Number): void {
            
celsiusDegree = n;
            
fahrenheitDegree = celsiusDegree * 9/5 + 32;
            
dispatchEvent(new Event("fahrenheitChanged", true, true));
        }
        
        [
Bindable(event="celsiusChanged")]
        
public function get celsius(): Number {
            
return celsiusDegree;
        }

Flex3에서 구현

    <TextInput id="fahrenheit" text="{celsius.text}"/>
    <TextInput id="celsius" text="{fahrenheit.text}">

Flex4 에서 구현

    <TextInput id="fahrenheit" text="@{celsius.text}"/>
    <TextInput id="celsius">

본론으로 돌아와서 예제(온도 변환기)에 대한 결과물을 한번 보겠습니다. 각 inpit 박스에 숫자를 넣고 엔터를 치면 각각 변환된 값이 출력되고, 토글 버튼으로 활성화 상태를 조정할 수 있도록 되어 있습니다.

Halo Component

Halo component 방식(architecture)에서 이 컴포넌트는 다음과 같이 제작 될 수 있습니다. VBox를 상속받아 두개의 label과 두개의 textBox를 만들고 이벤트 처리를 해두었습니다. 일단 토글 버튼은 생략했습니다.

<?xml version="1.0" encoding="utf-8"?>
<mx:VBox xmlns:mx="http://www.adobe.com/2006/mxml">
    <mx:Script>
        <![CDATA[
            private function onCelsiusEntered(e: Event): void {
                
fahrenheitInput.text = (Number(celsiusInput.text) * 9/5 + 32) + "";
            }
            
private function onFahrenheitEntered(e: Event): void {
                
celsiusInput.text = ((Number(fahrenheitInput.text) - 32) * 5/9) + "";
            }
        ]]>
    </mx:Script>
    <mx:Form>
        <mx:FormItem label="Celsius:">
            <mx:TextInput id="celsiusInput" change="onCelsiusEntered(event)"/>
        </mx:FormItem>
        <mx:FormItem label="Fahrenheit:">
            <mx:TextInput id="fahrenheitInput" change="onFahrenheitEntered(event)"/>
        </mx:FormItem>
    </mx:Form>
</mx:VBox>

이 컴포넌트는 Flex3 Application에서 다음과 같이 사용됩니다.

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

코드를 살펴보면 display-logic (controller) 은 ActionScript code 로 짜져 있고, 두 개의 이벤트 핸들러가 각 input박스의 값을 서로 업데이트 해줍니다. 이 컴포넌트에서 주목할 것은 display-logic과 view, layout등이 컴포넌트와 강하게 결합되어 있다는 것입니다. 컴포넌트에 관련된 모든 정의를 한곳에서 제공할 수 있다는 편리함은 있겠지만, 이런 방식은 상대적으로 유연성에 취약하게 됩니다. 이런 유연성에 대한 취약점은 컴포넌트가 보다 더 큰 application에 포함되어 사용될 경우 문제가 될 수도 있습니다.

예를 들어 application에서 온도 변환기가 사용자 입력을 받지 못하도록 비활성화 상태로 바꾸는 기능이 필요하다면, 다음 예제와 같이 enable 속성을 변경하는 것으로 이 기능을 구현할 수 있을 것입니다.

<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute" xmlns:local="*" backgroundColor="0x00DDFF">
    <mx:VBox>
        <local:Converter id="converter"/>
        <mx:Button label="Toggle" click="converter.enabled = !converter.enabled"/>
    </mx:VBox>
</mx:Application>

매우 간단하긴 하지만, 이렇게 컴포넌트 전체에 대하여 disable상태로 세팅하는 것은 비활성화 표시를 위해 background color 까지 변경되는 예기치 못한 결과를 가져옵니다. 따라서 텍스트 박스에만 disable이 적용되도록 보다 세심한 핸들링이 필요합니다. 물론 어떻게든 이를 구현할 수도 있지만 어떤 특별한 상황을 때문에 컴포넌트에 특정 코드를 집어넣는 방법은 적절치 않아 보입니다.

이렇게 visual 과 display-logic간의 강한 결합에 의해 발생되는 변화에 대해 유연성이 취약한 문제를 Spark에서는 파일(컴포넌트)을 두 개로 쪼개는 방법으로 해결합니다. 그리고 이 쪼개진 두 조각은 Spark architecture내에서 일정한 규칙에 의해 서로 상호작용하며 작동하게 됩니다.

Spark Component

위에서 Halo 방식으로 제작된 온도 변환기를 Spark architecture를 기반으로 리토링 해보도록 하겠습니다.

Controller and View Separation
Flex component의 어떤 부분이 controller에 해당하는지, 어떤 부분을 view부분으로 떼어내야 하는지를 항상 명백하게 구분할 수 있는 것은 아니지만, 일반적으로 visual 변화에 무관하게 (코드)재사용 가능한 어떤 행위를 정의하고 싶다면 controller에, 그렇지 않다면 view로 구분할 수 있습니다. Spark component에서 view는 SparkSkin클래스(Spark skin의 최상위 클래스)를 확장한 클래스에 정의합니다. Skin 파일은 display와 관련된 graphic Element, subcomponent, layout, image, transition등을 포함합니다. 대부분의 Flex 컴포넌트는 여러 개의 subcomponent로 구성되는데, 각 subcomponent는 자신의 skin을 독립적으로 정의할 수도 있습니다. 예제를 통해 살펴 보겠습니다.

Skin 파일 (ConverterSkin.mxml)

<?xml version="1.0" encoding="utf-8"?>
<s:SparkSkin
    xmlns:fx="
http://ns.adobe.com/mxml/2009"
    xmlns:s="
library://ns.adobe.com/flex/spark"
    xmlns:mx="
library://ns.adobe.com/flex/halo"
    width="
400" height="300">

    <fx:Metadata>
        [HostComponent("DegreeConverter")]
    
</fx:Metadata>

    <s:states>
        <s:State name="normal"/>
        <s:State name="disabled"/>
    </s:states>

    <mx:Form>
        <mx:FormItem label="Celsius:" alpha.disabled="0.5">
            <s:TextInput id="celsiusInput" enabled.disabled="false"/>
        </mx:FormItem>
        <mx:FormItem label="Fahrenheit:" alpha.disabled="0.5">
            <s:TextInput id="fahrenheitInput" enabled.disabled="false"/>
        </mx:FormItem>
    </mx:Form>
</s:SparkSkin>

skin파일은SparkSkin클래스을 확장하여 만드는데, 먼저 host component를 Metadata태그로 지정합니다. 내부적으로 Spark skin은 hostComponent속성으로 skin을 사용하는 컴포넌트 인스턴스에 대한 참조를 얻게 됩니다. 온도 변환기는 두 가지 enable, disable상태가 필요하므로 두 개의 skin state를 선언합니다. Spark 컴포넌트에서는 skin과 component 양쪽에 각자의 state를 유지하고 있는데, 컴포넌트의 state가 변하면 컴포넌트는 이를 자신과 연관된 skin에 알려줌으로써 state 변화를 공유하게 됩니다. 이렇게 선언된 state은 다음과 같이 name 속성값으로 skin내부 어느 곳에서나 접근할 수 있습니다.

property.state이름="0.5"

Skin Parts and State
컴포넌트 스킨을 만들었으니 이것을 컴포넌트에 적용해 보겠습니다. Skinnable Spark 컴포넌트는 SkinnableComponent 클래스를 확장하여 만듭니다. 다음에 보여주는 코드는 display와는 직접적으로 관련이 없는 코드로 구성되었습니다.

Component 파일 (DegreeConverter.as)

package com.artima {
    
import flash.events.Event
;
    
import spark.components.TextInput
;
    
import spark.components.supportClasses.SkinnableComponent
;
    
    [
SkinState("normal"
)]
    [
SkinState("disabled"
)]
    
    
public class DegreeConverter extends SkinnableComponent
{
        
        [
SkinPart(required="true"
)]
        
public var celsiusInput: TextInput
;
        
        [
SkinPart(required="true"
)]
        
public var fahrenheitInput: TextInput
;
        
        
override public function set enabled(value:Boolean) : void
{
            
if (enabled != value
)
                
invalidateSkinState
();
            
            
super.enabled = value
;
        }
        
        
override protected function getCurrentSkinState() : String
{
            
if (!enabled
)
                
return "disabled"
;
            
return "normal"
;
        }
        
        
override protected function partAdded(partName: String, instance: Object): void
{
            
if (instance == celsiusInput
)
                
celsiusInput.addEventListener(Event.CHANGE, onCelsiusInput
);
            
            
if (instance == fahrenheitInput
)
                
fahrenheitInput.addEventListener(Event.CHANGE, onFahrenheitInput
);
        }
        
        
override protected function partRemoved(partName:String, instance:Object) : void
{
            
if (instance == celsiusInput
)
                
celsiusInput.removeEventListener(Event.CHANGE, onCelsiusInput
);
            
            
if (instance == fahrenheitInput
)
                
fahrenheitInput.removeEventListener(Event.CHANGE, onFahrenheitInput
);
        }
        
        
private function onCelsiusInput(e: Event): void
{
            
fahrenheitInput.text = (Number(celsiusInput.text) * 9/5 + 32) + ""
;
        }
        
        
private function onFahrenheitInput(e: Event): void
{
            
celsiusInput.text = ((Number(fahrenheitInput.text) - 32) * 5/9) + ""
;
        }
    }
}

이 코드는 skin part와 컴포넌트 state에 대한 동작을 중심이 되는 코드입니다. SkinPart metadata는 컴포넌트를 디자인함에 있어 module방식의 접근이 가능토록 해줍니다. 온도변환기는 두 개의 textInput을 skin part로 정의할 때 skin에 설정된 인스턴스의 id값을 컴포넌트의 변수명으로 선언하여 skin과 컴포넌트에서 참조를 수월하게 할 수 있도록 하고 있습니다. 이렇게 함으로써 runtime에 컴포넌트 내부에 subComponent를 가진 스킨 요소(skin element)가 자동으로 연결되게 되는데 이런 역할을 해주는 것이 바로 SkinPart metadata입니다. 예제에서 fahrenheitInput과 celsiusInput변수명은 skin에서 textInput 인스턴스의 id값이고, 각각 해당 id의 인스턴스를 참조하게 됩니다. 또한 스킨요소를 연결 시켜줄때 Spark 컴포넌트의 lifecycle에서 중요한 과정이 추가되었습니다. 바로 컴포넌트에서 skin part가 추가, 제거될 때 partAdded()와 partRemoved() 메서드가 런타임에 호출된다는 것인데, 이 메서드들은 주로 override하여 구현할 때 새로 추가된 subcomponent에 대한 이벤트 리스너를 추가하거나 제거하는 코드가 삽입되는 곳입니다.

온도 변환기 예제에서는 disabled와 normal 두 개의 state가 필요합니다. 이 state는 skin과 컴포넌트 양쪽에 선언되었습니다. 런타임에 state가 변경될 때 Flex 컴파일러가 해당 state에서 필요한 skin을 참조하도록 하는 작업이 필요 한데 getCurrentState() 메서드를 통해 컴포넌트의 현재 state를 skin에 알려줄 수 있습니다. 예제에서 enable속성의 setter메서드를 override 하면서 invalidateSkinState() 메서드를 호출함으로써 현재 state값을 변경하는 루틴을 호출합니다. 스킨에서 현재 컴포넌트의 state가 변경되면 해당 state로 기술된 모든 속성값이 새로 설정됩니다.

Skin과 component 두 개의 파일로 온도 변환기를 만들면서 Spark component를 제작하는 방식을 알아 보았습니다. Flex application 에서 이 컴포넌트를 사용하는 가장 간단한 방법은 MXML에서 컴포넌트와 함께 관련 skin 을 기술하는 것입니다.

<?xml version="1.0" encoding="utf-8"?>
<s:Application xmlns:fx="http://ns.adobe.com/mxml/2009"
    xmlns:s="
library://ns.adobe.com/flex/spark"
    xmlns:mx="
library://ns.adobe.com/flex/halo"
    minWidth="
1024" minHeight="768"
    xmlns:artima="
com.artima.*">
    <fx:Script>
        <![CDATA[
            
import com.artima.ConverterSkin;
        ]]>
    
</fx:Script>
    
    
<s:Group>
        <s:layout>
            <s:VerticalLayout/>
        </s:layout>
        
        
<artima:DegreeConverter skinClass="com.artima.ConverterSkin" id="converter"/>
        <s:Button label="Toggle" click="converter.enabled = !converter.enabled"/>
    </s:Group>
</s:Application>

 ActionScript 와 CSS에서 컴포넌트의 skinClass 속성을 설정할 수도 있습니다. 이렇게 컴포넌트에서 skin을 분리함으로써 런타임에 skin을 변경할 수도 있고 Application이 시작될 때 선택하게 할 수도 있습니다. 다음 글에서는 Flex4 컴포넌트에서 변경되거나 추가된 Lifecycle에 대해 구체적으로 알아보겠습니다.

참고

이 글은 Frank Sommers의 Creating a Custom Look and Feel for Flex 4 Components 내용을 참고로 작성되었습니다.