본문 바로가기
FE/react

[React] 리액트 carousel 라이브러리 없이 직접 구현하기

by s0ojin 2023. 5. 6.

캐러셀(carousel)이란?


캐러셀은 슬라이드쇼와 같은 방식으로 콘텐츠를 표시하는 UX 구성 요소이다. 버튼을 눌러 옆으로 사진이 넘어가는 모션이 마치 회전목마와 비슷하다고해서 붙여진 이름이다. 캐러셀은 용도에 따라 자동재생되게 하거나, 사용자가 수동으로 넘기도록 구현할 수도 있다. 

 

대중적으로 이용되는 라이브러리


react-slick이라는 라이브러리가 가장 대중적으로 사용되는 캐러셀 라이브러리이다.

 

장점이라면 역시 문서보고 따라하면 잘 작동하는 캐러셀을 쉽게 구현할 수 있다는 점? 그리고 커스텀도 상당히 자유롭다고한다. 사실 직접 해당 라이브러리를 사용해보진 않아서 모르겠지만 많은 사람이 이용하는만큼 최적화도 잘 되어있으리라 짐작된다. 

 

위 데모를 보면

1. 양옆 버튼을 눌러 앞뒤로 슬라이드가 부드럽게 넘어가고,

2. 드래그로 넘길 수도 있다.

3. 아래 점은 현재 사진 위치를 보여주는 동시에 클릭하면 해당 사진으로 이동한다.

 

내가 오늘 라이브러리없이 구현하려는 것은 위 데모에서 드래그를 이용한 넘기기를 제외한 기능들을 가지고 있는 캐러셀이다. 드래그가 있는 편이 모바일에서 확실히 편하기 때문에 추후 주요기능들의 구현이 완료되면 추가할 생각이 있다.

 

구현 방식


우선 화면에서 모든 이미지가 보이진 않지만, 사실 여러개의 사진들이 가로로 쭉 나열되어있다. 디자인이나 목적에따라 조금씩 다른 방식으로 구현할 수 있겠지만, 기본적으로 아래와 같은 방식을 따른다.

 

  1. 사진이 회전목마처럼 넘어가는 듯한 효과를 주려면 버튼을 누를 때마다 사진을 변경시키는 것이 아닌, 가로로 쭉 나열시켜야한다. 편한대로 flex나  float : right속성을 활용하면 될 것이다.
  2. 화면을 벗어나는 사진들은 overflow : hidden으로 가린다. 옆으로 넘어갈 때 이전 사진을 지나 다음사진으로 넘어가는 듯한 모습은  transform : translate()으로 이동시켜 구현할 수 있다.
  3. 현재의 사진 혹은 위치, 즉 index를 useState로 선언하고 다뤄주어야한다. 버튼을 누르면, 이 index만 변경시킨다. 이 index는 transform : translate() 속성에서 동적으로 작동되게 할 것이다. 가령 0번 이미지에서 1번 이미지로 넘어갈 때, transform : translate(-100vw)이라고 작성하면 된다. index에 따라 동적으로 변경하고 싶다면transform : translate(`-${index}vw`)과 같이 작성하면 될 것이다.
  4. 애니메이션 효과는 transition 을 활용하면 된다.

 

코드


**React, TypeScript, tailwindCSS 로 작성된 코드입니다.

import React, { useState } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import { ReactComponent as CloseBtn } from 'src/assets/plus.svg';
import { ReactComponent as Arrow } from 'src/assets/back.svg';
 
interface IImageList {
  images: { id: number; order: number; url: string }[];
  index: number;
}

