리엑트에서 반복 캐러셀 만들기 - React Infinite Carousel

최원빈·2022년 10월 17일
0


만든지는 좀 됐지만, 만든 과정이 재밌었기때문에 정리를 해두려 한다.

생긴건 간단한 모바일 화면에서 볼 수 있는 간단한 캐러섈인데,
일단 몇가지 짚고 가야할 기능들이 있다.

  • 슬라이드의 첫 시작지점은 중간. (스크롤의 시작지점이 아님)
  • 무한 캐러셀. 왼쪽으로 넘기면 다시 왼쪽에 생겨야 된다.
  • 슬라이드 중에도 데이터는 초마다 업데이트.
  • 중앙에 있는 목적지 시프트 변경을 누르면 현재 카드 순서가 유지되어야 한다.
  • 편의상, 어느 정도 슬라이드를 한다면 카드의 중심이 착 고정되면 좋을 것 같다.
  • 화면 사이즈에 따라 카드 크기가 반응형으로 조정되어야 한다.
  • 중심 카드를 제외한 좌우 카드는 사이즈가 조금 줄어든다.
  1. 기본적인 캐러셀 구조 제작.
// Container
const sliderRef = useRef();
const [mobileTypes, setMobileTypes] = useState(["좌측 카드","중앙 카드","우측 카드"]);

state로 mobileTypes의 순서를 바꿔가며 무한히 보이게끔 만들 생각이다.

  • 화면 사이즈에 따라 카드 크기가 반응형으로 조정되어야 한다.
    그래서 width를 75%로 설정했다.
// Presenter
// CSS를 styled-components를 통해 작성해서 태그명이 다르다.
// 모든 태그들을 div + className이라고 봐도 무방.

const BusCard = styled.div`
  display: inline-block;
  width: 75%;
  ...
`

const MobileSwiper = styled.div`
  position: relative;
  width: 100%;
  overflow-x: scroll;
  overflow-y: hidden;
  white-space: nowrap;
  ...
`

<MobileSwiper ref={sliderRef}>  
  {mobileTypes.map((type, index) => {
    return (
      <BusCard busType={type} key={type} index={index}>
        // 카드 내용물..
      </BusCard>
    )
  })}
</MobileSwiper>

sliderRef를 통해 MobileSwiper에 접근할거고, CSS까지 작성했다면 이런 화면이 완성된다.

이제 간단해보이는것부터 하나씩 해결해보자.

  • 중심 카드를 제외한 좌우 카드는 사이즈가 조금 줄어든다.

사이즈를 조정하는 간단한 CSS가 있다. transform: scale(n);으로 비율을 맞추면 된다.

const BusCard = styled.div`
  display: inline-block;
  width: 75%;
  
  // +
  transform: scale(${props => props.index === 1 ? 1.0 : 0.9 });
  ...
`

useState로 작성한 카드들의 index값이 1인 경우(가운데인경우) 사이즈를 1로 만들고,

나머지를 0.9배로 만들면 깔끔할 것 같다.

생각보다 금방 깔끔하게 완성됐다. 줄어들면서 좌우 간격도 생겼다.

  • 슬라이드의 첫 시작지점은 중간. (스크롤의 시작지점이 아님)

useRef로 받아둔 sliderRef를 사용할 때가 됐다.
Container - Presenter 패턴을 사용하고 있으니, Presenter에서 useEffect를 통해 값을 조정하자.

스크롤 상태는 element.scrollLeft 속성으로 접근할 수 있으니(기본값 0), ref에 맞춰서 값을 작성하면 될 것 같다.
뒤 식은 뭔가 많아보이지만, 반응형 화면에 맞췃 가운데로 맞추기 위한 노력의 결과물이라고 보면 된다.

useEffect(() => {
  sliderRef.current.scrollLeft = (window.innerWidth*0.75 - (window.innerWidth - window.innerWidth*0.75) / 2);
}, [])

사실 식 짜는게 제일 어려웠다.. 다음부턴 바로 종이꺼내서 방정식부터 써야겠다...

  • 편의상, 어느 정도 슬라이드를 한다면 카드의 중심이 착 고정되면 좋을 것 같다.

