본문 바로가기
nextjs

커스텀 crousel (nextjs + ts + styled-components)

by spare8433 2023. 4. 20.

저장해두면 언젠가 쓸까해서 저장해두는 나의 커스텀 crousel

만들때는 참 오래걸렷는데 버리는 것도 한순간이군


사용 방법


  1. ReactElement[] | ReactElement 형식의 children 을 받아 배열의 경우 매핑하여 슬라이드 배치
  2. 추가로 필요한 banner 가 있다면 받아서 배치 (딱히 필요없음)
  3. props 로 styleOption:{ height:CssValue } 받아 crousel 높이 설정 (width 는 자동으로 100% 상위 태그로 감싸서 넓이 조정하여 사용하는 방식)
  4. CssValue 는 따로 css 를 props 를 입력받을때 타입을 자세히 설정해두려고 만든 class 형식의 type 이며 간단하게 string 으로 조금 고쳐서 사용해도 상관없다.

보완점


  1. css 입력 받는 부분이 CssValue 로 통일돼있지 않는데 일부 컨텐츠 사이즈를 입력받는 부분에서 실제로 넓이관려해서 계산하는 부분 때문이다.
  2. 기능 부족

특징 정리


  1. 화면 resize 에 대응하여 넓이 조정됨
  2. 자동 넘기기 기능
  3. 슬라이드 넘기는 버튼 존재

  import React, { ReactElement, useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react'
  import styled from 'styled-components';
  import AngleRight from '@svgs/angle-right.svg'    // 좌측 버튼 이미지 소스
  import AngleLeft from '@svgs/angle-left.svg'        // 우측 버튼 이미지 소스

  const AppLayout = styled.div`
    width: 100%;
  `

  const CarouselContainor = styled.div`
    position: relative;
  `

  const CarouselBox = styled.div<{ height:CssValue }>`
    width: 100%;
    height: ${({ height }) => height.getValue()};
    margin: 30px 0;
  `

  const CarouselView = styled.div`
    width: 100%;
    height: 100%;
    position: relative;
    overflow-x :hidden;
  `

  const CarouselTrack = styled.div<{ contetWidth:number, currentSlide:number }>`
    width: ${({ contetWidth }) => `${contetWidth.toString()}px`};
    height: 100%;
    position: absolute;
    transform: translateX(${({ contetWidth, currentSlide }) => `${(contetWidth * currentSlide * -1).toString()}px`});
    left: 0;
    top: 0;
    display: flex;
    transition: all 1s ease-in-out;
  `

  const CarouselButton = styled.div`
    position: absolute;
    top:50%;
    transform: translate(0, -50%);
    z-index: 10;
    width: auto;
    height: auto;
    img{}
    &.prev{left: 0px;}
    &.next{right: 0px;}
  `

  const SlidePage = styled.div<{ contetWidth:number }>`
    position: relative;
    min-width: ${({ contetWidth }) => `${contetWidth.toString()}px`};
    height: 100%;
  `

  interface CarouselProps {
    styleOption:{
      height:CssValue
    },
    banner?: ReactElement,
    children: ReactElement[] | ReactElement;
  }

  function isReactElements(arg: any): arg is ReactElement[] {
    return arg.length !== undefined;
  }

  const Carousel = ({ styleOption, banner, children }:CarouselProps) => {
    const [currentSlide, setCurrentSlide] = useState(0)
    const [isAutoPlay, setIsAutoPlay] = useState(true)
    const [boxSize, setBoxSize] = useState<number>(0)

    const carouselBoxRef = useRef<HTMLDivElement>(null)
    const timer = useRef<NodeJS.Timeout>()

    // 다음 슬라이드로 이동
    const next = useCallback(() => {
      if (!isReactElements(children)) return

      if (currentSlide >= children.length - 1) {
        setCurrentSlide(0)
      } else {
        setCurrentSlide(currentSlide + 1);
      }
    }, [currentSlide, children])

    // 이전 슬라이드로 이동
    const prev = useCallback(() => {
      if (!isReactElements(children)) return

      if (currentSlide === 0) {
        setCurrentSlide(children.length - 1)
      } else {
        setCurrentSlide(currentSlide - 1);
      }
    }, [currentSlide, children])

    const pause = useCallback(() => setIsAutoPlay(false), [])
    const start = useCallback(() => setIsAutoPlay(true), [])

    // 자동 넘기기 기능
    useEffect(() => {
      clearTimeout(timer.current)
      if (isAutoPlay) timer.current = setTimeout(next, 2000)
    }, [next, isAutoPlay])

    // 가변적인 화면사이즈 대응
    useLayoutEffect(() => {
      setBoxSize(carouselBoxRef.current?.offsetWidth ?? 0)
    }, [carouselBoxRef.current?.offsetWidth])

    return (
      <AppLayout>
        <CarouselContainor>
          <CarouselButton key="prev" className="prev" onClick={() => { pause(); prev(); start(); }}><AngleLeft width="24" height="24" fill="gray" /></CarouselButton>
          <CarouselButton key="next" className="next" onClick={() => { pause(); next(); start(); }}><AngleRight width="24" height="24" fill="gray" /></CarouselButton>

          <CarouselBox ref={carouselBoxRef} height={styleOption.height}>
            <CarouselView>
              <CarouselTrack contetWidth={boxSize} currentSlide={currentSlide}>
                {Array.isArray(children)
                  ? children.map((res, index) => <SlidePage key={`slidepage_${index}`} contetWidth={boxSize}>{res}</SlidePage>)
                  : children}
              </CarouselTrack>
              {banner ?? ''}
            </CarouselView>
          </CarouselBox>
        </CarouselContainor>
      </AppLayout>
    )
  }
  export default Carousel