Creative Wrong Answer


모든 이벤트는 typetarget 속성을 가지고 있다. 이것은 어떤 이벤트가 발생했는지 어떤 객체가 이벤트를 발생시켰는지에 대한 정보이기 때문에 꼭 필요하다.
EventDispatcher 가 이벤트를 Broadcase(전파) 하게 되면 Listener가 발생한 이벤트를 받게 된다.

이벤트는 두가지 형식으로 나눠진다.
스테이지에 보이는 객체가 발생시키는 이벤트와 보이지 않는 객체가 발생시키는 이벤트이다.

스테이지에 보이는 객체가 발생시키는 이벤트는 이벤트 흐름(event flow)에 따라서 이동하게 된다.
여기서 나오는게 버블링이다.
보이지 않는 객체가 발생시키는 이벤트는 이벤트 흐름을 타지 않고 해당 객체에 직접 등록된 listener 에서만 캐치 할수 있다.

Loader 같은 것이 이런 경우이다.

private function loadImage():void
{
	var loader:Loader = new Loader();
	loader.load( new URLRequest("http://localhost:8080/logo.gif") );
	loader.addEventListener(Event.COMPLETE, loadComplete);
}
위 소스에서 처럼 loader에 직접 listener를 붙이는 경우에만 loader 객체가 발생시키는 이벤트를 받을 수 있다.

보이는 객체가 발생시키는 이벤트는 이벤트 흐름을 탄다고 했는데. 이제 본격적으로 알아보자.

1. 어떤 이벤트가 발생한다. (마우스 클릭, 키보드 누르기, 등등)
2. 어플리케이션은 어떤객체가 이벤트를 발생시켰는지 찾기 위해서 하위 자식들을 검색해간다. (캡쳐)
3. 어플리케이션이 이벤트를 발생시킨 객체를 찾았다 (타겟)
4. 이벤트의 정보를 가지고 다시 어플리케이션으로 돌아온다. (버블)

대부분의 이벤트는 이 버블링 단계에서 listen 해서 사용하게 된다.
거품이 물 안에서 위로 올라오는 것처럼 이벤트를 발생시킨 객체에서 어플리케이션으로 올라오기 때문에 버블링이라고 하는게 아닐까 싶다.
거품이 위로 올라올때 중간에서 가로챌수 있는것처럼. 이벤트가 발생하게 되면 발생시킨 객체의 상위 parent 에 listener가 등록되어있다면 그 이벤트를 받아서 처리 할 수 있다.

아래의 화면은 이벤트가 발생하는 것을 보여준다.
빨간색과 파란색에는 각각 MouseEvent.CLICK 리스너가 붙어있다.





파란색 캔버스를 클릭하게 되면 파란색 캔버스가 클릭되었다는 메시지가 나온다.
빨간색 캔버스를 클릭하게 되면 빨간색 캔버스가 클릭되었다는 메시지가 나온 이후에 파란색이 클릭되었다는 메시지가 나오게 된다.

이벤트 흐름

빨간색 캔버스가 MouseEvent.CLICK 이벤트를 디스패치 한다.
캡쳐 단계 - 어플리케이션은 빨간색 캔버스를 찾아 들어간다 ( Application - blueCanvas - redCanvas )
타겟 단계 - 빨간색 캔버스를 찾았고 빨간색의 리스너에게 클릭되었다는것을 알려준다 (redCanvas Click)
버블 단계 - 이벤트를 가지고 어플리케이션으로 올라온다. (redCanvas - blueCanvas - Application)
                이때 blueCanvas 도 Click 이벤트 리스너가 붙어있기 때문에 리스너에 등록된 함수가 실행된다.
                (blueCanvas Click)

빨간색 캔버스를 클릭한 후에 나오는 메시지의 Phase 를 보면 Red Click - phase : 2 라고 되어있다.
phase가 2 이기 때문에 타겟 단계에서 이벤트를 받았다는 이야기이다. 이때 event.target 으로 빨간색 캔버스가 참조된다.
Blue Click - phase : 3 에서 phase가 3 이므로 버블 단계임을 알수 있다. event.target 은 여전히 빨간색 캔버스이다.

여기서 target 과 currentTarget 의 관계도 알 수 있다.