고정시키면서, 아래 이슈도 동시에 해결하자.

  • 무한 캐러셀. 왼쪽으로 넘기면 다시 왼쪽에 생겨야 된다.
// 슬라이더의 변위
let walk;

// 화면 기준 슬라이더의 터치된 지점
let startX;

// 슬라이더 터치된 슬라이더의 터치된 지점
let scrollValue;


// e.touches[0] 을 통해 터치된 지점을 확인할 수 있다.
// 터치가 되는 순간, startX와 scrollValue값을 초기화한다.
function slideTouchStart (e){
  startX = e.touches[0].pageX - sliderRef.current.offsetLeft;
  scrollValue = sliderRef.current.scrollLeft;
}

// 터치가 진행되는 동안, walk값이 변한다.
function slideTouchMove (e){
  e.preventDefault()
  
  // 0.9를 곱한 이유는, 카드의 너비가 화면보다 작았기 때문에, 드래그로 화면을 초과하는 것을 막았다.
  walk = (e.touches[0].pageX - sliderRef.current.offsetLeft - startX) * 0.9;
  
  // 이를 scrollLeft에 적용시킴으로써 실질적으로 드래그되는 변화량을 줄였다.
  sliderRef.current.scrollLeft = scrollValue - walk;
}


// End하거나 Cancel되면, walk값의 변화량에 따라 왼쪽으로 변화시킬 지, 오른쪽으로 변화시킬 지 정한다.
function slideTouchEnd (){
  if(walk) {
    sliderRef.current.scrollLeft = (window.innerWidth*0.75 - (window.innerWidth - window.innerWidth*0.75) / 2);
    if (walk < 0) {
    
      // 변위가 너무 작으면, 이동하지 않는다.
      if (walk < -120) {
        setMobileTypes((state) => state.slice(1, 3).concat(state[0]))
      }
    } 
    else if (walk > 0) {
      if (walk > 120) {
        setMobileTypes((state) => [state[2]].concat(state.slice(0, 2)))
      }
    }
  }
  walk = 0;
}

function slideTouchCancel (){
  if(walk) {
    sliderRef.current.scrollLeft = (window.innerWidth*0.75 - (window.innerWidth - window.innerWidth*0.75) / 2);
    if (walk < 0) {
      if (walk < -120) {
        setMobileTypes((state) => state.slice(1, 3).concat(state[0]))
      }
    } else if (walk > 0) {
      if (walk > 120) {
        setMobileTypes((state) => [state[2]].concat(state.slice(0, 2)))
      }
    }
  }
  walk = 0;
}

useEffect(() => {
  sliderRef.current.addEventListener('touchstart', slideTouchStart);
  sliderRef.current.addEventListener('touchend', slideTouchEnd);
  sliderRef.current.addEventListener('touchmove', slideTouchMove);
  sliderRef.current.addEventListener('touchcancel', slideTouchCancel);
}, [])

근데 문제가 생긴다.
드래그 중 1초가 지나 카드가 업데이트 되면 원래 자리로 돌아간다.

문제는 함수들을 useEffect 외부에 작성하고, useEffect 내부에서 addEventListener를 통해 렌더링마다 초기화되는 외부 함수를 받아왔기 때문에 발생했는데, useEffect 내부에서 함수들을 선언함으로써 문제를 해결했다.

useEffect(() => {
  sliderRef.current.scrollLeft = (window.innerWidth*0.75 - (window.innerWidth - window.innerWidth*0.75) / 2);
  
  // +
  let walk;
  let startX;
  let scrollValue;
  .
  .
  sliderRef.current.addEventListener('touchcancel', slideTouchCancel);

  // 해제를 안하면 warning이 뜨니 해제도 작성했다.
  return () => {if(sliderRef.current)sliderRef.current.removeEventListener('touchmove',slideTouchMove);}
}, [])

마지막으로 이동 시 캐러셀이 부드럽게 이동하지 않아 트랜지션도 추가했다.

const BusCard = styled.div`
  display: inline-block;
  width: 75%;
  transform: scale(${props => props.index === 1 ? 1.0 : 0.9 });
  
  // +
  transition: transform .3s;
  ...
`

profile
FrontEnd Developer

0개의 댓글