데스크톱, 모바일 호환되는 이미지 캐로셀 구현

8일 전

이미지

구현 환경 : 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);
};

모바일 환경이면 터치 이벤트 객체를 이용하여 슬라이드를 구현하고, 데스크톱 환경이면 마우스 이벤트 객체를 이용하여 슬라이드를 구현한다.

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으로 페이지 전환하기 위한 상태 변수

이미지이미지

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

이미지이미지


캐로셀 기능 구현 함수

onSlideStart

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


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

이미지
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;
  }
}

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