8일 전
구현 환경 : Next.js 14, Tailwindcss, Typescript
터치 이벤트는 일반적으로 터치 스크린을 가진 디바이스에서 이용가능하다. 하지만, 터치 스크린을 갖는 디바이스를 포함한 모든 데스크탑 디바이스에서 터치 이벤트 API를 사용할 수 없다. 따라서 모바일 환경과 데스크탑 환경을 구분해야 한다.
Next.js 서버 컴포넌트에서 header
의 user-agent
값을 이용해서 모바일인지 확인하고, 클라이언트 컴포넌트에 props
로 isMobile
값을 전달한다.
import { headers } from "next/headers";
const header = headers();
const userAgent = header.get("user-agent");
// 크롬 개발자 도구에서 userAgent :
// Iphone 12 Pro -> Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) ...
// Galaxy S20 Ultra -> Mozilla/5.0 (Linux; Android 13; SM-G981B) ...
export const isMobile = (str: string) => {
let mobileRegex = /Android|iPhone|iPad|iPod|BlackBerry|Windows Phone/i;
return !!str.match(mobileRegex);
};
모바일 환경이면 터치 이벤트 객체를 이용하여 슬라이드를 구현하고, 데스크톱 환경이면 마우스 이벤트 객체를 이용하여 슬라이드를 구현한다.
const getPageX = (e: TouchEvent<HTMLDivElement> | MouseEvent<HTMLDivElement>) => {
if (isMobile) {
const touchEvent = e as TouchEvent<HTMLDivElement>;
return touchEvent.changedTouches[0].pageX;
}
const mouseEvent = e as MouseEvent<HTMLDivElement>;
return mouseEvent.pageX;
};
터치 이벤트를 기준으로 설명하겠다.
boxWidth
: 현재 브라우저 창의 크기에 대한 박스의 크기
startPageX
: 브라우저 창을 기준으로 한 터치가 시작된 절대적인 위치
curPageX
: 현재 페이지에 대한 터치가 시작된 상대적인 위치
scrollX
: 현재 포인터 위치에 따라 동적으로 스크롤하기 위한 값
isOnScroll
: 현재 스크롤(슬라이드) 중인지를 나타내는 boolean 값
isLongPressScroll
: 일정 시간 이상 꾹 눌렀는지 확인하는 boolean 값
startPageX
: offset으로 페이지 전환하기 위한 상태 변수
curPageX
: onTouchMove
이벤트가 발생할 때 scrollX
를 변경하기 위한 상태 변수
const onSlideStart = (e: TouchEvent<HTMLDivElement> | MouseEvent<HTMLDivElement>) => {
let pageX = getPageX(e);
setStartPageX(pageX);
setCurPageX(pageX - scrollX);
setIsOnScroll(true);
startLongPressScrollTimer();
};
startPageX
와 curPageX
를 설정하고, onScroll
를 true로 변경한다.
꾹 눌렀는지 확인하는 타이머를 시작한다.
const onSlide = (e: TouchEvent<HTMLDivElement> | MouseEvent<HTMLDivElement>) => {
if (!isOnScroll) return;
let pageX = getPageX(e);
if (pageX - curPageX >= 0 || pageX - curPageX <= -boxWidth * (length - 1))
return;
setScrollX(pageX - curPageX);
};
onTouchMove
의 이벤트 객체의 pageX 값을 이용하여 scrollX
를 설정한다.
첫 번째 페이지에서 왼쪽으로 슬라이드, 마지막 페이지에서 오른쪽으로 슬라이드하는 것을 막는다.
const onSlideEnd = (
e: TouchEvent<HTMLDivElement> | MouseEvent<HTMLDivElement>
) => {
if (!isOnScroll) return;
setIsOnScroll(false);
initLongPressScrollTimer();
// 오랫동안 눌렀을 때는 절반 이상 넘어가야 페이지 전환
if (isLongPressScroll) {
setScrollX(
-boxWidth * (Math.ceil((scrollX - boxWidth / 2) / -boxWidth) - 1)
);
setIsLongPressScroll(false);
return;
}
// 짧게 눌렀을 때는 조금만 움직여도 페이지 전환
let pageX = getPageX(e);
const offset = pageX - startPageX;
if (offset === 0) return;
if (offset < -10) {
if (Math.ceil(scrollX / -boxWidth) >= length) return;
setScrollX(-boxWidth * Math.ceil(scrollX / -boxWidth));
}
if (offset > -10) {
if (Math.ceil(scrollX / -boxWidth) <= 0) return;
setScrollX(-boxWidth * (Math.ceil(scrollX / -boxWidth) - 1));
}
};
if (isLongPressScroll) {
setScrollX(
-boxWidth * (Math.ceil((scrollX - boxWidth / 2) / -boxWidth) - 1)
);
setIsLongPressScroll(false);
return;
}
위 코드는 아래 코드를 일반화한 것이다.
// 현재 페이지에서 scrollX가 boxWidth의 절반 이상 넘어가면 다음 페이지로 이동
// scrollX가 boxWidth의 절반 이상 넘어가지 못하면 현재 페이지로 이동
if (isLongPressScroll) {
if (scrollX > -boxWidth / 2) {
setScrollX(0);
} else if (scrollX < -boxWidth / 2 && scrollX > (-boxWidth * 3) / 2) {
setScrollX(-boxWidth * 1);
} else if (scrollX < (-boxWidth * 3) / 2 && scrollX > (-boxWidth * 5) / 2) {
setScrollX(-boxWidth * 2);
} else if (scrollX < (-boxWidth * 5) / 2 && scrollX > (-boxWidth * 7) / 2) {
setScrollX(-boxWidth * 3);
}
setIsLongPressScroll(false);
return;
}
if (offset < -10) {
if (Math.ceil(scrollX / -boxWidth) >= length) return;
setScrollX(-boxWidth * Math.ceil(scrollX / -boxWidth));
}
위 코드는 아래 코드를 일반화한 것이다.
// 오른쪽으로 슬라이드했을 때,
if (offset < -10) {
// n번째 페이지이면, n+1 페이지로 이동
if (scrollX > -boxWidth) {
setScrollX(-boxWidth * 1);
} else if (scrollX > -boxWidth * 2) {
setScrollX(-boxWidth * 2);
} else if (scrollX > -boxWidth * 3) {
setScrollX(-boxWidth * 3);
} else {
return;
}
}
코드를 일반화하면 페이지가 늘어나도 코드를 변경할 필요가 없다는 장점이 있다.