데스크톱, 모바일 호환되는 가로 슬라이더 구현

Next.js
2024-03-12

image

환경

Next.js 14, Tailwindcss, Typescript

데스크톱과 모바일을 구분하는 이유

터치 이벤트는 일반적으로 터치 스크린을 가진 디바이스에서 이용가능하다.

하지만, 터치 스크린을 갖는 디바이스를 포함한 모든 데스크탑 디바이스에서 터치 이벤트 API를 사용할 수 없다.

따라서 모바일 환경과 데스크탑 환경을 구분해야 한다.

모바일 환경에서만 터치 이벤트 적용하기

Next.js 서버 컴포넌트에서 headeruser-agent 값을 이용해서 모바일인지 확인하고,

클라이언트 컴포넌트에 propsisMobile 값을 전달한다.

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); };
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; };
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와 curPageX의 차이

startPageX : offset으로 페이지 전환하기 위한 상태 변수

image

image

curPageX : onTouchMove 이벤트가 발생할 때 scrollX를 변경하기 위한 상태 변수

image

image

슬라이더 기능 구현 함수

onSlideStart

const onSlideStart = (e: TouchEvent<HTMLDivElement> | MouseEvent<HTMLDivElement>) => { let pageX = getPageX(e); setStartPageX(pageX); setCurPageX(pageX - scrollX); setIsOnScroll(true); startLongPressScrollTimer(); };
const onSlideStart = (e: TouchEvent<HTMLDivElement> | MouseEvent<HTMLDivElement>) => { let pageX = getPageX(e); setStartPageX(pageX); setCurPageX(pageX - scrollX); setIsOnScroll(true); startLongPressScrollTimer(); };

startPageXcurPageX 를 설정하고, onScroll 를 true로 변경한다.

꾹 눌렀는지 확인하는 타이머를 시작한다.

onSlide

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); };
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 를 설정한다.

첫번째 페이지에서 왼쪽으로 슬라이드 및 마지막 페이지에서 오른쪽으로 슬라이드하는 것을 막는다.

onSlideEnd

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)); } };
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)); } };

오랫동안 눌러서 터치했을 때

image

if (isLongPressScroll) { setScrollX( -boxWidth * (Math.ceil((scrollX - boxWidth / 2) / -boxWidth) - 1) ); setIsLongPressScroll(false); return; }
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; }
// 현재 페이지에서 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; }

빠르게 슬라이드 했을 때

image

if (offset < -10) { if (Math.ceil(scrollX / -boxWidth) >= length) return; setScrollX(-boxWidth * Math.ceil(scrollX / -boxWidth)); }
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; } }
// 오른쪽으로 슬라이드했을 때, 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; } }

코드를 일반화하면 페이지가 늘어나도 코드를 변경할 필요가 없다는 장점이 있다.

관련 포스트

post thumbnail

쿠키를 이용한 다크모드 구현

쿠키를 이용하여 Next.js 13 환경에서 다크모드를 구현했다. Next.js의 서버 컴포넌트에서는 브라우저의 API를 사용할 수 없다. 즉, 웹 스토리지를 이용할 수 없기 때문

2024-03-11
post thumbnail

Next.js + Vercel TTFB 문제 2

에서 Edge runtime으로 TTFB 문제를 해결했었는데, Express를 Next.js로 이미그레이션 후에는 Edge runtime을 사용할 수 없어서 다른 방법으로 문제를

2024-03-19
post thumbnail

Next.js + Vercel TTFB 문제

로컬 환경에서는 애플리케이션의 TTFB(Time To First Byte)가 70~150ms정도 걸리는데, 배포 환경에서는 TTFB가 느리면 1500ms, 빠르면 400ms정도가

2024-03-12
post thumbnail

[번역] Next.js 14

Next.js 14는 다음과 같은 가장 중점으로 둔 릴리스이다. - Turbopack: App & Pages Router에서 5,000번의 테스트 통과 - 로컬 서버 시작이 53%

2024-03-12

도로모의 기술 블로그

# Contact : jyw966@naver.com

Copyright © doromo. All Rights Reserved.