event.target 은 이벤트를 발생시킨 객체이다. 빨간 캔버스를 클릭한 후 캐치된 두개의 이벤트 모두 target 은 빨간 캔버스이다.
event.currentTarget 은 리스너가 붙어있는 객체이다.
RedClick 이벤트의 currentTarget 은 빨간색 캔버스 이지만 BlueClick 이벤트의 currentTarget 은 파란색 캔버스가 되는 것이다.

이 관계를 잘 고려해서 이벤트 모델을 구성해야 자잘한 에러를 막을 수 있다.

예를 들면 canvas 를 base로 커스텀 컴포넌트를 만들었는데 안에 라벨이 들어있는경우 event.target 으로 캔버스를 받고 싶었지만 라벨쪽이 클릭된 경우 target 으로 label 이 넘어와서 오류가 난다거나 하는 경우가 생기게 된다.
target 과 currentTarget 을 구분해서 사용하는 것은 쉬운듯 하면서도 빠뜨리기 쉬운 부분이다.

위의 예제에서 useCapture 를 체크 한 상태로 캔버스를 클릭 해보자.

useCapture Flag 는 capture 단계에서 이벤트를 사용할것인가에 관한 것이다.

파란색 캔버스를 클릭하게 되면 캡쳐 단계에서만 이벤트를 받게 되므로.. 아무런 이벤트도 발생하지 않는다. 파란색 캔버스가 클릭되었다는 메시지가 나와야 할것 같지만. 파란색 캔버스의 경우는 target 단계이므로 아무런 이벤트도 받을 수 없는 것이다.

빨간색 캔버스를 클릭하게 되면 빨간색을 찾아가는 동안에 만나게 되는 파란색캔버스만 이벤트가 발생하게 된다.

이렇게 useCapture Flag 를 true 로 설정하게 되면 타겟과 버블링 단계가 무시되고 오직 캡쳐 단계에서만 이벤트를 받게 된다.
자주 쓰이는 속성은 아니지만 꼭 필요하게 되는 경우가 생기기도 하기 때문에 알아두는 것이 좋다.

늦은 감이 있지만 이쯤에서 addEventListener 를 한번 보고 가야 할것 같다.


addEventListener(type:String, listener:Function, useCapture:Boolean = false, priority:int = 0, useWeakReference:Boolean = false):void
리스너에서 이벤트 알림을 받을 수 있도록 EventDispatcher 객체에 이벤트 리스너 객체를 등록합니다.

네번째 파라미터인 priority 는 우선순위에 관한 것이다 숫자가 높을수록 먼저 처리되고 숫자가 같은 경우 먼저 추가된 리스너 부터 처리 된다.
하지만 useCapture 보다 더 쓰이지 않는다.

마지막의 useWeakReference 가 있는데 이것은 이전 글 [2010/02/05 - [Flex/Event] - Flex Event 기본 설명 ] 에서 removeEventListener 를 명식적으로 사용하기 힘든 경우에 체크 한다고 이야기 했었다.

한번더 강조 하지만 addEventListener 는 가비지 컬렉터가 메모리 해제를 하지 못하는 가장 많은 경우이다.
꼭 removeEventListener 해주는 습관을 기르자.


위의 예제에서는 빨간색캔버스가 클릭되어도 파란색캔버스에 붙어있는 listener Function이 실행되게 되는데 파란색캔버스가 직접 클릭되었을때만 실행되도록 하기 위해서는 어떻게 해야 할까.

이럴때 사용하는것이 로그에 같이 찍히던 phase 이다.

파란색 캔버스의 리스너함수에 event.eventPhase가 2 일때만 실행되도록 함수를 구성하게 되면 파란색 캔버스가 직접 클릭되었을때만 함수의 내용을 실행하도록 만들수 있다.
예제 아래에 적어져 있듯이 2 이면 타겟 이기 때문에 파란색 캔버스가 직접 클릭되었다는것을 알수 있게 된다.
event.currentTarget 값이 buleCanvas 일때의 조건을 사용해도 결과는 동일하다

또는 빨간색캔버스의 리스너에서 e.stopImmediatePropagation() 이나 e.stopPropagation() 으로 끊어줘도 동일하게 결과가 나타난다.

