[js] 동기화 vs 비동기화 2 / 3

저번 글에 이어 비동기화 시스템의 사용처를 구체적인 예와 함께 살펴보겠습니다.

이번에 다룰 주제는 이벤트리스너와 콜백 입니다. 원래 2부작으로 기획되었으나 쓰다보니 또 길어져서 3부작으로 확장합니다.

역시 코드로 환원되지 않는 개념은 작용하기 힘들기 때문에 코드 샘플이 잔뜩 등장합니다(스코프에 익숙하시다고 가정하고 인정사정 없이 가겠습..^^;;)

 
 
 

이벤트리스너와 콜백

콜백은 처리하는 측에서 상태가 완료된 뒤 통보하는 전반적인 전략입니다.

c 에서는 함수포인터를 사용할 것이고 자바라면 받아온 인스턴스의 인터페이스에 정의된 메서드를 호출해 줄 것입니다.

자바스크립트라면 함수객체가 넘어와 그 함수를 호출하겠죠.

이렇게 콜백이란걸 생각해보면 이벤트 시스템도 콜백시스템의 범주에 들어가는 전략입니다. 그럼 차이점이 뭘까요?

이걸 한 마디로 정의할 수 있으면 좋겠지만 이벤트 시스템을 정의하는 방법이나 정의는 실제로 너무나 다양하고 상이합니다. 개인적으로 봐온 여러 이벤트 시스템에서 느낀 콜백과의 중요한 차이점은 두 가지 입니다.

  1. 콜백은 보통 하나의 함수에게 통보하지만, 이벤트는 여러개의 함수를 등록, 삭제하는 별도의 시스템을 갖추고 등록된 함수 전체에게 통보한다.
  2. 콜백은 개발자가 작성한 코드만으로 구성되지만, 이벤트는 언어 하부의 시스템층이 지원하는 기능과 결합하여 사용된다.

 
 
 

dom 이벤트 처리

dom 의 이벤트 시스템을 예로 들면 addEventListener, removeEventListener, dispatchEvent 등의 메서드를 통해 특정 이벤트를 수신할 리스너를 등록, 삭제할 수 있고 dispatch하는 경우 일제히 현재 등록된 리스너에게 통보합니다.
c#은 더욱 극적인데 저러한 메서드가 일반적으로 사용된다는 것을 착안하여 아예 연산자화 해버렸습니다. 즉 아래와 같이 등록하거나, 삭제합니다.

class Test{

	Delegate A void(int a );
	Event  A evA;

	Test(){

		//이벤트에 메서드를 등록하자
		evA += listener1;
		evA += listener2;

		//하나 빼자
		evA -= listener2;

		//dispatch하자
		evA( 3 ); //3출력
	}

	void listener1( int a ){
		log( a );
	}

	void listener2( int a ){
		log( a );
	}
}

자바스크립트에서 이벤트시스템을 소유하는 것은 대부분 시스템 객체입니다. 시스템 객체의 경우 알아서 내부적으로 실행되는게 기본입니다만 dom 의 경우 강제적으로 이벤트를 dispatch 할 수 있는 기능이 있습니다.

예를 들어 ‘bside’ 라는 이벤트를 정의해서 리스너도 등록하고 통보도 받아보죠.

var div = document.getElementById( 'test' );

//이벤트 등록
div.addEventListener( 'bside', function( $e ){
	alert( $e.type );
} );

//이벤트객체 생성
var e = document.createEvent( "Event" );
e.initEvent( "bside", true, true );

//디스패치
div.dispatchEvent( e ); //bside출력

dom 객체 외에 개발자가 브라우저에 내장된 이벤트 시스템을 사용할 수 있는 방법은 없습니다만 직접 구현하면 그만입니다. 결국 이벤트 시스템은 옵저버 패턴(observer)의 구상체일 뿐입니다.
일반적인 옵저버패턴에 비해 서브젝트(subject)가 다채널 리스너를 관리해주는 것과 info 객체인 event 로부터 디스패치할 채널을 얻어낸다는 점이 특정입니다.

var customEvent = (function(){
	var events = {};
	return {
		dispatchEvent: function( $e ){
			//e객체의 type으로부터 채널을 얻어낸다.
			var ev = events[$e.type];

			//디스패치
			var i = ev.length;
			while( i-- ) ev[i]( $e );
		},
		addEventListener: function( $type, $listener ){
			//채널이 없으면 생성
			if( !events[$type] ) events[$type] = [];

			//채널에 추가
			events[$type]. push( $listener );
		},
		removeEventListener: function( $type, $listener ){
			//채널에서 제거
			events[$type].splice( events[$type].indexOf( $listener ), 1 );
		}
	};
})();

이제 구현된 커스텀 이벤트 시스템을 사용해 봅니다.

//이벤트 등록
customEvent.addEventListener( 'bside', function( $e ){
	alert( $e.type );
} );

//이벤트객체 생성
var e = document.createEvent( "Event" );
e.initEvent( "bside", true, true );

