마우스로 터치 스크롤 구현하기
Touch Scroll
모바일에서는 스크롤 가능한 요소를 터치 제스처를 통해 스크롤하면서 읽을 수 있습니다. 이를 데스크톱 환경에서도 마우스를 가지고 동일하게 동작할 수 있도록 만들 수 있는데, 웨이브나 카카오 이모티콘샵과 같은 서비스를 방문해 보면 이 기능이 어떤 것인지 확인할 수 있습니다. 모바일에서는 기본적으로 터치를 이용한 스크롤이 가능하지만, 데스크톱에서는 불가능하기 때문에 마우스, 터치, 클릭 이벤트에 대해 기능을 구현해야 합니다. 처음에는 크게 헤맸지만 만들고 보니 크게 어렵지 않더라고요.
그럼 함께 만들어 볼까요? 😆
구현할 내용은 모두 자바스크립트이기 때문에, HTML과 CSS 코드는 자유롭게 작성하셔도 됩니다. 리스트 요소를 수평 일렬로 표현하기만 하면 되는데, 만약 제가 만든 코드를 그대로 따라 하고 싶으시면 다음을 참고해 주세요!
HTML
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<title>Touch Scroll</title>
<!-- Styles -->
<link rel="stylesheet" href="style.css" />
</head>
<body>
<ul class="list">
<li class="item">
<a class="link" href="#">
<img class="image" src="./images/1.jpeg" alt="첫 번째 별나비" />
</a>
</li>
<li class="item">
<a class="link" href="#">
<img class="image" src="./images/2.png" alt="두 번째 별나비" />
</a>
</li>
<li class="item">
<a class="link" href="#">
<img class="image" src="./images/3.jpeg" alt="세 번째 별나비" />
</a>
</li>
<li class="item">
<a class="link" href="#">
<img class="image" src="./images/4.jpeg" alt="네 번째 별나비" />
</a>
</li>
<li class="item">
<a class="link" href="#">
<img class="image" src="./images/5.jpeg" alt="다섯 번째 별나비" />
</a>
</li>
<li class="item">
<a class="link" href="#">
<img class="image" src="./images/6.jpeg" alt="여섯 번째 별나비" />
</a>
</li>
</ul>
<!-- Scripts -->
<script src="app.js"></script>
</body>
</html>
CSS
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
width: 100%;
padding: 4rem 0;
overflow: hidden;
}
.list {
padding: 1rem 0;
width: 100%;
display: flex;
transform: translate(0, 0);
}
.item {
padding-right: 1rem;
list-style: none;
user-select: none;
}
.item:first-child {
padding-left: 1rem;
}
.link {
display: block;
-webkit-user-drag: none;
}
.image {
display: block;
width: 200px;
height: 200px;
-webkit-user-drag: none;
}
마크업 & 스타일링 결과
그럼 오늘도 예제를 위해 열일하시는 별나비 양과 함께 코딩 시작해 봅시다! ✨
Coding Start
필요한 변수 선언
예제에서 사용할 변수는 그렇게 많지 않습니다.
// 요소 & 사이즈
const list = document.querySelector('.list');
const listScrollWidth = list.scrollWidth;
const listClientWidth = list.clientWidth;
// 이벤트마다 갱신될 값
let startX = 0;
let nowX = 0;
let endX = 0;
let listX = 0;
scrollWidth와 clientWidth를 구한 이유는 리스트를 css translate 속성을 이용해 움직일 것이기 때문입니다. clientWidth는 가려진 영역은 제외한 현재 화면에 보이는 요소에 대한 가로 사이즈이고, scrollWidth는 가려진 영역(스크롤 영역)을 포함한 요소의 가로 사이즈입니다. 그렇기 때문에 'scrollWidth - clientWidth'의 결과는 translateX로 요소를 이동시킬 수 있는 최대치라 볼 수 있습니다.
이벤트 핸들러 선언
간단한 터치 스크롤을 구현하는 예제에서는 다음 4가지의 이벤트에 대해서만 처리하면 됩니다.
const onScrollStart = (e) => {};
const onScrollMove = (e) => {};
const onScrollEnd = (e) => {};
const onClick = (e) => {};
유틸 함수 정의
코드에서 재사용되는 부분은 유틸 함수로 만들어 간편하게 사용할 수 있도록 했습니다.
const getClientX = (e) => {
const isTouches = e.touches ? true : false;
return isTouches ? e.touches[0].clientX : e.clientX;
};
const getTranslateX = () => {
return parseInt(getComputedStyle(list).transform.split(/[^\-0-9]+/g)[5]);
};
const setTranslateX = (x) => {
list.style.transform = `translateX(${x}px)`;
};
getClientX
마우스로 클릭한 지점의 X좌표는 e.clientX로 얻을 수 있지만, 터치 이벤트가 발생한 경우에는 e.touches[0].clientX를 참조해야 합니다.
getTranslateX
최초 스크롤은 상관없지만, 두 번째 스크롤부터는 스크롤이 종료된 위치도 고려하여 계산해야 하기 때문에 요소의 translateX 위치를 가져와야 합니다. window 객체에 내장된 API인 getComputedStyle 메서드를 사용하면 요소가 가진 CSS의 속성 값을 얻을 수 있는데, transform의 경우 x, y, z의 값을 모두 반환하므로 정규표현식을 통해 필요한 x의 값만 얻도록 했습니다.
setTranslateX
스크롤 됨에 따라 요소의 위치를 조정해야 하기 때문에, 간편하게 함수로 만들어 재사용했습니다.
이벤트 연결
웹사이트가 열림과 동시에 이벤트를 연결해야 하는데, 처음에는 클릭과 관련된 이벤트만 연결합니다.
const bindEvents = () => {
list.addEventListener('mousedown', onScrollStart);
list.addEventListener('touchstart', onScrollStart);
list.addEventListener('click', onClick);
};
bindEvents();
스크롤 시작 이벤트 구현
스크롤을 시작하게 되면 마우스 또는 터치한 지점을 startX 변수에 저장하고, 나머지 이벤트를 마저 등록합니다.
const onScrollStart = (e) => {
startX = getClientX(e);
window.addEventListener('mousemove', onScrollMove);
window.addEventListener('touchmove', onScrollMove);
window.addEventListener('mouseup', onScrollEnd);
window.addEventListener('touchend', onScrollEnd);
};
스크롤 진행 이벤트 구현
스크롤 중에도 계속해서 현재 마우스 포인터가 위치하는 지점에 대한 X 좌표를 nowX 변수에 저장하면서, 시작 지점이 저장된 nowX와의 값의 차를 요소의 translateX 속성 값으로 사용합니다. translateX의 값이 양수면 오른쪽으로, 음수라면 왼쪽으로 이동함을 유의해야 합니다.
const onScrollMove = (e) => {
nowX = getClientX(e);
setTranslateX(listX + nowX - startX);
};
listX 변수는 최초 스크롤 시에는 필요 없지만, 요소를 이동한 후의 두 번째 스크롤부터 필요합니다. listX 변수에는 요소의 translateX 값이 저장되어 있으며, 스크롤 종료 이벤트 함수 안에서 할당합니다. 만약 이 값을 함께 계산하지 않는다면, 요소가 다음과 같이 매 순간 최초 위치(tarnslateX: 0)에서 시작될 것입니다.
여기까지 구현하셨다면 다음의 결과를 얻을 수 있습니다.
하지만 마우스를 떼더라도 스크롤링이 이어질 텐데, 이는 마지막 스크롤 종료 이벤트에서 처리하면 됩니다.
스크롤 종료 이벤트 구현
스크롤 종료를 담당하는 onScrollEnd 이벤트 핸들러에서는 리스트 요소가 정해진 범위를 벗어나면 보정해 주고, 모든 이벤트를 제거하는 역할을 담당합니다. 범위를 벗어나면 유효 범위로 자연스럽게 돌아올 수 있도록 애니메이션을 부여했습니다. 그리고 애니메이션이 300ms 동안 지속되기 때문에, 제거한 이벤트를 300ms 이후 다시 바인딩할 수 있도록 setTimeout 함수를 사용했습니다.
const onScrollEnd = (e) => {
endX = getClientX(e);
listX = getTranslateX();
if (listX > 0) {
setTranslateX(0);
list.style.transition = `all 0.3s ease`;
listX = 0;
} else if (listX < listClientWidth - listScrollWidth) {
setTranslateX(listClientWidth - listScrollWidth);
list.style.transition = `all 0.3s ease`;
listX = listClientWidth - listScrollWidth;
}
window.removeEventListener('mousedown', onScrollStart);
window.removeEventListener('touchstart', onScrollStart);
window.removeEventListener('mousemove', onScrollMove);
window.removeEventListener('touchmove', onScrollMove);
window.removeEventListener('mouseup', onScrollEnd);
window.removeEventListener('touchend', onScrollEnd);
window.removeEventListener('click', onClick);
setTimeout(() => {
bindEvents();
list.style.transition = '';
}, 300);
};
여기까지 구현하면 기능적인 구현은 끝이기에, 결과물은 최종 결과에서 확인하시면 됩니다! 😆
클릭 이벤트 구현
마우스나 터치 이벤트가 발생한 후에는 클릭 이벤트가 추가로 발생합니다. 그렇기 때문에 스크롤링을 하지 않은 경우에만 클릭 이벤트가 발생하도록 처리해야 합니다. 저는 스크롤을 하지 않은 경우에만, 이미지에 걸린 링크로 이동할 수 있도록 다음과 같이 처리했습니다.
const onClick = (e) => {
if (startX - endX !== 0) {
e.preventDefault();
}
};
만약 터치를 시작한 지점(startX)과 터치를 종료한 지점(endX)의 값에 차이가 있다면 스크롤을 조금이라도 했다는 것이기 때문에, 클릭된 요소의 기본 동작을 막았습니다. 이벤트 객체(e)에 있는 preventDefault 메소드를 사용하면, 요소의 기본 동작을 막을 수 있습니다.
최종 결과
이렇게 터치 스크롤 구현을 완료했습니다 👏
하지만 코드는 좀 더 보완해야 하는데요. 브라우저 창의 크기를 조절(Resize)하면 올바르게 동작하지 않을 테고, 어떤 분들은 모바일처럼 작은 사이즈일 때만 터치 스크롤이 동작하도록 하고 싶을 수 있습니다. 이는 window 객체에 resize 이벤트를 연결하여 처리하면 되는데, 크게 어렵지 않으니 직접 구현해 보시면 좋을 것 같습니다!
예제에 사용된 코드는 Git 저장소에서 확인 가능합니다 ❤️
Referenes
'🌈 기술스택 > JavaScript' 카테고리의 다른 글
간단한 페이지네이션 구현하기 (0) | 2022.01.07 |
---|---|
Intersection Observer API로 무한 스크롤 구현하기 (0) | 2022.01.04 |
비동기 처리를 위한 문법 (Promise) (0) | 2021.10.08 |
자바스크립트에 동시성을 부여하는 이벤트 루프 (0) | 2021.10.07 |
연속으로 발생하는 이벤트를 제어하는 방법 (0) | 2021.10.07 |
댓글
이 글 공유하기
다른 글
-
간단한 페이지네이션 구현하기
간단한 페이지네이션 구현하기
2022.01.07 -
Intersection Observer API로 무한 스크롤 구현하기
Intersection Observer API로 무한 스크롤 구현하기
2022.01.04 -
비동기 처리를 위한 문법 (Promise)
비동기 처리를 위한 문법 (Promise)
2021.10.08 -
자바스크립트에 동시성을 부여하는 이벤트 루프
자바스크립트에 동시성을 부여하는 이벤트 루프
2021.10.07