나아갈 방향을 가리키기

Sérgio Gomes

웹에서 항목을 가리키는 것은 예전에는 간단했습니다. 마우스를 가지고 움직이거나 버튼을 누르는 정도였습니다. 마우스가 아닌 모든 것이 마우스로 에뮬레이션되었으며 개발자는 무엇을 기대할 수 있는지 정확히 알고 있었습니다.

단순하다고 해서 반드시 좋은 것은 아닙니다. 시간이 지남에 따라 모든 것이 마우스가 아니거나 마우스인 척하는 것이 점점 더 중요해졌습니다. 압력 감지 및 기울기 감지 펜을 사용하면 놀라운 창의적 자유를 누릴 수 있었습니다. 손가락을 사용할 수 있으므로 기기와 손만 있으면 되었습니다. 한 손가락 이상을 사용하는 것도 좋습니다.

이를 지원하기 위해 한동안 터치 이벤트가 있었지만, 터치 전용의 완전히 별개의 API이므로 마우스와 터치를 모두 지원하려면 두 개의 별도 이벤트 모델을 코딩해야 합니다. Chrome 55에는 두 모델을 통합하는 최신 표준인 포인터 이벤트가 제공됩니다.

단일 이벤트 모델

포인터 이벤트는 브라우저의 포인터 입력 모델을 통합하여 터치, 펜, 마우스를 단일 이벤트 세트로 통합합니다. 예를 들면 다음과 같습니다.

document.addEventListener('pointermove',
    ev => console.log('The pointer moved.'));
foo.addEventListener('pointerover',
    ev => console.log('The pointer is now over foo.'));

다음은 사용 가능한 모든 이벤트 목록입니다. 마우스 이벤트에 익숙한 경우 매우 익숙하게 보일 것입니다.

pointerover 포인터가 요소의 경계 상자에 들어갔습니다. 마우스 오버를 지원하는 기기에서는 즉시, 지원하지 않는 기기에서는 pointerdown 이벤트 전에 발생합니다.
pointerenter pointerover와 유사하지만 버블링되지 않으며 자손을 다르게 처리합니다. 사양에 관한 세부정보
pointerdown 입력 기기의 시맨틱에 따라 버튼이 눌리거나 접촉이 설정된 상태에서 포인터가 활성 버튼 상태로 전환되었습니다.
pointermove 포인터의 위치가 변경되었습니다.
pointerup 포인터가 활성 버튼 상태를 벗어났습니다.
pointercancel 포인터가 더 이상 이벤트를 내보낼 가능성이 낮음을 의미하는 상황이 발생했습니다. 즉, 진행 중인 작업을 취소하고 중립 입력 상태로 돌아가야 합니다.
pointerout 포인터가 요소 또는 화면의 경계 상자를 벗어났습니다. 기기가 마우스 오버를 지원하지 않는 경우 pointerup 뒤에도 적용됩니다.
pointerleave pointerout와 유사하지만 버블링되지 않으며 자손을 다르게 처리합니다. 사양에 관한 세부정보
gotpointercapture 요소가 포인터 캡처를 수신했습니다.
lostpointercapture 캡처 중인 포인터가 해제되었습니다.

다양한 입력 유형

일반적으로 포인터 이벤트를 사용하면 다양한 입력 장치에 대해 별도의 이벤트 핸들러를 등록하지 않고도 입력과 관계없는 방식으로 코드를 작성할 수 있습니다. 물론 마우스 오버 개념이 적용되는지와 같은 입력 유형 간의 차이점은 여전히 유의해야 합니다. 서로 다른 입력 기기 유형을 구분하려면(예: 서로 다른 입력에 별도의 코드/기능 제공) 동일한 이벤트 핸들러 내에서 PointerEvent 인터페이스의 pointerType 속성을 사용하면 됩니다. 예를 들어 측면 탐색 창을 코딩하는 경우 pointermove 이벤트에 다음 로직을 사용할 수 있습니다.

switch(ev.pointerType) {
    case 'mouse':
    // Do nothing.
    break;
    case 'touch':
    // Allow drag gesture.
    break;
    case 'pen':
    // Also allow drag gesture.
    break;
    default:
    // Getting an empty string means the browser doesn't know
    // what device type it is. Let's assume mouse and do nothing.
    break;
}

기본 작업

터치 지원 브라우저에서는 특정 동작을 사용하여 페이지를 스크롤하거나, 확대/축소하거나, 새로고침합니다. 터치 이벤트의 경우 이러한 기본 작업이 실행되는 동안에도 이벤트가 계속 수신됩니다. 예를 들어 사용자가 스크롤하는 동안 touchmove가 계속 실행됩니다.