//디스패치
customEvent.dispatchEvent( e ); //bside출력

짜피 흉내내보면 간단한 거죠. 이렇게 만들면 이벤트 시스템을 흉내낼 수 있지만 이벤트 시스템이라 부르지는 않습니다. 왜냐면 dom 의 이벤트는 브라우저의 시스템이 직접 해주는 것이고 위의 customEvent 는 제가 만들었기 때문입니다.
역시 시스템이 제공해야 이벤트라 부르는것 같습니다.

 
 
 

이미지로더

또 다른 예를 하나 더 살펴보겠습니다. 이 예에서는 dom 의 이벤트와 커스텀으로 직접 작성한 비동기 로직을 합체하여 크로스 브라우저에서 동작하는 이미지 로더를 만들어 냅니다.

일단 이미지로더에 왜 크로스브라우저 처리 층이 필요한가를 설명해보죠.

  1. img 태그의 경우 canvas 를 지원하는 브라우저라면 100% onload 이벤트를 지원합니다. 이 이벤트는 이미지가 로딩완료되면 발생하는 이벤트입니다.
  2. 하지만 IE 구버전은 8을 포함하여 onload 라는 이벤트는 존재하나 태그가 로딩되는 것을 의미하지 이미지데이터가 로딩될 때 발생하지 않습니다.
  3. 대신 모든 브라우저에는 img 태그의 속성으로 complete 라는게 있는데 이 속성을 조사하면 로딩이 완료되었는지 아닌지를 알 수 있게 됩니다.

그럼 complete 속성을 사용해서 구현할 거냐 라고 묻는다면 경우에 따라서 다르다라고 말씀드리겠습니다.

  1. canvas 를 지원하는 브라우저는 onload 를 사용하고
  2. 지원하지 않는 브라우저는 직접 비동기로직을 이용하여 complete 를 체크하도록

합니다. 역시 있는건 있는걸 쓰는게 쵝오죠!

자바스크립트에서 개발자는 오직 setInterval 등의 시간지연 적재 함수만 사용할 수 있으므로 이를 통해 동기로직을 비동기로 바꿔야합니다. 실제 코드는 간단하니 한방에 가겠습니다.

var imgLoader = (function(){
	//canvas가 지원되는 경우
	if( window['HTMLCanvasElement'] ) return function( $url, $end ){
		var img = document.createElement( "img" );
		img.onload = function(){
			//로딩이 완료되면 img를 넘긴다.
			$end( img );
		};
		img.src = $url; //로딩시작
	};

	//IE 등..
	return function( $url, $end ){
		var img = document.createElement( "img" );
		var interval = setInterval( function(){
			if( img.complete ){ //로딩되었으면
				clearInterval( interval );
				$end( img );
			}
		}, 20 );
		img.src = $url;
	};
})();

눈 여겨 볼 부분은 밑에 부분의 로더인데 개발자가 통제할 수 있는 비동기 명령인 setInterval 을 통해 로딩체크를 계속 해서 다음 프레임으로 미뤄주면서 감시하게 되는 거죠.

이벤트 시스템이란 결국 인터페이스 상으로 자동 콜백될 뿐이지, 로직 상으로는 루프로 감시하다 특정 조건에 발생하는 콜백인 것입니다.

다 만들었으니 사용해 봅니다.

//이미 있던걸 활성화
imgLoader( "aa.jpg", function( $img ){
	var i = document.getElementById( "titleImg" );
	i.src = $img.src;
	i.style.visibility = 'visible';
} );

//직접 엘레멘트로 활용
imgLoader( "aa.jpg", function( $img ){
	document.getElementById( "GNB" ).appendNode( $img );
} );

이미지를 로딩한 후에는 다른 이미지에서 src를 지정해도 304 가 되어 캐쉬에서 꺼내니 번쩍하고 적용됩니다.

..하지만 뭔가 좀 섭섭합니다.

이미지 한 개를 로딩하라니…

여러 개를 로딩하고 싶습니다. 스코프를 이용해 좀 더 확장해봅니다.

일단 예상하는 인터페이스는 다음과 같습니다.

imgLoader( ['aa.jpg', 'bb.jpg', 'cc.jpg'...], function( $imgs ){
	var i = $imgs.length;
	while( i-- ) document.getElementById( "GNB" ).appendNode( $imgs[i] );
} );

 
 
 

동기화된 이미지로더

배열로 넘겨준 순서와 동일하게 로딩이 전부 완료된 img 객체가 들어오게 하고 싶다는 거죠. 이 알고리즘을 구현하기 전에 먼저 생각해보실 내용이 있습니다.

  1. 이미지로더의 본래 목적은 비동기성의 확보입니다.
  2. 이미지가 로딩완료된 뒤에 콜백이 작동하게 실행프레임을 미뤄버린거죠.
  3. 하지만 여러개의 이미지를 로딩하는 경우는 비동기화된 로직을 다시 동기화시키는 효과가 있습니다.
  4. 즉 aa.jpg, bb.jpg, cc.jpg가 각각 1초, 2초, 3초에 로딩되었다고 하면, imgLoader 입장에서는 가장 늦은 3초까지 대기한 뒤 콜백을 호출하게 됩니다.
  5. 이는 비동기로 인해 불규칙한 프레임에서 작동하는 로직을 다시 균등하게 평탄하여 3초후의 프레임에서 실행하게 하는 효과가 있습니다.

