[js] 터치이벤트를 손가락별 이벤트로 분리하기

마우스 이벤트는 매우 심각하게 다뤄온지 오래되었기 때문에 여러가지 기법이 널리 알려져있습니다. 이에 반해 터치이벤트는 손쉽게 정리하기 쉽지 않습니다.

터치이벤트는 더 이상 모바일 기기만의 문제가 아닙니다. 반응형 웹을 구성할 때 마우스 조차 터치의 슬라이드를 흉내내야하고 터치패널이 내장된 윈도우 기기도 심심치 않게 있습니다.

터치이벤트를 깊이 들여다보고, 보다 손쉽게 사용할 방법을 생각해 보겠습니다.

 
 
 

터치이벤트의 문제top

기본은 터치이벤트라 불리는 이벤트의 종류입니다. 현재 대부분의 모바일 기기에서 지원되는 이벤트는 3가지로 그 외에는 벤더의 의존성이 너무 심해서 범용으로 쓰기는 무리가 있습니다.

  • touchstart : 손가락을 화면에 닿는 순간 발생
  • touchmove : touchstart 한 상태에서 떼지 않고 화면을 움직여 다닐 때 주기적으로 발생
  • touchend : 손가락을 화면에서 떼면 발생

느낌으로 보자면 mousedown, mousemove, mouseup 과 같은 분위기 입니다…만!

터치이벤트는 중요한 차이점이 있습니다. 멀티터치를 지원한다는 거죠.

  1. 예를 들어 검지가 화면에 닿는 순간 이미 touchstart 가 발생했습니다.
  2. 하지만 그 상태에서 중지가 화면에 닿으면 touchend가 발생한 적도 없이 또 touchstart가 발생합니다.
  3. 마찬가지로 검지를 움직이면서 중지를 떼면 touchmove가 발생하면서 동시에 touchend도 발생합니다.

이 점이 바로 지옥입니다.

손가락이 여러 개다 보니 하면서 ~ 한다는 거죠. 이런 관점으로 마우스 이벤트를 새삼 바라보면 손가락을 한 개만 지원하는 프로요같은 싱글 터치기반의 이벤트와 비슷한 것입니다.

단순히 dom 에 터치이벤트를 거는 것 만으로는 복잡한 처리를 할 수 없습니다. touchstart 가 수신 되어도 모든 손가락이 touchstart 가 아니고 어떤 손가락은 touchmove 중이고, 어떤 손가락은 이미 touchstart 가 발생된 상태이기 때문입니다.

이를 테스트하기 위한 간단한 js를 제작해 봅시다.

  1. div 를 하나 설정하고 id 를 test 로 준다.
  2. touchstart, touchend 를 걸어 결과를 test.innerHTML 을 통해 보여준다.
  3. 별도의 로그함수를 작성하여 간단히 사용한다.

로그함수를 작성해봅시다. div는 공간이 유한하므로 10개 정도 보이다가 다시 클리어하고 보여주는 식으로 가겠습니다.

<div style=”width:100%;height:480px;background:#ff0;color:#000;font-size:9px” id=”test”>
</div>
var test = document.getElementById( "test" );
var logCount = 0;

function log( $v ){
	if( ++logCount > 10 ){
		logCount = 0;
		test.innerHTML = $v + "<br>";
	}else{
		test.innerHTML += $v + "<br>";
	}
}

다음은 이벤트 리스너에서의 작동인데 배열에 필요한 내용을 차곡히 넣은 뒤 join( ” ) 을 통해 log 로 쏴주면 될 것입니다.
터치된 객체 자체는 touches 에 들어있지만 touchend 의 경우는 changedTouches 를 조사해야 하니 두 개 다 정리해서 보여주도록 하겠습니다.

function touchListener( $e ){
	var i, j, t0, touch;

	//touches부터 정리
	touch = $e.touches;
	t0 = [ '<b>',
		$e.type,
		'</b><br>touches(length:',
		j = touch.length
	];

	for( i = 0 ; i < j ; i++ )
		t0.push(
			', ', i, ':',
			parseInt( touch[i].pageX ), ',',
			parseInt( touch[i].pageY )
		);

	//changedTouches 정리
	touch = $e.changedTouches;
	t0.push(
		')<br> changed(length:',
		j =touch.length
	);
	for( i = 0 ; i < j ; i++ )
		t0.push(
			', ', i, ':',
			parseInt( touch[i].pageX ), ',',
			parseInt( touch[i].pageY )
		);

	//로그에 보고
	log( t0.join( '' ) + ')' );
	test.dispatchEvent( new Event("to") );
}

