일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
- 공유기 서버
- spring reactive
- Spring Batch
- reactor
- reactor core
- ipTIME
- 웹 커리큘럼
- Spring Framework
- 웹 스터디
- 서버운영
- reactive
- 웹앱
- Today
- Total
Hello World
모바일웹 터치 제스쳐 적용기 - [1] 터치 제스쳐 이해하기 본문
FE(Front-End) 개발자에게 모바일웹 서비스 개발은 매우 까다로운 작업입니다.
PC웹 환경은 디바이스 하드웨어 성능도 뛰어나며, 네트워크 속도도 기가(GIGA?) 막히게 빠릅니다. (물론 아직도 저사양 PC에IE7,8,9을 사용하는 분들이 소수 존재하기에, 구버전 웹브라우저에서 사용가능한 서비스들도 있습니다. 다만 화면깨짐은 있더라도 말이죠.)
반면, 모바일은 저성능의 디바이스, 일정하지 않은 네트워크 속도, 작은 디스플레이 사이즈라는 제약 안에서 개발이 진행됩니다. 거기다 OS 종류, 버전, 제조사에 따라 웹브라우저가 조금씩 다른 입출력 결과를 내놓고 있어, 모든 모바일 단말기에서 안정성을 보장하기란 쉽지 않습니다.
이런 열악한 환경을 개선하기 위한 방법으로 리소스(이미지,JS,CSS) 사이즈를 최소화하고, 작은 화면에서도 사용자의 불편함이 없도록 화면설계(UI/UX)를 하는데, 오늘은 이 중에서 터치 제스쳐(gesture)에 대해 설명 드리고자 합니다.
터치 제스쳐란?
PC웹에서 마우스(mouse)이벤트가 있다면 모바일웹에서 터치(touch)이벤트가 있습니다.
PC에서도 더블클릭, 우클릭 등 다양한 마우스이벤트를 지원하지만, 실제로 사용하는 서비스는 드뭅니다. PC웹 환경은 컨텐츠와 메뉴 버튼들을 한 화면에 담을 수 있고, 클릭만으로도 불편함 없이 사용이 가능합니다. 만약 클릭 이외의 다른 액션을 추가한다면, 사용자에게 사용법에 대한 가이드도 제공해야 하는데, 대부분의 사용자들은 가이드를 번거롭다고 느끼는 경향이 있습니다.
반면 작은 화면, 큰 포인터(손가락) 환경인 모바일에서는 컨텐츠와 메뉴(네비게이션)를 한화면에 담기 어렵습니다. 컨텐츠 관련 메뉴 보기를 위한 추가적인 터치가 필요한데, 터치의 형태를 다양화하여 PC에서의 단축키와 같이 단축 액션을 제공하는 것이 터치 제스쳐 입니다.
대표적인 터치 제스쳐와, 실행되는 액션은 아래와 같습니다.
출처 gesturecons.com
[주요 터치 제스쳐]
제스쳐 명 | 주요 액션 |
Tap | 선택 |
Double Tap | 열기(팝업), 화면 확대/원복 |
Hold(Long Tap) | 모드 전환(리스팅<->액션) |
Drag-Flick(Swipe) | 이전/다음 컨텐츠로 이동(삭제) |
Pinch | 이미지 확대/축소 |
Rotation | 이미지 회전 |
현재 티몬 모바일웹에는 사용 중인 터치 제스쳐가 없습니다.
만약 터치 제스쳐를 아래와 같이 적용한다면, 사용자의 편의성을 높이면서 딜 상세페이지에서 소비될 데이터를 아껴주는 효과를 가져오지 않을까요?
- 상품목록 페이지 - 더블탭을 적용 - 새창에서 딜상세 페이지를 보여줌
- 상품목록 페이지 - 롱탭 - 상품 찜하기 또는 카트에 담기 액션과 연결
- 상품상세 페이지 - 플리킹(스와이프) - 이전/다음 딜로 이동
- 상품상세 페이지 - 더블탭 시 상품 이미지 확대/축소 (확대,축소 버튼제거)로 적용 - 플로팅 버튼(+)을 제거
추가적으로 프로모션 페이지에서는 쿠폰 다운로드 버튼 터치시 강도에 따라 쿠폰금액을 랜덤하게 발급하는 이벤트를 오픈할수도 있을 것입니다. (※ IOS9 - IPHONE 6S - Safari browser 에서 터치의 강도를 측정할 수 있음)
적용분야를 알아봤으니, 코드 구현은 어떻게 해야하는지 알아보겠습니다.
터치 이벤트
모바일 웹브라우저에서는 W3C에서 권고하는 4가지 터치 이벤트를 지원합니다.
[기본 터치 이벤트]
이벤트 명 | 설명 |
touchstart | 스크린에 손가락이 닿을 때 발생 |
touchmove | 스크린에 손가락이 닿은 채로 움직일 때 발생 |
touchend | 스크린에서 손가락을 뗄 때 발생 |
touchcancel | 시스템에서 이벤트를 취소시킬 때 발생 (브라우저마다 다르기에, touched 이벤트로 간주해도 무방함) |
각 이벤트 발생시에 전달되는 이벤트 객체에는 touches, targetTouches, changedTouches 의 속성이 존재하고,
[이벤트 객체 주요 속성]
속성 | 설명 |
changedTouches | 이벤트에 할당된 모든 접촉점에 대한 터치 리스트 - touchstart 이벤트의 changedTouches는 현재 이벤트와 함께 바로 활성화된 터치점(touch point)의 리스트 - touchmove 이벤트의 changedTouches는 마지막 이벤트에서 이동한 터치점의 리스트 - touchend와 touchcancel 이벤트의 changedTouches는 화면에서 막 떼어진 터치점의 리스트 |
targetTouches | 현재 이벤트의 타겟인 엘리먼트에서 시작되어 화면을 터치하고 있는 모든 접촉점에 대한 터치 리스트 |
touches | 현재 화면을 터치하고 있는 모든 접촉점의 터치 리스트이며 터치된 구역의 정보를 담은 배열 - event.touches.length를 이용해 멀티터치 여부를 계산할 수 있음 - 특정 터치의 좌표 : event.touches[i].pageX(pageY) |
각 속성들은 Touch 객체 배열로 구성되어 있습니다.
[Touch 객체 속성]
속성 | 설명 |
identifier | 인식 점을 구분하기 위한 인식 점 번호 |
screenX | 디바이스 화면을 기준으로 한 X좌표 |
screenY | 바이스 화면을 기준으로 한 Y 좌표 |
clientX | 브라우저 화면을 기준으로 한 X 좌표 (스크롤 미포함) |
clientY | 브라우저 화면을 기준으로 한 Y 좌표 (스크롤 미포함) |
pageX | 가로 스크롤을 포함한 브라우저 화면을 기준으로 한 X 좌표 |
pageY | 세로 스크롤을 포함한 브라우저 화면을 기준으로 한 Y 좌표 |
target | 터치된 DOM 객체 |
참고로, IOS의 경우 멀티터치에 대한 비표준 이벤트를 추가로 제공하여, 편의성을 높이고 있습니다.
[IOS 제스쳐 이벤트]
이벤트 명 | 설명 |
gesturestart | 두번째 손락락이 화면에 닿을 때 발생 |
gesturechange | 두개 이상의 손가락이 화면에 닿은 상태에서 움직일때 발생 |
gestureend | 두개 이상의 손가락이 화면에 닿은 상태에서 하나의 손가락이 떨어질 때 발생 |
터치 제스쳐 구현
자, 그럼 Tap, Double Tap을 간략하게 구현해 보겠습니다.
Tap
탭(Tap)은 touchstart 이벤트 발생후 touchemove 이벤트 발생없이 touchend이벤트가 발생할때 탭으로 식별합니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | var bStartEvent = false; //touchstart 이벤트 발생 여부 플래그 var bMoveEvent = false; //touchmove 이벤트 발생 여부 플래그 function init(){ document.addEventListener("touchstart", this.onStart.bind(this), false); document.addEventListener("touchmove", this.onMove.bind(this), false); document.addEventListener("touchend", this.onEnd.bind(this), false); } function onStart(e) { bStartEvent = true; } function onMove(e) { if(!bStartEvent) { return; //touchstart 이벤트가 발생하지 않으면 처리하지 않는다. } bMoveEvent = true; //touchMove 이벤트 발생 여부를 설정한다. } function onEnd(e) { if(bStartEvent && !bMoveEvent) { //클릭 이벤트로 판단한다. alert('Tap!'); } //각 플래그 값을 초기값으로 설정한다. bStartEvent = false; bMoveEvent = false; } |
Double Tap
더블탭 구현은 탭구현에서 탭과 다음탭이벤트 사이의 간격체크, 첫번째 탭과 두번째 탭 사이의 포인트거리체크 로직이 추가됩니다.
탭과 다음탭 사이의 간격이 200ms 보다 짧으면 더블탭으로 판단하고, 첫번째 탭이후 300ms 후 더블탭이벤트가 발생하지 않았다면 탭이벤트로 식별합니다. (※ 탭만 사용할경우에 비해 탭이벤트가 300ms 늦게 발생합니다.)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 | var bStartEvent = false; //touchstart 이벤트 발생 여부 플래그 var bMoveEvent = false; //touchmove 이벤트 발생 여부 플래그 htClickInfo = { //더블탭을 판단하기 위한 마지막 탭 이벤트의 정보 해시 테이블 sType : null, nX : -1, nY : -1, nTime : 0 } var nDoubleTapDuration = 200; //더블탭을 판단하는 기준 시간(ms) var nTapThreshold = 5; //탭을 판단하는 거리 var oTapEventTimer = null; //탭-더블탭 대기 타이머 function init(){ document.addEventListener("touchstart", this.onStart.bind(this), false); document.addEventListener("touchmove", this.onMove.bind(this), false); document.addEventListener("touchend", this.onEnd.bind(this), false); } function initClearInfo() { htClickInfo.sType = null; } function onStart(e) { bStartEvent = true; } function onMove(e) { if(!bStartEvent) { return; //touchstart 이벤트가 발생하지 않으면 처리하지 않는다. } bMoveEvent = true; //touchmove 이벤트 발생 여부를 설정한다. } function onEnd(e) { var nX = e.changedTouches[0].pageX; var nY = e.changedTouches[0].pageY; var nTime = e.timeStamp; if(bStartEvent && !bMoveEvent) { //이전 탭 이벤트와 시간 차이가 200ms 이하일 경우 if(htClickInfo.sType == "tap" && (nTime - htClickInfo.nTime) <= nDoubleTapDuration){ if( (Math.abs(htClickInfo.nX-nX) <= nTapThreshold) && (Math.abs(htClickInfo.nY-nY) <= nTapThreshold) ){ //더블탭으로 판단한다. (탭이 발생하지 않게 탭 발생 타이머 초기화한다.) clearTimeout(oTapEventTimer); alert("Double Tap"); } } else { //탭 이벤트로 판단한다. //현재 탭 이벤트들에 대한 정보를 업데이트한다. oTapEventTimer = setTimeout(function(){ alert("Tap"); }.bind(this), 300); htClickInfo.sType = "tap"; htClickInfo.nX = nX; htClickInfo.nY =nY; htClickInfo.nTime = nTime; } } else { //탭 이벤트가 아니므로 탭 이벤트 정보를 초기화한다. initClearInfo(); } //각 플래그 값을 초기값으로 세팅한다. bStartEvent = false; bMoveEvent = false; } |
Swipe
swipe는 touchstart이벤트와 touchEnd사이의 이동거리와, 각도, 방향을 고려해야 합니다.
각도 임계치를 정하여, 상하 스크롤시에 스와프이벤트가 발생하는 것을 방지해야 합니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | function onMove(e) { if(!bStartEvent) { return } var nX = e.changedTouches[0].pageX; var nY = e.changedTouches[0].pageY; //현재 touchmMove에서 사용자 터치에 대한 움직임을 판단한다. nMoveType = getMoveType(nX, nY); //현재 사용자 움직임을 수직으로 판단해 기본 브라우저의 스크롤 기능을 막고 싶으면 아래 코드를 사용한다. if(nMoveType === 1) { e.preventDefault(); } } function getMoveType(x, y) { //0은 수평방향, 1은 수직방향 var nMoveType = -1; var nX = Math.abs(htTouchInfo.nX- x); var nY = Math.abs(htTouchInfo.nY - y); var nDis = nX + nY; //현재 움직인 거리가 기준 거리보다 작을 땐 방향을 판단하지 않는다. if(nDis < 25) { return nMoveType } //수평 방향을 판단하는 기준 기울기 var nHSlope = ((window.innerHeight / 2) / window.innerWidth).toFixed(2) * 1; //현재 터치의 기울기 var nSlope = parseFloat((nY / nX).toFixed(2), 10); if(nSlope > nHSlope) { nMoveType = 1; } else { nMoveType = 0; } return nMoveType; } |
마치며
한 손가락의 터치 제스쳐인 tap, double tap, swipe 구현도 쉽지 않습니다. 정확한 제스쳐 식별을 위해서는 적절한 임계치(터치 시간, 각도, 속도 등) 정보가 필요할 것입니다. 두 손가락의 제츠쳐인 Pinch, Rotation은 두 포인트 사이의 거리, 각도, 가까워지는지, 멀어지는지, 좌측으로 회전하는지, 우측으로 회전하는지 등 식별이 훨씬 복잡해집니다.
다음 2부에서는 이런 어려움을 해결시켜줄 터치이벤트 라이브러리 Hammer.js를 소개하고, 실제 서비스에 적용 가능한 코드를 구현해 보겠습니다.
참고
http://gesturecons.com
http://d2.naver.com/helloworld/80243
http://hcitrends.kr/portfolio-item/ui-report
http://www.w3.org/TR/touch-events/
[출처] 모바일웹 터치 제스쳐 적용기 - [1] 터치 제스쳐 이해하기|작성자 데몬
출처: http://tmondev.blog.me/220680473335
'Javascript > Core' 카테고리의 다른 글
Debouncing and Throttling Explained Through Examples (0) | 2016.04.18 |
---|---|
NHN Entertainment 자바스크립트 개발 가이드 (0) | 2016.04.18 |
번역: What (function (window, document, undefined) {})(window, document); really means (0) | 2016.03.02 |
HTML5 LocalStorage 살펴보기 (0) | 2016.02.18 |
[펌] eval() 함수에 대한 재정의 : eval() is not evil?/ (0) | 2016.02.02 |