즉 여러 개의 리스너를 병합하여 모든 결과가 만족될때까지 기다렸다가 콜백을 호출하는 중간층을 만들면 비동기적으로 발생하는 프레임명령을 하나의 프레임에서 실행하도록 처리할 수 있고, 이는 비동기로직을 다시 동기화로직으로 되돌리는 효과가 있습니다.

예를 들어 이미지로더가 동기화로직이라면 다음과 같이 순차적인 호출을 보장할 것입니다.

i1 = imgLoader( 'aa.jpg' );

//이시점에 i1은 반드시 로딩이 완료되어있다

i2 = imgLoader( 'bb.jpg' );

//여기서는 i1, i2가 로딩완료되어있다

i3 = imgLoader( 'cc.jpg' );

//전부 로딩되어있는게 확실하므로 그냥 사용하면 된다.

document.getElementById( "GNB" ).appendNode( i1 );
document.getElementById( "GNB" ).appendNode( i2 );
document.getElementById( "GNB" ).appendNode( i3 );

동기화된 로직은 위에서 아래로 읽으면 되기 때문에 이해하기 쉽고 직관적입니다. 대신 블로킹(blocking)을 일으켜 저게 처리될 때까지는 모든게 먹통이 됩니다. 동기적인 이미지로더는 아래와 같이 작성하면 됩니다.

function imgLoader( $url ){
	var img = document.createElement( "img" );
	img.src = $url;
	while( !img.complete );
	return img;
}

(저 while 이 스크립트 타입아웃에 안걸리길 빕니다!)

동기화 로직으로 작성하면 완전히 먹통이 됩니다. 로딩 게이지를 돌리는 것조차 불가능합니다.
반대로 비동기로 작성되지만 매 비동기 순간마다 연결되는 로직은 서로 분리되어있으므로 관리가 어렵습니다.

 
 
 

멀티이미지로더

여러 비동기를 묶어서 하나의 프레임으로 출력해주는 방식을 쓰지 않으면 관리가 힘들겠죠.
이제 여러 개의 이미지를 로딩하여 전부 로딩이 완료되면 end를 호출하는 함수로 개조해봅니다.


var multiLoader = (function(){
	function singleLoader( $src, $loaded){
		var t0, t1;
		t0 = new Image;
		if( window['HTMLCanvasElement'] ){
			t0.onload = $loaded;
		}else{ //인터벌 귀찮..
			( t1 = function(){
				t0.complete ? $loaded() : setTimeout( t1, 10 );
			} )();
		}
		t0.src = $src;
		return t0;
	}

	return function multiLoader( $urls, $end ){
		//혹시 모르니 복사본으로
		$urls = $urls.concat();

		var count = 0;
		function loaded(){
			//로딩이 완료되면..
			if( ++count == $url.length ) $end( $urls );
			}
		};

		//전부 일거에 비동기 로드를 시작!
		var i = $urls.length;
		while( i-- )$urls[i] = singleLoader( $urls[i], loaded ); };
	};
})();

singleLoader 야 이미 앞에 나온 소스입니다. 단지 interval 관리 대신 setTimeout 으로 구성해봤습니다.
함수의 마지막에 생성한 이미지 객체를 넘겨줌으로서 $urls 의 문자열로 들어온 배열을 전부 이미지 객체로 변환시킵니다.

loaded 는 카운터를 관리하면서 각 이미지객체가 로딩 완료될 때마다 카운트를 증가시키다가 최종적으로 $end 에게 로딩이 전부 끝난 이미지 객체의 배열을 넘겨주게 됩니다.

바로 이 loaded 함수야 말로 비동기화된 개별 이미지로딩의 완료시점을 하나로 묶어 $end 부터 다시 동기화 로직으로 돌입할 수 있게 해주는 것입니다.
비동기가 불연속적으로 발생할 때는 위와 같이 전체 결과를 취합하여 대기한 뒤 최종적으로 보고하는 형태로 구성하면 그 이후부터 다시 동기화 로직으로 들어갈 수 있는데, 일종의 시점 분리를 하는 셈입니다.

이를 많이 들 페이즈(phase)라고 부르기도 하고 스테이지(stage)라고 부르기도 합니다.

호스트코드는 이미 위에 보여드렸습니당~

 
 
 

결론

쓰다보니 길어져 3부작이 되었으므로 아직도 결론 따위를 말할 때가 아닙니다.

마지막 편에서는 무거운 동기화 로직의 분산에 대해 다루겠습니다.