다 되었으니 이제 이벤트 리스너만 걸어주면 됩니다.

test.addEventListener( 'touchstart', touchListener );
test.addEventListener( 'touchend', touchListener );

다음과 같은 로그들이 찍혀 나오기 시작할 겁니다.

1

 
 
 

touchmove의 특이점top

touchmove 는 touchstart 나 touchend 와 달리 일반적으로 브라우저가 처음부터 소유해 버립니다.

따라서 최초 한 번은 발생하지만 그 이후는 브라우저의 스크롤이 가져가 버립니다.

만약 어떤 dom 에서 touchmove 이벤트를 사용하겠다는 뜻은 바꿔 말하면

그 영역에서는 스크롤이 안일어나게 하겠다

는 뜻입니다.

따라서 touchmove 의 경우는 반드시 e.preventDefault() 를 호출하여 막아주지 않으면 제대로 작동하지 않습니다. 이러한 특이점을 머리 속에 염두해두고 귀찮으므로, 대부분의 실험은 touchstart 와 touchend 를 이용하는 쪽으로 가겠습니다.

 
 
 

Touches와 changedTouchestop

이제 이 테스터를 갖고 놀다보면 어떤 식으로 터치를 처리해야 하는 지 감이 옵니다. 터치이벤트는 touches 배열과 changedTouches 배열의 미묘한 조합인 것입니다. 이 중 changedTouches 를 먼저 이해해 보죠.

  1. 우선 검지를 화면에 가져갑시다.

    touchstart
    touches(length:1,0:100,100)
    changed(length1,0:100,100)

    즉 터치된 것도 변화가 일어난 것도 각각 1개이고 그 둘은 같은 거죠.
     

  2. 그 상태에서 움직이지 않고 중지도 화면에 가져갑니다.

    touchstart
    touches(length:1,0:100,100, 1:200,200)
    changed(length1,0:200,200)

    0번은 유지 되지만 1번이 새롭게 터치 객체에 추가됩니다. 이 때 변화한 손가락 쪽은 중지로 changed에는 중지만 들어와 있습니다.
     

  3. 검지를 떼봅시다.

    touchend
    touches(length:1,0:200,200)
    changed(length1,0:100,100)

    화면에 남아있는 중지가 touches의 0번이 되고 방금 뗀 검지는 changed의 0번이 되어 들어옵니다.
     

  4. 중지도 뗍니다.

    touchend
    touches(length:0)
    changed(length1,0:200,200)

    touches는 더 이상 남아있지 않고 중지는 changed의 0번이 되어 들어옵니다.

우선 위의 실험에서 알 수 있는 것은…

  • touchstart 와 touchend 에서 changedTouches 는 언제나 1개만 들어옵니다.
  • changedTouches 가 여러 개 들어오는 상황은 touchmove 에서 2개 이상의 손가락을 동시에 움직일 때 입니다.
  • 인덱스는 그냥 인덱스일 뿐 처음 터치한 손가락이 뭔지, 두 번째 터치한 손가락이 뭔지를 알려주지는 않습니다.

 
 
 

identifier 를 이용한 손가락별 이벤트top

그렇습니다. touches 의 배열과 changedTouches 의 배열을 굴려봐야 거기의 인덱스로는 아무 것도 알 수 없습니다.
각 터치 객체에는 identifier 라는 손가락 고유의 id 를 갖고 있습니다. 바로 이것을 이용해야 합니다.

잠시 마우스 이벤트로 돌아가 봅시다.

  1. 마우스 이벤트를 싱글터치 이벤트로 이해할 수도 있지만 아주 정확하게 말하자면 마우스 왼쪽 버튼에 한정된 이벤트로 볼 수도 있습니다.
  2. 그렇다면 터치이벤트도 손가락별 이벤트로 나눌 수 있습니다.
    단지 검지이벤트로 할 수는 없고 1번째 손가락 이벤트, 2번째 손가락 이벤트 등으로 처리할 수 있습니다.

이 아이디어를 적용하려면 dom 의 이벤트를 받는 리스너가 손가락별 이벤트의 라우터로 작동하여 다시 이벤트를 전달해줘야 합니다. 이러한 이벤트를 매우 상세하게 정의해보면 다음과 같이 생각할 수 있습니다(네개까지만 하겠습니다 =.=; )

touchstart0, touchmove0, touchend0
touchstart1, touchmove1, touchend1
touchstart2, touchmove2, touchend2
touchstart3, touchmove3, touchend3

또한 앞에서 한 실험을 고려해보면 개별 손가락 이벤트로 처리하는 이상 변화된 손가락만 인지하면 되므로 touches 를 무시하고 changedTouches 만 사용하면 됩니다.

