사이트에 터치 추가

전화기에서 데스크톱 화면에 이르기까지 점점 더 많은 기기에서 터치스크린을 사용할 수 있습니다. 앱은 직관적이고 멋진 방식으로 터치스크린의 터치에 반응해야 합니다.

전화기에서 데스크톱 화면에 이르기까지 점점 더 많은 기기에서 터치스크린을 사용할 수 있습니다. 사용자가 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;
}

맞춤 동작 구현

사이트에 맞는 맞춤 상호작용과 동작을 구현할 경우 다음 두 가지 사항을 명심해야 합니다.

  1. 모든 브라우저를 지원하는 방법
  2. 프레임 속도를 높게 유지하는 방법

이 도움말에서는 모든 브라우저를 지원하는 데 필요한 API를 다루고 이러한 이벤트를 효율적으로 사용하는 방법을 다룹니다.

제스처가 어떤 동작을 수행하는지에 따라 사용자가 한 번에 하나의 요소와 상호작용하거나 또는 여러 요소와 동시에 상호작용할 수 있도록 해야 합니다.

이 문서의 두 가지 예시인 모든 브라우저를 지원하는 방법과 프레임 속도를 높게 유지하는 방법에 대해 살펴보겠습니다.

문서 터치를 보여주는 예시 GIF

첫 번째 예에서는 사용자가 하나의 요소와 상호작용할 수 있습니다. 이 경우 동작이 요소에서 처음 시작되었다면 모든 터치 이벤트를 이 요소에 지정할 수 있습니다. 예를 들어 스와이프 가능 요소에서 손가락을 떼더라도 여전히 이 요소를 제어할 수 있습니다.

이 기능은 뛰어난 유연성을 사용자에게 제공하지만 사용자가 UI와 상호작용할 수 있는 방식이 제한됩니다.

요소 터치에 대한 예시 GIF

하지만 사용자가 멀티 터치를 사용하여 동시에 여러 요소와 상호작용할 것으로 예상되는 경우 터치를 특정 요소로 제한해야 합니다.

이 방법은 사용자에게 더 많은 유연성을 제공하지만, UI 조작을 위한 로직이 복잡하며 사용자 오류에 대한 복원성이 떨어집니다.

이벤트 리스너 추가

Chrome (버전 55 이상), Internet Explorer, Edge에서 맞춤 동작을 구현하려면 PointerEvents를 사용하는 것이 좋습니다.

다른 브라우저에서는 TouchEventsMouseEvents가 올바른 방법입니다.

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를 호출한 후 터치가 발생한 위치에 관계없이 이벤트를 추적합니다.

마우스 이동 및 종료 이벤트의 경우, 동작 시작 메서드 안에 이벤트 리스너를 추가하고 이 리스너를 문서에 추가합니다. 즉, 동작이 완료될 때까지 커서를 추적할 수 있습니다.

구현 단계는 다음과 같습니다.

  1. 모든 TouchEvent 및 PointerEvent 리스너를 추가합니다. MouseEvents의 경우 시작 이벤트 추가합니다.
  2. 시작 동작 콜백 내에서 마우스 이동 및 종료 이벤트를 문서에 바인딩합니다. 이렇게 하면 이벤트가 원래 요소에서 발생하는지와 관계없이 모든 마우스 이벤트가 수신됩니다. PointerEvents의 경우 추가적인 모든 이벤트를 수신하려면 원래 요소에서 setPointerCapture()를 호출해야 합니다. 그런 다음 동작 시작을 처리합니다.
  3. 이동 이벤트를 처리합니다.
  4. 종료 이벤트에서 마우스 이동 및 종료 리스너를 문서에서 삭제하고 동작을 종료합니다.

다음은 이동 및 종료 이벤트를 문서에 추가하는 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);

직접 해 보기

문서에 이동 이벤트를 추가하는 이 패턴을 따르면 사용자가 요소와 상호작용하기 시작하고 동작을 요소 외부로 이동해도 이벤트가 문서에서 수신되므로 페이지의 위치와 관계없이 마우스 움직임을 계속 가져올 수 있습니다.

이 다이어그램은 동작이 시작된 후에 이동 및 종료 이벤트를 문서에 추가할 때 터치 이벤트가 무엇을 수행 중인지 보여줍니다.

`touchstart`에서 문서에 터치 이벤트를 바인딩하는 그림

효율적으로 터치에 반응

이제 시작 및 종료 이벤트를 처리했으므로 실제로 터치 이벤트에 반응할 수 있습니다.

모든 시작 및 이동 이벤트에 대해 여러분은 xy를 이벤트로부터 쉽게 추출할 수 있습니다.