export default function ImagesDetail() {
  const navigate = useNavigate();
  const location = useLocation();
  
  // 사진리스트는 게시글에서 사진을 클릭할 때 navigate state에 담아서 가져왔다.
  const imageList: IImageList = location.state;
  
  //현재 보고있는 사진의 인덱스
  const [current, setCurrent] = useState(imageList.index);
  
  //tailwind.css가 동적스타일링을 하려면 완성된 문장으로 써야하기 때문에 추가한 부분이다.
  //tailwind가 아니라면 동적 스타일링 방식으로 간단하게 작성할 수 있을 것이다.
  //ex) transform : translate(`-${current}00vw`);
  //current에 따라 x의 위치를 이동시켜 현재 인덱스에 맞는 사진을 보여준다.
  const moveStyle: { [key: number]: string } = {
    0: 'translate-x-0',
    1: 'translate-x-[-100vw]',
    2: 'translate-x-[-200vw]',
    3: 'translate-x-[-300vw]',
    4: 'translate-x-[-400vw]',
    5: 'translate-x-[-500vw]',
    6: 'translate-x-[-600vw]',
    7: 'translate-x-[-700vw]',
    8: 'translate-x-[-800vw]',
    9: 'translate-x-[-900vw]',
    10: 'translate-x-[-1000vw]',
  };

//X버튼을 눌렀을 때 게시글로 돌아감
  const closeBtnHandler = () => {
    navigate(-1);
  };

//다음 버튼을 누르면 current를 1 증가시킴, 만약 현재 마지막 사진이면 첫번째 사진으로 가도록 0 return
  const nextHandler = () => {
    setCurrent(() => {
      if (current === imageList.images.length - 1) {
        return 0;
      } else {
        return current + 1;
      }
    });
  };

//이전 버튼을 누르면 current를 1 감소시킴, 만약 현재 첫번째 사진이면 마지막 사진으로 가도록 마지막사진 인덱스 return
  const prevHandler = () => {
    setCurrent(() => {
      if (current === 0) {
        return imageList.images.length - 1;
      } else {
        return current - 1;
      }
    });
  };

  return (
  {/* 가장 바깥부분 overflow:hidden 속성으로 나머지 사진 안보이도록 하기*/}
    <div className="relative flex h-[100vh] items-center overflow-hidden bg-black-100">
      <CloseBtn
        onClick={closeBtnHandler}
        className="absolute top-10 right-10 z-[100] h-[3.5rem] w-[3.5rem] rotate-45 fill-black-10"
      />
        {/* 사진리스트, 사진을 감싸고 있는 태그에 transform: translate() 속성작성*/}
      <div
        className={`flex max-h-[60%] items-center ${moveStyle[current]} transition`}>
        {imageList.images.map((image) => (
          <div key={image.id} className="w-[100vw]">
            <img src={image.url} className="w-full object-contain" />
          </div>
        ))}
      </div>
        {/*사진 넘기는 버튼*/}
      <button
        onClick={nextHandler}
        className="absolute right-4 flex h-[4rem] w-[4rem] items-center justify-center rounded-full bg-black-10 opacity-50">
        <Arrow className="h-[2.5rem] rotate-180 stroke-black-50" />
      </button>
      <button
        onClick={prevHandler}
        className="absolute left-4 flex h-[4rem] w-[4rem] items-center justify-center rounded-full bg-black-10 opacity-50">
        <Arrow className="h-[2.5rem]" />
      </button>
        {/* 하단 점, 점과 current가 같다면 투명도를 변경시켜 표시함. 클릭하면 current를 클릭한 index로 변경시킴 */}
      <ul className="absolute bottom-20 flex w-full justify-center gap-4">
        {imageList.images.map((_, idx) => (
          <li
            key={idx}
            className={`h-[1.2rem] w-[1.2rem] rounded-full bg-white ${
              idx === current ? 'opacity-100' : 'opacity-50'
            }`}
            onClick={() => setCurrent(idx)}
          />
        ))}
      </ul>
    </div>
  );
}

 

작동 모습


 

 

References


https://web.dev/i18n/ko/carousel-best-practices/#lcp-measurement-for-carousels

https://www.youtube.com/watch?v=qHzSQrLjxlQ

 

 

 

 

 

댓글