모든 내용을 바탕으로 우선 터치이벤트를 라우팅 해 줄 리스너를 작성해봅시다.

function touchRouter( $e ){
	var t, e, i, j, k;

	//앞 서 설명한대로, move 일 땐 막자.
	if( $e.type == "touchmove" ) $e.preventDefault();

	t = $e.changedTouches;
	for( i = 0, j = t.length ; i < j ; i++ ){

		//id를 붙여 이벤트를 만들고
		e = document.createEvent( "Event" );
		e.initEvent( $e.type + t[i].identifier, true, true );

		//속성을 복사해준다.
		for( k in t[i] ) e[k] = t[i][k];

		//라우팅~
		$e.target.dispatchEvent( e );
	}
}

머..딱히 설명할 것도 없습니다. 그냥 라우팅 하고 있습니다. 이제 손가락별로 이벤트를 걸어봅시다. 순서는 test 에 라우터를 걸어준 뒤, 손가락별 이벤트를 셋팅하면 됩니다. 손가락별 리스너를 먼저 작성해볼까요.

function fingerListener( $e ){
	log(
		'<b>' + $e.type +
		'</b><br>pos(' +
		parseInt( $e.pageX ) + ',' +
		parseInt( $e.pageY ) +
		')'
	);
}

이제 test 에 위에 언급한 순서대로 걸어줍니다.

//라우터 셋팅
test.addEventListener( 'touchstart', touchRouter );
test.addEventListener( 'touchmove', touchRouter );
test.addEventListener( 'touchend', touchRouter );

//0번손가락
test.addEventListener( 'touchstart0', fingerListener );
test.addEventListener( 'touchend0', fingerListener );

//1번손가락
test.addEventListener( 'touchstart1', fingerListener );
test.addEventListener( 'touchend1', fingerListener );

이제 다음과 같은 화면을 볼 수 있습니다.

Screenshot_2013-10-03-13-10-23

성능 상의 개선이라면 이벤트객체를 매번 생성치 말고 풀링하는 방안과 for~in 루프 대신 touch같은 속성에 직접 touches객체를 넣어서 전달해주는 방법 등을 고려해볼만 합니다.

 
 
 

결론top

이게 정말 끝이라고 생각하신건 아니겠죠? 모바일 개발의 지옥은 지금부터 입니다.

현재 일반적으로 커버해야하는 모바일 브라우저의 수는 국내처럼 갤럭시로 대동통합된 경우를 봐도 20개 이상입니다. 20개?!! 정말? 주요한 것들만 세봅니다.

  • 아이폰, 아이패드용 : 16종
    ( 크롬, 파폭, 오페라, 네이버내장브라우저, 페이스북내장브라우저 – 5종 +
    iOS5 에서 풀스크린이 지원되지 않는점, iOS7 에서 화면이 더 넓어진 점을 고려하면 사파리 3종 ) * 2(아이패드별도)
     
  • 안드로이드 공통 : 5종
    크롬, 파이어폭스, 돌핀, 오페라, 네이버내장브라우저 – 나머지는 점유율 상 무시
     
  • 갤럭시 내장 : 5종
    2, 2업뎃안함(꽤많음), 3, 4, 탭시리즈 전기종 – 나머지는 대략 이 넘들과 호환
     
  • 제조사별 최신 기종 : 7종
    엘지 G Gp G2 뷰 4종 + 베가 최근순으로 3종 – 나머지 HTC, 넥서스등 다 쌩깜.

이게 바로 최소 국내 스펙입니다. 비사이드는 일본 일을 하는데 그렇게 되면 위의 항목에 추가하여 기종별 내장 브라우저의 작동을 확인해야 하는데..

  • 구글 넥서스 시리즈 최근 순으로 : 3종
  • 소니는 OS별로 다 특이함. 진저, 아이스크림, 젤리빈 : 3종
  • 샤프, 도시바, 히타치 등 일본메이커 : 40여종..
  • 중국산 HTC, 하웨이, ZTE 등 중국 메이커 : 10여종..

이 추가됩니다.

예를 들어 소니의 이전 내장 브라우저 중 하나는 모두 손가락을 떼기 전에는 절대로 changedTouches 에 아무 것도 안들어오는 버그가 있습니다. 이것만으로도 위에 구축한 손가락별 이벤트는 개망입니다.

아이스크림 이후의 기종들은 제법 안정화 되었다고는 하나 모바일브라우저의 세계는 정말 즐겁게 해줍니다. =.=;

모쪼록 여러분의 건투를 빕니다.