전화기에서 데스크톱 화면에 이르기까지 점점 더 많은 기기에서 터치스크린을 사용할 수 있습니다. 앱은 직관적이고 멋진 방식으로 터치스크린의 터치에 반응해야 합니다.
전화기에서 데스크톱 화면에 이르기까지 점점 더 많은 기기에서 터치스크린을 사용할 수 있습니다. 사용자가 UI와의 상호작용을 선택하면 앱은 직관적인 방식으로 터치에 반응해야 합니다.
요소 상태에 반응
웹페이지에서 어떤 요소를 터치하거나 클릭했는데 사이트가 이 터치나 클릭을 실제로 감지했는지 궁금했던 적이 있으신가요?
사용자가 UI의 일부를 터치하거나 상호작용할 때 요소의 색상을 변경하기만 해도 사이트가 제대로 작동하는지를 확인할 수 있습니다. 그러면 좌절감이 줄어들 뿐만 아니라 사이트가 빠르고 반응성이 뛰어나다는 느낌을 줍니다.
DOM 요소는 default, focus, hover, active의 네 가지 상태를 임의로 상속할 수 있습니다. 이러한 각 상태에 대해 UI를 변경하려면 아래와 같이 다음 의사 클래스 :hover
, :focus
, :active
에 스타일을 적용해야 합니다.
.btn {
background-color: #4285f4;
}
.btn:hover {
background-color: #296cdb;
}
.btn:focus {
background-color: #0f52c1;
/* The outline parameter suppresses the border
color / outline when focused */
outline: 0;
}
.btn:active {
background-color: #0039a8;
}
대부분의 모바일 브라우저에서는 요소를 탭한 후 마우스 오버 또는 포커스 상태가 요소에 적용됩니다.
어떤 스타일을 설정할지 그리고 사용자가 이 스타일을 터치했을 때 어떻게 보일지를 신중하게 고려하세요.
기본 브라우저 스타일 억제
다른 상태에 대한 스타일을 추가한 경우, 대부분의 브라우저는 사용자 터치에 반응하여 자체 스타일을 구현하게 됩니다. 그 주된 이유는, 휴대기기가 처음 시작될 때 다수의 사이트는 :active
상태에 대한 스타일이 없기 때문입니다. 따라서 사용자 피드백을 제공하기 위해 많은 브라우저들이 추가적인 강조 표시 색상이나 스타일을 추가했습니다.
대부분의 브라우저에서는 요소에 포커스가 있을 때 outline
CSS 속성을 사용하여 요소 주위에 링을 표시합니다. 다음과 같이 억제할 수 있습니다.
.btn:focus {
outline: 0;
/* Add replacement focus styling here (i.e. border) */
}
Safari 및 Chrome에서는 -webkit-tap-highlight-color
CSS 속성으로 방지가 가능한 탭 하이라이트 색상을 추가합니다.
/* Webkit / Chrome Specific CSS to remove tap
highlight color */
.btn {
-webkit-tap-highlight-color: transparent;
}
Windows Phone에 설치된 Internet Explorer도 동작은 유사하지만 메타 태그를 통해 억제됩니다.
<meta name="msapplication-tap-highlight" content="no">
Firefox에는 처리할 두 가지 부작용이 있습니다.
-moz-focus-inner
의사 클래스는 터치 가능한 요소에 윤곽선을 추가하므로 border: 0
를 설정하여 삭제할 수 있습니다.
Firefox에서 <button>
요소를 사용 중인 경우 그라데이션을 적용하게 되는데, background-image: none
을 설정하여 이 그라데이션을 제거할 수 있습니다.
/* Firefox Specific CSS to remove button
differences and focus ring */
.btn {
background-image: none;
}
.btn::-moz-focus-inner {
border: 0;
}
user-select 사용 중지
UI를 만들 때 사용자가 요소와 상호작용하기를 원하거나, 텍스트를 길게 눌러 선택하거나 마우스를 UI 위로 드래그하는 기본 동작을 억제해야 하는 경우가 있습니다.
이를 위해 user-select
CSS 속성을 사용할 수 있지만 주의할 점은, 사용자가 요소의 텍스트를 선택하기를 원하는데 콘텐츠에서 이 작업을 수행할 경우 사용자의 극도로 분노를 유발할 수 있습니다.
따라서 주의해서 사용하고 가급적 사용하지 않는 것이 좋습니다.
/* Example: Disable selecting text on a paragraph element: */
p.disable-text-selection {
user-select: none;
}
맞춤 동작 구현
사이트에 맞는 맞춤 상호작용과 동작을 구현할 경우 다음 두 가지 사항을 명심해야 합니다.
- 모든 브라우저를 지원하는 방법
- 프레임 속도를 높게 유지하는 방법
이 도움말에서는 모든 브라우저를 지원하는 데 필요한 API를 다루고 이러한 이벤트를 효율적으로 사용하는 방법을 다룹니다.
제스처가 어떤 동작을 수행하는지에 따라 사용자가 한 번에 하나의 요소와 상호작용하거나 또는 여러 요소와 동시에 상호작용할 수 있도록 해야 합니다.
이 문서의 두 가지 예시인 모든 브라우저를 지원하는 방법과 프레임 속도를 높게 유지하는 방법에 대해 살펴보겠습니다.
첫 번째 예에서는 사용자가 하나의 요소와 상호작용할 수 있습니다. 이 경우 동작이 요소에서 처음 시작되었다면 모든 터치 이벤트를 이 요소에 지정할 수 있습니다. 예를 들어 스와이프 가능 요소에서 손가락을 떼더라도 여전히 이 요소를 제어할 수 있습니다.
이 기능은 뛰어난 유연성을 사용자에게 제공하지만 사용자가 UI와 상호작용할 수 있는 방식이 제한됩니다.
하지만 사용자가 멀티 터치를 사용하여 동시에 여러 요소와 상호작용할 것으로 예상되는 경우 터치를 특정 요소로 제한해야 합니다.
이 방법은 사용자에게 더 많은 유연성을 제공하지만, UI 조작을 위한 로직이 복잡하며 사용자 오류에 대한 복원성이 떨어집니다.
이벤트 리스너 추가
Chrome (버전 55 이상), Internet Explorer, Edge에서 맞춤 동작을 구현하려면 PointerEvents
를 사용하는 것이 좋습니다.
다른 브라우저에서는 TouchEvents
및 MouseEvents
가 올바른 방법입니다.
PointerEvents
의 좋은 기능은 마우스, 터치, 펜 이벤트를 비롯한 여러 유형의 입력을 하나의 콜백 집합으로 병합한다는 것입니다. 수신할 이벤트는 pointerdown
, pointermove
, pointerup
, pointercancel
입니다.
다른 브라우저에서 이에 상응하는 이벤트는 터치 이벤트의 경우 touchstart
, touchmove
, touchend
, touchcancel
이며, 마우스 입력 시에 동일한 동작을 구현하려면 mousedown
, mousemove
, mouseup
을 구현해야 합니다.
어떤 이벤트를 사용할지 궁금하면 터치, 마우스, 포인터 이벤트 표를 확인하세요.
이러한 이벤트를 사용하려면 DOM 요소에서 이벤트 이름, 콜백 함수, 불리언과 함께 addEventListener()
메서드를 호출해야 합니다.
불리언은 다른 요소가 이벤트를 포착하고 해석할 기회를 갖기 전 또는 후에 이벤트를 포착해야 하는지를 결정합니다. (true
는 이벤트가 다른 요소 앞에 표시되기를 원하는 것을 의미합니다.)
다음은 상호작용의 시작을 수신 대기하는 예시입니다.
// Check if pointer events are supported.
if (window.PointerEvent) {
// Add Pointer Event Listener
swipeFrontElement.addEventListener('pointerdown', this.handleGestureStart, true);
swipeFrontElement.addEventListener('pointermove', this.handleGestureMove, true);
swipeFrontElement.addEventListener('pointerup', this.handleGestureEnd, true);
swipeFrontElement.addEventListener('pointercancel', this.handleGestureEnd, true);
} else {
// Add Touch Listener
swipeFrontElement.addEventListener('touchstart', this.handleGestureStart, true);
swipeFrontElement.addEventListener('touchmove', this.handleGestureMove, true);
swipeFrontElement.addEventListener('touchend', this.handleGestureEnd, true);
swipeFrontElement.addEventListener('touchcancel', this.handleGestureEnd, true);
// Add Mouse Listener
swipeFrontElement.addEventListener('mousedown', this.handleGestureStart, true);
}
단일 요소 상호작용 처리
위의 간단한 코드 스니펫에는 마우스 이벤트의 시작 이벤트 리스너만 추가되었습니다. 그 이유는, 이벤트 리스너가 추가된 요소 위를 커서로 가리킬 때만 마우스 이벤트가 트리거되기 때문입니다.
TouchEvents
는 터치가 발생한 위치에 관계없이 동작이 시작된 후 동작을 추적하며, PointerEvents
는 DOM 요소에서 setPointerCapture
를 호출한 후 터치가 발생한 위치에 관계없이 이벤트를 추적합니다.
마우스 이동 및 종료 이벤트의 경우, 동작 시작 메서드 안에 이벤트 리스너를 추가하고 이 리스너를 문서에 추가합니다. 즉, 동작이 완료될 때까지 커서를 추적할 수 있습니다.
구현 단계는 다음과 같습니다.
- 모든 TouchEvent 및 PointerEvent 리스너를 추가합니다. MouseEvents의 경우 시작 이벤트만 추가합니다.
- 시작 동작 콜백 내에서 마우스 이동 및 종료 이벤트를 문서에 바인딩합니다. 이렇게 하면 이벤트가 원래 요소에서 발생하는지와 관계없이 모든 마우스 이벤트가 수신됩니다. PointerEvents의 경우 추가적인 모든 이벤트를 수신하려면 원래 요소에서
setPointerCapture()
를 호출해야 합니다. 그런 다음 동작 시작을 처리합니다. - 이동 이벤트를 처리합니다.
- 종료 이벤트에서 마우스 이동 및 종료 리스너를 문서에서 삭제하고 동작을 종료합니다.
다음은 이동 및 종료 이벤트를 문서에 추가하는 handleGestureStart()
메서드의 스니펫입니다.
// Handle the start of gestures
this.handleGestureStart = function(evt) {
evt.preventDefault();
if(evt.touches && evt.touches.length > 1) {
return;
}
// Add the move and end listeners
if (window.PointerEvent) {
evt.target.setPointerCapture(evt.pointerId);
} else {
// Add Mouse Listeners
document.addEventListener('mousemove', this.handleGestureMove, true);
document.addEventListener('mouseup', this.handleGestureEnd, true);
}
initialTouchPos = getGesturePointFromEvent(evt);
swipeFrontElement.style.transition = 'initial';
}.bind(this);
추가하는 종료 콜백은 handleGestureEnd()
입니다. 이 콜백은 이동 및 종료 이벤트 리스너를 문서에서 삭제하고 동작이 다음과 같이 완료되면 포인터 캡처를 해제합니다.
// Handle end gestures
this.handleGestureEnd = function(evt) {
evt.preventDefault();
if (evt.touches && evt.touches.length > 0) {
return;
}
rafPending = false;
// Remove Event Listeners
if (window.PointerEvent) {
evt.target.releasePointerCapture(evt.pointerId);
} else {
// Remove Mouse Listeners
document.removeEventListener('mousemove', this.handleGestureMove, true);
document.removeEventListener('mouseup', this.handleGestureEnd, true);
}
updateSwipeRestPosition();
initialTouchPos = null;
}.bind(this);
문서에 이동 이벤트를 추가하는 이 패턴을 따르면 사용자가 요소와 상호작용하기 시작하고 동작을 요소 외부로 이동해도 이벤트가 문서에서 수신되므로 페이지의 위치와 관계없이 마우스 움직임을 계속 가져올 수 있습니다.
이 다이어그램은 동작이 시작된 후에 이동 및 종료 이벤트를 문서에 추가할 때 터치 이벤트가 무엇을 수행 중인지 보여줍니다.
효율적으로 터치에 반응
이제 시작 및 종료 이벤트를 처리했으므로 실제로 터치 이벤트에 반응할 수 있습니다.
모든 시작 및 이동 이벤트에 대해 여러분은 x
및 y
를 이벤트로부터 쉽게 추출할 수 있습니다.
다음 예에서는 targetTouches
의 존재 여부를 확인하여 이벤트가 TouchEvent
에서 발생했는지 확인합니다. 그런 경우에는 첫 번째 터치에서 clientX
와 clientY
를 추출합니다.
이벤트가 PointerEvent
또는 MouseEvent
인 경우 이벤트 자체에서 clientX
및 clientY
를 직접 추출합니다.
function getGesturePointFromEvent(evt) {
var point = {};
if (evt.targetTouches) {
// Prefer Touch Events
point.x = evt.targetTouches[0].clientX;
point.y = evt.targetTouches[0].clientY;
} else {
// Either Mouse event or Pointer Event
point.x = evt.clientX;
point.y = evt.clientY;
}
return point;
}
TouchEvent
에는 터치 데이터가 포함된 세 개의 목록이 있습니다.
touches
: 화면의 모든 현재 터치 목록(터치가 어떤 DOM 요소에 있는지는 상관없음).targetTouches
: 이벤트가 바인딩된 DOM 요소에 현재 있는 터치 목록입니다.changedTouches
: 변경될 경우 이벤트를 발생시키는 터치 목록입니다.
대부분의 경우 targetTouches
는 개발자가 필요로 하는 모든 기능을 제공합니다. (이들 목록에 대한 자세한 내용은 터치 목록을 참고하세요).
requestAnimationFrame 사용
이벤트 콜백은 메인 스레드에서 발생하므로 프레임 속도를 높게 유지하고 버벅거림 현상을 방지하기 위해 이벤트 콜백에서 코드를 최대한 적게 실행해야 합니다.
requestAnimationFrame()
를 사용하면 브라우저가 프레임을 그리기 직전에 UI를 업데이트할 수 있는 기회가 있으며, 이벤트 콜백에서 일부 작업을 이동하는 데 도움이 됩니다.
requestAnimationFrame()
에 대해 잘 모르는 경우 여기에서 자세히 알아보세요.
일반적인 구현에서는 시작 및 이동 이벤트에서 x
및 y
좌표를 저장하고 이동 이벤트 콜백 내에서 애니메이션 프레임을 요청합니다.
이 데모에서는 초기 터치 위치를 handleGestureStart()
에 저장합니다(initialTouchPos
검색).
// Handle the start of gestures
this.handleGestureStart = function(evt) {
evt.preventDefault();
if (evt.touches && evt.touches.length > 1) {
return;
}
// Add the move and end listeners
if (window.PointerEvent) {
evt.target.setPointerCapture(evt.pointerId);
} else {
// Add Mouse Listeners
document.addEventListener('mousemove', this.handleGestureMove, true);
document.addEventListener('mouseup', this.handleGestureEnd, true);
}
initialTouchPos = getGesturePointFromEvent(evt);
swipeFrontElement.style.transition = 'initial';
}.bind(this);
handleGestureMove()
메서드는 애니메이션 프레임을 요청하기 전에 이벤트의 위치를 저장하고, onAnimFrame()
함수를 콜백으로 전달합니다.
this.handleGestureMove = function (evt) {
evt.preventDefault();
if (!initialTouchPos) {
return;
}
lastTouchPos = getGesturePointFromEvent(evt);
if (rafPending) {
return;
}
rafPending = true;
window.requestAnimFrame(onAnimFrame);
}.bind(this);
onAnimFrame
값은 호출 시 UI를 이동하도록 변경하는 함수입니다. 이 함수를 requestAnimationFrame()
에 전달하면
페이지를 업데이트(즉, 페이지에 변경사항을 적용하기 직전) 직전에
이 함수를 호출하도록 브라우저에 지시합니다.
handleGestureMove()
콜백에서 먼저 rafPending
이 false인지 확인합니다. 이는 마지막 이동 이벤트 이후에 requestAnimationFrame()
에 의해 onAnimFrame()
가 호출되었음을 나타냅니다. 즉, 실행을 기다리는 requestAnimationFrame()
는 어느 시점에서든 하나밖에 없습니다.
onAnimFrame()
콜백이 실행될 때, rafPending
을 false
로 업데이트하기에 앞서 이동하려는 모든 요소에 대해 변환을 설정합니다. 이렇게 하면 다음 터치 이벤트가 새 애니메이션 프레임을 요청할 수 있습니다.
function onAnimFrame() {
if (!rafPending) {
return;
}
var differenceInX = initialTouchPos.x - lastTouchPos.x;
var newXTransform = (currentXPosition - differenceInX)+'px';
var transformStyle = 'translateX('+newXTransform+')';
swipeFrontElement.style.webkitTransform = transformStyle;
swipeFrontElement.style.MozTransform = transformStyle;
swipeFrontElement.style.msTransform = transformStyle;
swipeFrontElement.style.transform = transformStyle;
rafPending = false;
}
터치 액션을 사용하여 동작 제어
CSS 속성 touch-action
를 사용하면 요소의 기본 터치 동작을 제어할 수 있습니다. 이 예시에서는 touch-action: none
를 사용하여 브라우저가 사용자의 터치로 아무것도 수행하지 못하도록 합니다. 이렇게 하면 모든 터치 이벤트를 가로챌 수 있습니다.
/* Pass all touches to javascript: */
button.custom-touch-logic {
touch-action: none;
}
touch-action: none
사용은 기본 브라우저 동작을 모두 방지하므로 다소 까다롭습니다. 대부분의 경우 아래 옵션 중 하나가 더 나은 해결책입니다.
touch-action
를 사용하면 브라우저에 의해 구현된 동작을 비활성화할 수 있습니다.
예를 들어 IE10 이상에서는 동작 확대/축소를 위해 두 번 탭을 지원합니다. touch-action
를 manipulation
로 설정하면 기본 두 번 탭 동작이 방지됩니다.
이렇게 하면 두 번 탭 동작을 직접 구현할 수 있습니다.
다음은 일반적으로 사용되는 touch-action
값의 목록입니다.
이전 버전의 IE 지원
IE10을 지원하려면 공급업체 접두사가 붙은 PointerEvents
버전을 처리해야 합니다.
PointerEvents
지원을 확인하려면 일반적으로 window.PointerEvent
를 찾지만 IE10에서는 window.navigator.msPointerEnabled
를 찾습니다.
공급업체 접두사가 붙은 이벤트 이름은 'MSPointerDown'
, 'MSPointerUp'
, 'MSPointerMove'
입니다.
아래 예에서는 지원 여부를 확인하고 이벤트 이름을 전환하는 방법을 보여줍니다.
var pointerDownName = 'pointerdown';
var pointerUpName = 'pointerup';
var pointerMoveName = 'pointermove';
if (window.navigator.msPointerEnabled) {
pointerDownName = 'MSPointerDown';
pointerUpName = 'MSPointerUp';
pointerMoveName = 'MSPointerMove';
}
// Simple way to check if some form of pointerevents is enabled or not
window.PointerEventsSupport = false;
if (window.PointerEvent || window.navigator.msPointerEnabled) {
window.PointerEventsSupport = true;
}
자세한 내용은 Microsoft의 업데이트 문서를 확인하세요.
참조
터치 상태를 나타내는 의사 클래스
터치 이벤트 최종 참조는 W3C 터치 이벤트에서 확인할 수 있습니다.
터치, 마우스, 포인터 이벤트
이러한 이벤트는 새 동작을 애플리케이션에 추가하기 위한 기본 요소입니다.
터치 목록
각 터치 이벤트에는 세 개의 목록 속성이 포함됩니다.
iOS에서 활성 상태 지원 사용 설정
iOS용 Safari에서는 기본적으로 active 상태가 적용되지 않습니다.
작동하게 하려면 touchstart
이벤트 리스너를 문서 본문 또는 각 요소에
추가해야 합니다.
iOS 기기에서만 실행되도록 하려면 사용자 에이전트 테스트 후에 이 작업을 수행해야 합니다.
touchstart를 본문에 추가하면 DOM의 모든 요소에 적용되는 이점이 있습니다. 그러나 이 경우 페이지를 스크롤할 때 성능 문제가 발생할 수도 있습니다.
window.onload = function() {
if (/iP(hone|ad)/.test(window.navigator.userAgent)) {
document.body.addEventListener('touchstart', function() {}, false);
}
};
대안은 페이지에서 상호작용 가능한 모든 요소에 터치 시작 리스너를 추가하여 성능 문제를 완화하는 것입니다.
window.onload = function() {
if (/iP(hone|ad)/.test(window.navigator.userAgent)) {
var elements = document.querySelectorAll('button');
var emptyFunction = function() {};
for (var i = 0; i < elements.length; i++) {
elements[i].addEventListener('touchstart', emptyFunction, false);
}
}
};