다음 예에서는 targetTouches의 존재 여부를 확인하여 이벤트가 TouchEvent에서 발생했는지 확인합니다. 그런 경우에는 첫 번째 터치에서 clientXclientY를 추출합니다. 이벤트가 PointerEvent 또는 MouseEvent인 경우 이벤트 자체에서 clientXclientY를 직접 추출합니다.

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()에 대해 잘 모르는 경우 여기에서 자세히 알아보세요.

일반적인 구현에서는 시작 및 이동 이벤트에서 xy 좌표를 저장하고 이동 이벤트 콜백 내에서 애니메이션 프레임을 요청합니다.

이 데모에서는 초기 터치 위치를 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() 콜백이 실행될 때, rafPendingfalse로 업데이트하기에 앞서 이동하려는 모든 요소에 대해 변환을 설정합니다. 이렇게 하면 다음 터치 이벤트가 새 애니메이션 프레임을 요청할 수 있습니다.

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-actionmanipulation로 설정하면 기본 두 번 탭 동작이 방지됩니다.

이렇게 하면 두 번 탭 동작을 직접 구현할 수 있습니다.

다음은 일반적으로 사용되는 touch-action 값의 목록입니다.

터치 액션 매개변수
touch-action: none 브라우저에서 터치 상호작용을 처리하지 않습니다.
touch-action: pinch-zoom 브라우저에서 계속 처리하는 `pinch-zoom`을 제외하고 `touch-action: none`과 같은 모든 브라우저 상호작용을 사용 중지합니다.
touch-action: pan-y pinch-zoom 세로 스크롤 또는 핀치 확대/축소를 사용 중지하지 않고 JavaScript에서 가로 스크롤을 처리합니다 (예: 이미지 캐러셀).
touch-action: manipulation 브라우저의 클릭 지연을 방지하는 더블탭 동작을 사용 중지합니다. 스크롤과 핀치 줌을 브라우저에 맡깁니다.

이전 버전의 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의 업데이트 문서를 확인하세요.

참조

터치 상태를 나타내는 의사 클래스

클래스 설명
:hover
눌린 상태의 버튼
커서가 요소 위에 있을 때 이 상태로 진입합니다. 마우스로 가리킬 때 UI가 변경된다면 사용자가 요소와 상호작용하는 데 도움이 됩니다.
:focus
포커스 상태가 있는 버튼
사용자가 페이지의 요소를 탭할 때 이 상태로 진입합니다. 포커스 상태에서는 사용자가 현재 상호작용 중인 요소를 알 수 있으며, 또한 사용자가 키보드를 사용하여 쉽게 UI를 탐색할 수 있습니다.
:active
눌린 상태의 버튼
요소가 선택 중일 때 이 상태로 진입합니다(예: 사용자가 요소를 클릭하거나 터치하는 경우).

터치 이벤트 최종 참조는 W3C 터치 이벤트에서 확인할 수 있습니다.

터치, 마우스, 포인터 이벤트

이러한 이벤트는 새 동작을 애플리케이션에 추가하기 위한 기본 요소입니다.

터치, 마우스, 포인터 이벤트
touchstart, mousedown, pointerdown 손가락이 처음 요소를 터치하거나 사용자가 마우스를 아래로 클릭할 때 호출됩니다.
touchmove, mousemove, pointermove 이 메서드는 사용자가 화면을 가로질러 손가락을 움직이거나 마우스로 드래그할 때 호출됩니다.
touchend, mouseup, pointerup 이 메서드는 사용자가 손가락을 화면에서 떼거나 마우스에서 손을 떼면 호출됩니다.
touchcancel pointercancel 브라우저가 터치 동작을 취소할 때 호출됩니다. 예를 들어 사용자가 웹 앱을 터치한 다음 탭을 변경합니다.

터치 목록

각 터치 이벤트에는 세 개의 목록 속성이 포함됩니다.

터치 이벤트 속성
touches 화면의 모든 현재 터치 목록(어떤 요소가 터치 중인지는 상관없음).
targetTouches 현재 이벤트의 타겟인 요소에서 시작된 터치의 목록입니다. 예를 들어 <button>에 바인딩하면 현재 버튼에 있는 터치만 수신됩니다. 문서에 바인딩하면 문서의 모든 현재 터치가 나타납니다.
changedTouches 변경될 경우 이벤트를 발생시키는 터치 목록:
  • touchstart 이벤트의 경우 -- 현재 이벤트로 방금 활성화된 터치 포인트의 목록입니다.
  • touchmove 이벤트의 경우 -- 마지막 이벤트 이후로 이동된 터치 지점의 목록입니다.
  • touchend touchcancel 이벤트의 경우 - 표면에서 제거된 터치 지점의 목록

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);
    }
  }
};