슬라이스쇼에서 슬라이드를 움직이는 방법은 버튼과 하단 포인터 뿐만이 아니다. 마우스를 이용한 드래그도 방법으로 사용될 수 있다. 구현이 쉽지 않을 것이라고 생각되는 일이라 반쯤은 호기심으로 기능 구현을 시도해보았다.
내가 처음부터 모든 기능을 다 구현할 수는 없었고 외부에서 이렇게 하면 된다라는 자료를 찾아서 기능 동작 방식을 공부해보는 방법으로 기능을 구현하였다.
const [mouseDownClientX, setMouseDownClientX] = useState(0);
const [mouseDownClientY, setMouseDownClientY] = useState(0);
const [mouseUpClientX, setMouseUpClientX] = useState(0);
const [mouseUpClientY, setMouseUpClientY] = useState(0);
우선 필요한 것은 마우스를 클릭 했을 때, 그리고 클릭이 해제되었을 때 화면상의 좌표값 데이터이다. 이들은 고정되지 않고 변화하기 때문에 useState로 관리하여준다.
const onMouseDown = (event) => {
setMouseDownClientX(event.clientX);
setMouseDownClientY(event.clientY);
};
다음은 마우스를 클릭하였을 때 동작할 함수. 이 함수는 마우스를 클릭 했을 때 화면상의 좌표값을 받아와서 setState해주는 역할을 수행한다.
const onMouseUp = (event) => {
setMouseUpClientX(event.clientX);
setMouseUpClientY(event.clientY);
const dragSpaceX = Math.abs(mouseDownClientX - mouseUpClientX);
const dragSpaceY = Math.abs(mouseDownClientY - mouseUpClientY);
const vector = dragSpaceX / dragSpaceY;
if (mouseDownClientX !== 0 && dragSpaceX > 100 && vector > 2) {
if (mouseUpClientX < mouseDownClientX) {
showSlide(currentSlide - 1);
}
else if (mouseUpClientX > mouseDownClientX) {
showSlide(currentSlide + 1);
};
};
};
다음은 마우스를 클릭 해제했을 때 동작할 함수. 우선 수행되는 작업은 클릭이 해제된 시점에서의 화면상의 좌표값을 저장하는 것.
그리고 저장된 마우스를 클릭 했을 때, 그리고 클릭이 해제되었을 때 화면상의 좌표값 데이터를 가지고 다음과 같은 연산을 수행한다.
X, Y 좌표를 이용해 드래그한 거리(dragSpaceX, dragSpaceY).
X축 드래그 비율(vector).
이 연산의 결과값을 이용하여 이벤트를 발동시킬 조건으로 사용한다.
if (mouseDownClientX !== 0 && dragSpaceX > 100 && vector > 2)
우선 mouseDownClientX !== 0. 마우스가 클릭되었을 때 좌표값이 0이 아니어야 한다. 좌표값이 초기값에서 변화하지 않았다는 것은 마우스를 클릭하는 이벤트가 일어나지 않았다는 것이다. 따라서 첫 번째 조건은 사용자가 확실하게 클릭하였을 때 이벤트가 발동되도록 하는 안전장치에 해당한다.
다음 조건 dragSpaceX > 100은 클릭과 클릭 해제 이벤트 사이의 거리를 연산하여 수평 이동이라고 할 만한 이벤트가 발생한게 맞는지 확인하는 역할이다. 이 값을 연산하는데 사용된 Math 객체의 abs 함수는 절대값을 반환하는 것으로 결과값이 +던 -던 +의 값을 반환하도록 되어있다. 마우스 이벤트가 왼쪽으로 일어나든, 오른쪽으로 일어나든 이동이 이루어진 거리값만을 계산하여 결과값이 100보다 클 경우에만 사용자가 슬라이드 이벤트를 발동할 생각이 있다고 간주하여 이벤트를 실행시키게 된다.
마지막 조건 vector > 2. 이 조건은 드래그가 이루어진 방향이 진짜 수평 방향인지를 확인하기 위한 것이다. 만약 드래그가 직선 방향으로 수평이동하지 않고 상하 방향으로 수직이동 했다면? 정상적인 드래그 이벤트가 발동되었다고 할 수가 없으므로 슬라이스 이벤트도 실행되어서는 안된다.
이상의 3개 조건은 사용자가 정말 드래그 이벤트를 발동시켰는지를 검증하는 '안전장치'에 해당한다. 이들이 존재하지 않으면 사용자가 실수로 혹은 브라우저 내에서의 어떠한 이유로 클릭 행위가 이루어졌을 때 슬라이스 이벤트가 멋대로 발동될 수도 있기 때문이다.
다음은 정상적인 드래그 이벤트가 발동되었을 때 진짜 동작해야하는 기능, 슬라이드를 이동하는 기능이다.
if (mouseUpClientX < mouseDownClientX) {
showSlide(currentSlide - 1);
}
else if (mouseUpClientX > mouseDownClientX) {
showSlide(currentSlide + 1);
};
슬라이드는 이전 혹은 다음으로 이동해야한다. 이를 구분하는 방법은 클릭시 좌표와 클릭해제시 좌표의 값을 비교하는 것.
마우스를 클릭했을 때 해당 좌표값이, 클릭을 해제한 시점의 좌표의 값보다 클 경우.
드래그가 왼쪽으로 이루어졌을 경우에 해당한다. 이전 슬라이드가 보여지도록 하면 된다.
마우스를 클릭했을 때 해당 좌표값이, 클릭을 해제한 시점의 좌표의 값보다 작을 경우.
드래그가 오른쪽으로 이루어졌을 경우에 해당한다. 다음 슬라이드가 보여지도록 하면 된다.
프로젝트를 진행하면서 가장 많이 사용한 CSS 의사 클래스는 단연 hover이다. 어떤 DOM 위에 마우스 커서가 올라갈 경우 hover 클래스가 동작하여 작성한 CSS 속성을 적용할 수 있는 것인데, 브라우저에서는 문제없이 잘 동작했지만 모바일 환경에서는 제대로 발동되지 않았다. 모바일 환경에서는 마우스 커서에 대한 개념이 브라우저와 다르기 때문이다.
따라서 모바일 환경에서의 동작을 보장하기 위해서는 마우스 클릭 이벤트 이외에 다른 방식의 기능을 구현해주어야 한다. 모바일 환경에서 클릭 이벤트는 클릭이 아니라 화면 터치를 기반으로 이루어져야 한다.
const [tochedX, setTochedX] = useState(0);
const [tochedY, setTochedY] = useState(0);
const onTouchStart = (event) => {
setTochedX(event.changedTouches[0].pageX);
setTochedY(event.changedTouches[0].pageY);
};
const onTouchEnd = (event) => {
const distanceX = tochedX - event.changedTouches[0].pageX;
const distanceY = tochedY - event.changedTouches[0].pageY;
const vector = Math.abs(distanceX / distanceY);
if (distanceX > 30 && vector > 2) {
showSlide(currentSlide - 1);
}
else if (distanceX < -30 && vector > 2) {
showSlide(currentSlide + 1);
};
};
기본적인 구조는 클릭 이벤트과 거의 동일하다. 다만 터치 이벤트는 클릭 이벤트과 다른 점이 있어 동일한 방식으로 기능을 구현할 수가 없다.
따라서 이를 위해 터치 이벤트 객체에는 이벤트마다 changedTouches라는 배열이 존재하고, 여기에는 해당 터치에 대한 데이터들이 저장되어 있다. 이를 이용하면 다중 터치가 발생하였을 때 특정 터치 이벤트 하나만을 타겟으로 할 수 있다.
따라서 터치가 이루어졌을 때의 좌표값과 터치가 해제되었을 때 좌표값을 구하는 방법도 changedTouches를 이용해야 한다. 터치가 이루어졌을 때 좌표값을 onTouchStart 함수를 통해 저장하고, 터치가 해제되었을 때는 onTouchStart 함수로 저장한 터치 시작 좌표값과 터치 해제시의 좌표값을 이용해 연산하면 클릭 이벤트와 마찬가지로 사용자가 정말 드래그 이벤트를 발동시켰는지를 검증하는 '안전장치' 및 드래그 이벤트가 발동되었다는 기준을 만들어 기능을 동작하도록 할 수 있다.
클릭 이벤트와 터치 이벤트 모두 기본적인 기능 구현의 구조는 동일하다. 그런데 세부적인 부분에서 차이점을 보이고 있는데, 어떤 차이점이 있는지를 정리하고 그 이유를 알아보았다.
터치 이벤트에서는 왜 터치가 해제되었을 때 좌표값을 따로 관리하지 않는가?
const [mouseDownClientX, setMouseDownClientX] = useState(0);
const [mouseDownClientY, setMouseDownClientY] = useState(0);
const [mouseUpClientX, setMouseUpClientX] = useState(0);
const [mouseUpClientY, setMouseUpClientY] = useState(0);
const [tochedX, setTochedX] = useState(0);
const [tochedY, setTochedY] = useState(0);
-> 이건 바로 위에서 설명했다. 다중 실행이 가능한지 여부가 다른 클릭 이벤트와 터치 이벤트의 차이점 때문.
두 이벤트에서 이동 거리와 vector를 계산하는 방식이 다른 이유는?
const dragSpaceX = Math.abs(mouseDownClientX - mouseUpClientX);
const dragSpaceY = Math.abs(mouseDownClientY - mouseUpClientY);
const vector = dragSpaceX / dragSpaceY;
const distanceX = tochedX - event.changedTouches[0].pageX;
const distanceY = tochedY - event.changedTouches[0].pageY;
const vector = Math.abs(distanceX / distanceY);
이해하는데 어려움을 겪고 있지만.. 내용을 정리하자면 대충 다음과 같다.
터치 이벤트는 이벤트가 다중으로 발생할 수 있다. 동시에 여러 개의 터치 포인트를 처리할 수 있어야 한다. 터치 이벤트는 각 터치 포인트의 좌표를 추적하고 다루어야 하므로, 각 터치 포인트의 이동 거리를 개별적으로 계산해야한다. 따라서 클릭 이벤트와 마찬가지로 클릭이 해제되었을 때 좌표값을 정확하게, 명시적으로 어떻게 연산하면 된다고 선언할 수가 없다. 터치 이벤트에서 터치가 해제되었을 때의 좌표값은 터치가 시작된 지점을 기준으로 상대적으로 계산할 수 밖에 없다.
그렇기에 터치 이벤트가 어느 방향으로 발생되었는지 여부를 판별하는 방식도 다를 수 밖에 없다. 클릭 이벤트에서야 두 지점의 값을 정확하게 가져올 수 있으니 두 값을 계산해서 기능 구현에 사용하면 되지만, 터치 이벤트는 이벤트가 시작된 지점만을 기준으로 삼을 수 있어 동일한 구조의 기능 구현이 불가능하기 때문.
결국 터치 이벤트에서는 이벤트가 시작된 지점을 기준으로 이동 거리를 계산하는데, 이벤트 방향을 판별하는 기준이 필요하기 때문에 클릭 이벤트와 달리 abs() 함수를 사용하지 않는다. 결과값이 양수냐 음수냐에 따라서 방향을 판별해야하기 때문.
vector의 경우 수직으로 이동하는 것을 판별하기 위한 조건으로만 사용하면 되기에 음수를 허용하지 않는다. 클릭에서는 이동거리를 계산하는 시점에 abs() 함수를 사용했지만, 터치는 그렇지 않았으므로 터치 이벤트에서는 vector를 계산하는 시점에서 abs() 함수를 사용해주는 것이다.