터치 디바이스, 모바일에서 hover 제거하기

Featured image

마크업 작업을 하다보면 엘리먼트에 :hover 가상 클래스를 사용한다. :hover는 사용자의 마우스 포인터가 해당 엘리먼트 위에 올라가 있으면 속성 값을 변경하는 가상 클래스로 주로 버튼이나 리스트등에 사용자와 상호작용하기 위해 사용된다. 그러나 모바일 디바이스, 즉 마우스가 아닌 터치로 동작하는 장치에선 해당 가상 클래스가 문제가 된다.

모바일 디바이스의 :hover

MDN의 :hover 페이지에 있는 예제를 PC 브라우저와 모바일 브라우저에서의 동작을 비교해보자. a 태그에 마우스 포인터가 올라가 있으면 배경색을 gold로 변경하는 코드이다.

a:hover {
  background-color: gold;
}

PC hover

PC 브라우저에선 원하는 대로 정상적으로 동작한다.

Mobile hover

그러나 모바일 브라우저에선 원하는 대로 동작하지 않는다. 마우스 포인터가 없으니 :hover 클래스가 동작하지 않을 것 같지만, 클릭 이벤트가 발생할 경우 :hover에 선언한 배경 색상이 변경되고 있다. 그리고 다른 영역을 클릭하면 효과가 해제된다. 왜 이런 현상이 발생하는 걸까? 원인은 상호작용의 방식에 따른 차이이다.

모바일 기기는 마우스 대신 터치로 상호작용을 한다. 엘리먼트를 터치하게 되면 브라우저는 포인터가 계속 해당 엘리먼트에 위에 머물고 있다고 인식한다. 따라서 다른 곳을 터치해 상호작용을 다시 발생시키지 않는 이상 :hover에 효과가 사라지지 않는 것이다. 반면에 PC는 마우스가 계속 움직이면서 상호 작용을 일으키기에 정상적으로 동작하는 것이다.

해결 방법

CSS의 미디어 쿼리 이용하기

미디어쿼리 Level 4 사양에는 상당수의 최신 브라우저를 지원하지만, 몇몇 기능들은 잘 지원되지 않으니 참고해야한다.

hover

a:hover { /* default hover effect */
  color: black;
  background: yellow;
}

@media (hover: hover) { /* when supported */
  a:hover {
    color: white;
    background: black;
  }
}

해결 방법 검색 시에 자주 나오는 Media Queries Level 4에 포함된 @media hover을 이용하는 방법이다. 실제로 개발자 도구의 Device toolbar를 이용하면 잘 동작하는 것 처럼 보인다.

Differ iOS / Android

확인해보기: https://codepen.io/michellebarker/pen/jOmNbgW

그러나 일부 Android 버전에는 길게 누를 때 호버링을 에뮬레이트하는 기능이 있어 hover를 지원하는 것으로 판단한다. 따라서 같은 코드라도 위와 같이 상이한 결과가 나타난다. 모든 사용자에게 동일한 UX를 제공하려면 두 번째 조건을 추가해야한다.

pointer

@media pointer는 장치에 포인터의 존재 여부와 포인팅 장치의 정확도를 확인한다.

@media (hover: hover) and (pointer: fine) { /* when supported */
  a:hover {
    color: white;
    background: black;
  }
}

hover를 지원하면서, pointer는 마우스와 같은 정확한 포인터 장치를 지원하는 장치에서만 :hover가 동작되도록하는 코드이다. 아래 링크에서 PC와 모바일 디바이스에서 확인해보면 결과가 다른 것을 볼 수 있다.

확인해보기: https://codepen.io/ykwan0714/pen/oNEoxWq

Javascript를 통한 모바일 디바이스 구분하기 (터치 가능 디바이스)

미디어 쿼리로 구분하는 방법은 간편하지만, 구형 브라우저에선 제대로 동작하지 않을 위험이 있다. 이 경우엔 javascript로 디바이스를 판단하여 클래스를 추가하여 처리하면 된다.

const isTouchDevice = ('ontouchstart' in window)||
(navigator.maxTouchPoints > 0)||
(navigator.msMaxTouchPoints > 0));

if(!isTouchDevice) { // 모바일 디바이스가 아니면
  document.body.classList.add('can-hover'); // 상위에 클래스를 추가한다.
}

ontouchstart 이벤트는 모바일 기기에만 포함되어 있고, navigator.maxTouchPoints는 PC인 경우 터치가 불가능하므로 0으로 출력된다. 위에 나온 값들로 터치 가능 디바이스를 구분한다.

즉, 터치 디바이스가 아니면 특정 클래스 이름을 최상위에 추가하고, 해당 클래스의 자식일때만 hover 효과가 적용되게 하는 것이다.

.can-hover a:hover {
   color: white;
   background: black;
}

확인해보기: https://codepen.io/ykwan0714/pen/MWQOepr

scss와 함께 사용하기

@mixin canHover {
  @at-root .can-hover & {
    @content;
  }
}
/* ... */
a {
  @include canHover {
    &:hover { 
      color: white;
      background: black;
    }
  }
}

scss를 사용한다면 depth 때문에 적용이 어려움을 겪을 수 있다. 이때는 @mixin@at-root 을 와 함께 사용하면 된다.

@mixin canHover {
  @at-root .can-hover & {
    @content;
  }
}
/* ... */
a {
  @include canHover {
    &:hover { 
      color: white;
      background: black;
    }
  }
}

React와 함께 CSS Module을 사용한다면, :global(selector)를 이용하면 된다.

참고