하지만 지금의 예제는 이벤트를 받는곳이 두군데 이기 때문에 같은 결과가 나오는 것이지. 많약 어플리케이션에서도 이벤트를 리슨 하고 있다면. 다른 결과가 나오게 된다.

만약 어플리케이션에서 마우스 이벤트의 리스너가 대기 중이라면 파란캔버스에 phase 로 조건을 줘서 걸러냈을경우 어플리케이션에서는 이벤트를 받을 수 있다.
하지만 빨간색 캔버스에서 이벤트 흐름을 끊었을 경우 어플리케이션에서도 이벤트를 받을 수가 없다.
이 부분은 다음 포스트에서 확인 해보자.

지금까지 이벤트 흐름에 관해서 정리해 보았다.
Flex 에서 이벤트 흐름은 아무리 강조해도 지나치지 않는다. 많은 자료들을 보고 정리 하는 습관을 길러야 한다.
저작자 표시 비영리 동일 조건 변경 허락
신고

Comment 1


이 글은 자꾸 까먹어서 정리겸 이해하고 있는 수준에 맞게 풀어서 쓴 글이다..
-------------------------

Flash나 Flex의 메모리는 개발자가 그냥 지울수가 없게 되어있다.
자바도 마찬가지고 VM기반의 언어에서는 메모리를 할당하고 해제 하는 과정이 시스템에서 알아서 하도록 되어있기 때문에.. 언제 메모리가 해제 되는지 개발자가 컨트롤 할수가 없다.

개발을 한 프로그램을 돌리기 시작했는데.. 메모리가 사용할수록 증가 한다면 그 프로그램은 결국에 가서는 시스템에 문제를 일으키고 종료될 것이다.

메모리 관리는 Garbage Collector (이하 GC) 라는 놈이 하게 되는데 말그대로 쓰레기를 수거하는 역할이다.
더이상 프로그램에서 사용하지 않는 객체들을 초기화하고 메모리를 시스템으로 반환해주게 된다.

플레시 플레이어는 객체가 생성되면 시스템에 메모리를 요청하게 되는데 이때 이후에 생길 객체들을 위해서 객체의 메모리보다 더 많은 양을 할당 받게 된다.
이 메모리가 부족해지면 다시 시스템에 더 많은 메모리를 요구 하게 되는데 이때 GC 가 일을 하게 된다. 쓸모 없는게 있는지 찾고 있으면 지우고 부족분에 대한 메모리를 요청하게 되는것이다.

개발자가 removeChild 나 null 을 선언할때 메모리가 해제되지 않는다는것이 중요하다.

GC 가 돌면서 사용하지 않는 것을 찾는 방법은 두가지 룰이 있다.

1. 레퍼런스 카운팅 (Reference Counting)
2. 마크 앤 스윕 (Mark and Sweep)


레퍼런스 카운팅은 해당 객체가 참조하고 있거나 해당 객체를 참조하고 있는 놈이 있는지 찾는다. 해당 객체를 참조하고 있는 애들을 찾아서 있으면 카운트를 1 올리는 방식이다.

전부 찾아보고 나서 카운트가 0 인 객체들을 메모리에서 해제한다.

함수에서 Label 을 만든다

var label:Label = new Label();


new 키워드로 인스턴스를 생성했으니 메모리 공간을 할당 받는다. 하지만 참조하고 있는 곳이 없다. 따라서 GC 가 실행되면 label 은 메모리에서 사라진다.

라벨을 만들고 어플리케이션에 추가한다.

var label:Label = new Label();this.addChild(label);


함수 안에서 만들어졌지만 어플리케이션에 추가 되었다.

어플리케이션은 child 로 label 을 참조하고 있고, label은 parent로 어플리케이션을 참조하게 된다.

this.removeChild(label);


하게 되면 참조 관계가 사라지고 레퍼런스 카운팅 값이 0 이 되어 사라질수 있게 된다.

어플리케이션안에 캔버스를 만들고 캔버스 안에 라벨을 만든다.

var canvas:Canvas = new Canvas();this.addChild(canvas);var label:Label = new Label();canvas.addChild(label);


어플리케이션이 child로 캔버스를 참조하고 있고 label은 parent 로 참조하고 있으니 캔버스의 레퍼런스 카운팅은 2 이다.
라벨은 캔버스와 관계가 있으니 카운팅은 1 이된다.

이때 캔버스를 삭제한다.