포인터 이벤트를 사용하면 스크롤이나 확대/축소와 같은 기본 작업이 트리거될 때마다 브라우저가 포인터를 제어했음을 알리는 pointercancel 이벤트가 발생합니다. 예를 들면 다음과 같습니다.

document.addEventListener('pointercancel',
    ev => console.log('Go home, the browser is in charge now.'));

기본 제공 속도: 이 모델은 동일한 수준의 응답성을 달성하기 위해 수동 이벤트 리스너를 사용해야 하는 터치 이벤트에 비해 기본적으로 더 나은 성능을 제공합니다.

touch-action CSS 속성을 사용하여 브라우저가 제어하지 못하도록 할 수 있습니다. 요소에서 이 속성을 none로 설정하면 해당 요소에서 시작된 모든 브라우저 정의 작업이 사용 중지됩니다. 하지만 브라우저가 x축의 움직임에는 반응하지만 y축의 움직임에는 반응하지 않도록 허용하는 pan-x와 같이 더 세부적인 제어를 위한 다른 값도 많이 있습니다. Chrome 55는 다음 값을 지원합니다.

auto 기본값입니다. 브라우저에서 모든 기본 작업을 실행할 수 있습니다.
none 브라우저는 기본 작업을 실행할 수 없습니다.
pan-x 브라우저는 가로 스크롤 기본 작업만 실행할 수 있습니다.
pan-y 브라우저는 세로 스크롤 기본 작업만 실행할 수 있습니다.
pan-left 브라우저는 가로 스크롤 기본 작업만 실행하고 페이지를 왼쪽으로만 이동할 수 있습니다.
pan-right 브라우저는 가로 스크롤 기본 작업만 실행하고 페이지를 오른쪽으로만 화면 이동할 수 있습니다.
pan-up 브라우저는 세로 스크롤 기본 작업만 실행하고 페이지를 위로만 이동할 수 있습니다.
pan-down 브라우저는 세로 스크롤 기본 작업만 실행하고 페이지를 아래로만 이동할 수 있습니다.
manipulation 브라우저는 스크롤 및 확대/축소 작업만 실행할 수 있습니다.

포인터 캡처

사용자가 클릭 타겟 외부의 버튼을 놓아서 mouseup 이벤트가 손상되었다는 사실을 깨닫기까지 한 시간 동안 짜증스러운 버그를 디버깅한 적이 있나요? 없으시다면 알겠습니다. 저만 그런 것일 수도 있습니다.

하지만 지금까지 이 문제를 해결할 만한 좋은 방법은 없었습니다. 물론 문서에 mouseup 핸들러를 설정하고 애플리케이션에 상태를 저장하여 항목을 추적할 수 있습니다. 하지만 이는 깔끔한 해결 방법은 아닙니다. 특히 웹 구성요소를 빌드하고 모든 것을 깔끔하게 격리하려는 경우 더욱 그렇습니다.

포인터 이벤트를 사용하면 훨씬 더 나은 솔루션을 사용할 수 있습니다. 포인터를 캡처하여 pointerup 이벤트 (또는 포착하기 어려운 다른 이벤트)를 확실히 받을 수 있습니다.

const foo = document.querySelector('#foo');
foo.addEventListener('pointerdown', ev => {
    console.log('Button down, capturing!');
    // Every pointer has an ID, which you can read from the event.
    foo.setPointerCapture(ev.pointerId);
});

foo.addEventListener('pointerup', 
    ev => console.log('Button up. Every time!'));

브라우저 지원

이 글을 작성하는 시점에서 포인터 이벤트는 Internet Explorer 11, Microsoft Edge, Chrome, Opera에서 지원되며 Firefox에서는 부분적으로 지원됩니다. caniuse.com에서 최신 목록을 확인할 수 있습니다.

포인터 이벤트 폴리필을 사용하여 공백을 메울 수 있습니다. 또는 런타임 시 브라우저 지원을 확인하는 것도 간단합니다.

if (window.PointerEvent) {
    // Yay, we can use pointer events!
} else {
    // Back to mouse and touch events, I guess.
}

포인터 이벤트는 점진적 개선을 위한 훌륭한 후보입니다. 초기화 메서드를 수정하여 위의 검사를 실행하고, if 블록에 포인터 이벤트 핸들러를 추가하고, 마우스/터치 이벤트 핸들러를 else 블록으로 이동하기만 하면 됩니다.

사용해 보고 의견을 알려주세요.