this.removeChild(canvas);


어플리케이션에서는 캔버스가 사라지고 아무것도 없다. 과연 캔버스 객체는 메모리에서 사라질까?
화면에서 사라졌지만.  캔버스와 라벨은 여전히 레퍼런스 카운팅 값이 1 이다. 상호참조 하고 있기 때문에 레퍼런스 카운팅 기법만으로는 여전히 삭제가 되지 않는다.

이러한 상호참조 문제를 해결하기 위해서 사용되는 방법이 두번째의 마크 앤 스윕이다.

마크 앤 스윕 기법은 어플리케이션에서부터 하위로 참조되고 있는 객체들을 찾아서 마크한다. 어플리케이션에서 부터 참조를 체크 한다는 것이 중요하다.
쭉 마크를 하고 모든 뎁스에 대해서 마크가 끝나면 마크가 없는 애들은 지운다.
마크 앤 스윕 방법은 레퍼런스 카운팅 보다 작업하는 시간이 더 걸리고 시스템에 부하도 많이 주기 때문에 자주 실행되지는 않는다고 한다.

위의 캔버스는 어플리케이션에서 참조되고 있지 않기 때문에 메모리를 반환하고 생을 마감하게 된다.

중요한 것은 위의 방법을 이해하고 사용하지 않는 것을 삭제 할때에 다음번 GC가 삭제 된것을 전부 쓸어갈수 있도록 코딩을 해야 메모리가 무한정 증가하다가 뻗는것을 막을수 있다.

그럼 메모리를 반환하는데 있어서 걸림돌이 되는것들이 무엇이 있을까.
가장 문제가 되는 것이 이벤트 리스너다.

객체지향으로 설계 하다보니 이벤트를 엄청나게 사용하게 되고 addEventListener가 컴포넌트를 만들다 보면 대여섯개씩 기본으로 붙는 경우가 허다하다.

이벤트는 기본적으로 이벤트를 디스패치한 객체의 참조를 가지고 날아가게 된다.
따라서 event.target 또는 currentTarget 으로 이벤트를 발생시킨 놈을 사용할 수 있는것이다.

어떤 컴포넌트가 이벤트를 리슨 하거나 디스패치 하게되면 그 객체는 레퍼런스 카운팅이 1이상으로 유지 되기 때문에 살아있는 이벤트가 하나라도 있으면 삭제가 되지 않는 문제가 발생한다.

따라서 addEventListener 해준것은 반드시 removeEventListener 해주는 습관을 들여야 한다.

지워지는 시점이 명확하지 않다거나 내부에서 참조되서 관리가 힘들다거나 하는 경우에는 언제 remove 해줘야 하는지 결정하기가 쉽지 않다.
이럴때 사용하는것이 useWeakReference 이다.

addEventListener (type:String, listener:Function, useCapture:Boolean = false, priority:int = 0, useWeakReference:Boolean = false):void
리스너에서 이벤트 알림을 받을 수 있도록 EventDispatcher 객체에 이벤트 리스너 객체를 등록합니다.

다섯번째 파라미터를 true 로 해주면 참조값을 약하게 잡고 있게 되고 이후에 GC 에서 삭제해줄 확률이 높아진다. 확률만 높아진다는 것이지 꼭 사라진다는건 아니기 때문에. removeEventListener 해주는 것이 가장 좋다.

이벤트쪽만 정리가 잘 되어있어도 메모리누수를 상당부분 막을수 있고.

플렉스IDE 를 사용한다면 개발중간중간에 프로파일러를 돌려서 체크 해보는 습관을 갖는게 좋을것같다.

나같은 경우는 커스텀 컴포넌트를 만들게 되면 destory() 함수를 만들어서 그 컴포넌트에서 사용되었던 이벤트 리스너를 일괄로 삭제하고 가능하면 컴포넌트 내부에 addChild 되어있던 것들도 삭제 해주는 함수를 만들어서 사용하고 있다.
removeChild 하기 전에 destory()를 실행시켜주고 삭제하면 기본적인 방지책은 되는것 같다.

PS. 메모리 관리에 좋은 방법들이 있으면 공유를 부탁드립니다~ 댓글 트랙백 환영
저작자 표시 비영리 동일 조건 변경 허락
신고

Comment +3