Web/React & Next.js

React | Swiper.js를 사용하여 이미지 슬라이드 구현하기

일렁이는코드 2022. 11. 25. 14:42

Preview

 

 

swiper 기본 세팅 및 무한 루프 기능

swiper.js 설치

npm i swiper --save

 

서버에서 받아오는 데이터 처럼 .json 형태의 파일을 fetch로 가져와 slide를 map으로 돌리려 합니다.

// swiper_event.json

{
  "data": [
    {
      "img": "https://images.unsplash.com/photo-1661956603025-8310b2e3036d?ixlib=rb-4.0.3&ixid=MnwxMjA3fDF8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1170&q=80",
      "url": "https://www.naver.com/"
    },
    {
      "img": "https://images.unsplash.com/photo-1505373877841-8d25f7d46678?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1112&q=80",
      "url": "/cookie"
    },
    {
      "img": "https://images.unsplash.com/photo-1472653431158-6364773b2a56?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1169&q=80",
      "url": ""
    },
    {
      "img": "https://images.unsplash.com/photo-1507878866276-a947ef722fee?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1171&q=80",
      "url": "https://www.naver.com/"
    },
    {
      "img": "https://plus.unsplash.com/premium_photo-1663100894140-930cf3ff227e?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1225&q=80",
      "url": ""
    }
  ]
}
// SwiperEvent.tsx

import React, { useEffect, useState } from 'react';
import styled from 'styled-components';
import { Swiper, SwiperSlide } from 'swiper/react';
import SwiperCore, { Autoplay } from 'swiper';
import 'swiper/css';

interface Type_SwiperEvent {
  img: string;
  url: string;
}

export default function SwiperEvent() {
  SwiperCore.use([Autoplay]);

  const [swiperList, setSwiperList] = useState<Type_SwiperEvent[]>([]);

  useEffect(() => {
    fetch(`/data/swiper/swiper_event.json`)
      .then(res => res.json())
      .then(res => {
        setSwiperList(res.data);
      });
  }, []);

  return (
    <Wrapper>
      {swiperList && swiperList.length > 0 ? (
        <Swiper
          slidesPerView={1}
          loop={true}
          autoplay={{ delay: 1500, disableOnInteraction: false }}
        >
          {swiperList.map((event: Type_SwiperEvent, idx: number) => (
            <SwiperSlide
              key={idx}
              onClick={() => {
                if (event.url) {
                  window.location.href = event.url;
                }
              }}
            >
              <img src={event.img} alt="img" />
            </SwiperSlide>
          ))}
        </Swiper>
      ) : null}
    </Wrapper>
  );
}

const Wrapper = styled.div`
  .swiper-slide {
    position: relative;
    width: 100%;
    height: 0;
    padding-bottom: 56.26%;
    overflow: hidden;
    z-index: 10;
  }

  img {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
  }
`;

1. swiper 관련 라이브러리를 import 해줍니다.

* swiper/css를 Import 하지 않으면 세팅을 해줘도 보이지 않으니 꼭꼭 import 하기! 

 

2. <Swiper> 세팅하기

<Swiper
        slidesPerView={1}
        loop={true}
        autoplay={{ delay: 1500, disableOnInteraction: false }} >
        <SwiperSlide>
        	<img src="https://image_1.com" alt="img" />
        </SwiperSlide>
        <SwiperSlide>
        	<img src="https://image_2.com" alt="img" />
        </SwiperSlide>
        <SwiperSlide>
        	<img src="https://image_3.com" alt="img" />
        </SwiperSlide>
</Swiper>

Swiper 태그 안에 SwiperSlide를 넣어 내부에 전체 콘텐츠를 세팅을 해주면 되는데, 이 부분을 map으로 처리해줍니다.

 

✅ 옵션 설명

slidePerView: 한 장에 보일 이미지/콘텐츠 개수
loop: 루프를 시킬지 말지
autoplay: 보이는 시간 초 설정 / swiper를 클릭했을 때 loop를 멈추지 않고 계속 돌리겠다는 옵션. 

📌 SwiperCore.use([Autoplay]);

autoplay가 적용되지 않을 때 상단에 입력해줍니다.

 

3. 반응형 적용

swiper 내의 css를 수정하고 싶을 때는 상단의 div 안에 해당 class명을 가지고 수정해줄 수 있습니다.

 

navigate 버튼 커스텀 하기

// 기존 코드에 추가하기
export default function SwiperEvent() {
  const swiperRef = useRef<SwiperCore>();

  return (
    <Wrapper>
      {swiperList && swiperList.length > 0 ? (
        <>
          <Swiper
            onBeforeInit={swiper => {
              swiperRef.current = swiper;
            }}
          >
           ...

          </Swiper>
          <SwiperModuleWrap>
            <SwiperBtnWrap>
              <PrevBtn
                type="button"
                onClick={() => swiperRef.current?.slidePrev()}
              >
                Prev
              </PrevBtn>
              <NextBtn
                type="button"
                onClick={() => swiperRef.current?.slideNext()}
              >
                Next
              </NextBtn>
            </SwiperBtnWrap>
          </SwiperModuleWrap>
        </>
      ) : null}
    </Wrapper>
  );
}

먼저 Swiper 태그 외부로 Button을 빼서 마크업 해야 하며 z-index를 사용해야지 커스텀한 버튼을 볼 수 있습니다. (처음 구현했을 때는 버튼이 있는데 안 보여서 한참 찾은 기억이...😫)

버튼을 누르면 swiper 내부의 함수를 호출해서 이전 / 다음 페이지로 넘겨야 하는데 내부 함수를 사용할 수 있는 방법이 useRef을 명시하여 사용하면 됩니다. 

 

📌 이전 / 다음 슬라이드 이동하는 함수

swiperRef.current?.slidePrev()
swiperRef.current?.slideNext()

 

Pagination 버튼 커스텀 하기

// 기존 코드에 추가하기
export default function SwiperEvent() {
  const swiperRef = useRef<SwiperCore>();
  
  const [currentIdx, setCurrentIdx] = useState(0);

return (
    <Wrapper>
      {swiperList && swiperList.length > 0 ? (
        <>
          <Swiper
            onBeforeInit={swiper => {
              swiperRef.current = swiper;
            }}
            onSlideChange={e => {
              setCurrentIdx(e.realIndex + 1);
            }}
          >
            ...
            
          </Swiper>
          <SwiperModuleWrap>
            <SwiperPgWrap>
              <CurrentPg>{currentIdx}</CurrentPg>
              <TotalPg>/{swiperList.length}</TotalPg>
            </SwiperPgWrap>
          </SwiperModuleWrap>
        </>
      ) : null}
    </Wrapper>
  );
}

swiper는 현재 보이고 있는 페이지의 index를 제공해줍니다. 그걸 토대로 현재 몇 번째 이미지가 로드되고 있는지 state를 사용하여, 페이징 네이션을 커스텀해볼 수 있었습니다. 

 

📌 현재 표시되는 index 값 가져오기

<Swiper
	onSlideChange={e => {
    	setCurrentIdx(e.realIndex + 1);
        }}
>

 

Pagination 버튼 커스텀 하기 (모바일 버전)

export default function SwiperEventMobile() {
  const swiperRef = useRef<SwiperCore>();

  const [currentIdx, setCurrentIdx] = useState(0);

  const clickSlide = (idx: number) => {
    swiperRef.current?.slideTo(idx + 1);
    setCurrentIdx(idx + 1);
  };

  return (
    <Wrapper>
      {swiperList && swiperList.length > 0 ? (
        <>
          <Swiper
            onBeforeInit={swiper => {
              swiperRef.current = swiper;
            }}
            onSlideChange={e => {
              setCurrentIdx(e.realIndex + 1);
            }}
          >
            ...
          </Swiper>
          <SwiperModuleWrap>
            {swiperList.map((event, idx) => (
              <SwiperBtn
                key={idx}
                onClick={() => clickSlide(idx)}
                className={idx + 1 === currentIdx ? 'active' : ''}
              />
            ))}
          </SwiperModuleWrap>
        </>
      ) : null}
    </Wrapper>
  );
}

모바일 버전에서는 현재 페이지를 숫자로 보여주는 것보다 그래픽으로 보여주는 게 더 직관적이었습니다. 위 코드는 저의 동료가 수정하여 구현했는데, 나중에 잊지 않고자 기록하려 합니다.

콘텐츠의 길이만큼 페이지 네이션과, 페이지 이동 기능을 같이 하는 버튼을 map으로 돌려 보여주고, 버튼을 클릭했을 때 해당 인덱스로 이동시키는 함수와 currentIdx state를 업데이트해주면 됩니다.

 

 

👩🏻‍💻 전체 코드 (PC)

더보기
import React, { useEffect, useRef, useState } from 'react';
import styled from 'styled-components';
import { Swiper, SwiperSlide } from 'swiper/react';
import SwiperCore, { Autoplay } from 'swiper';
import 'swiper/css';

 

interface Type_SwiperEvent {
img: string;
url: string;
}

 

export default function SwiperEvent() {
SwiperCore.use([Autoplay]);
const swiperRef = useRef<SwiperCore>();

 

const [swiperList, setSwiperList] = useState<Type_SwiperEvent[]>([]);
const [currentIdx, setCurrentIdx] = useState(0);

 

useEffect(() => {
fetch(`/data/swiper/swiper_event.json`)
.then(res => res.json())
.then(res => {
setSwiperList(res.data);
});
}, []);

 

return (
<Wrapper>
{swiperList && swiperList.length > 0 ? (
<>
<Swiper
onBeforeInit={swiper => {
swiperRef.current = swiper;
}}
onSlideChange={e => {
setCurrentIdx(e.realIndex + 1);
}}
slidesPerView={1}
loop={true}
autoplay={{ delay: 1500, disableOnInteraction: false }}
>
{swiperList.map((event: Type_SwiperEvent, idx: number) => (
<SwiperSlide
key={idx}
onClick={() => {
if (event.url) {
window.location.href = event.url;
}
}}
>
<img src={event.img} alt="img" />
</SwiperSlide>
))}
</Swiper>
<SwiperModuleWrap>
<SwiperPgWrap>
<CurrentPg>{currentIdx}</CurrentPg>
<TotalPg>/{swiperList.length}</TotalPg>
</SwiperPgWrap>
<SwiperBtnWrap>
<PrevBtn
type="button"
onClick={() => swiperRef.current?.slidePrev()}
>
Prev
</PrevBtn>
<NextBtn
type="button"
onClick={() => swiperRef.current?.slideNext()}
>
Next
</NextBtn>
</SwiperBtnWrap>
</SwiperModuleWrap>
</>
) : null}
</Wrapper>
);
}

 

const Wrapper = styled.div`
position: relative;

 

.swiper-slide {
position: relative;
width: 100%;
height: 0;
padding-bottom: 56.26%;
overflow: hidden;
z-index: 10;
}

 

img {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
`;

 

const SwiperModuleWrap = styled.div`
display: flex;
flex-direction: column;
justify-content: center;
position: absolute;
gap: 10px;
width: 84px;
height: 72px;
bottom: 10px;
right: 30px;
z-index: 100;
`;

 

const SwiperBtnWrap = styled.div`
display: flex;
justify-content: space-between;
`;

 

const NaviBtn = styled.button`
width: 32px;
height: 32px;
font-size: 0px;
border-style: none;
cursor: pointer !important;
`;

 

const PrevBtn = styled(NaviBtn)`
background: url(${'/images/arrow/slider_prev.svg'}) no-repeat 50% 34%;
`;
const NextBtn = styled(NaviBtn)`
background: url(${'/images/arrow/slider_next.svg'}) no-repeat 50% 34%;
`;

 

const SwiperPgWrap = styled.div`
display: flex;
justify-content: center;
gap: 5px;
`;

 

const CurrentPg = styled.span`
font-size: 16px;
font-weight: 400;
color: white;
`;

 

const TotalPg = styled.span`
font-size: 24px;
font-weight: 400;
color: white;
`;



 

👩🏻‍💻 전체 코드 (Mobile)

더보기
import React, { useEffect, useRef, useState } from 'react';
import styled from 'styled-components';
import { Swiper, SwiperSlide } from 'swiper/react';
import SwiperCore, { Autoplay } from 'swiper';
import 'swiper/css';

 

interface Type_SwiperEvent {
img: string;
url: string;
}

 

export default function SwiperEventMobile() {
SwiperCore.use([Autoplay]);
const swiperRef = useRef<SwiperCore>();

 

const [swiperList, setSwiperList] = useState<Type_SwiperEvent[]>([]);
const [currentIdx, setCurrentIdx] = useState(0);

 

const clickSlide = (idx: number) => {
swiperRef.current?.slideTo(idx + 1);
setCurrentIdx(idx + 1);
};

 

useEffect(() => {
fetch(`/data/swiper/swiper_event.json`)
.then(res => res.json())
.then(res => {
setSwiperList(res.data);
});
}, []);

 

return (
<Wrapper>
{swiperList && swiperList.length > 0 ? (
<>
<Swiper
onBeforeInit={swiper => {
swiperRef.current = swiper;
}}
onSlideChange={e => {
setCurrentIdx(e.realIndex + 1);
}}
slidesPerView={1}
loop={true}
autoplay={{ delay: 1500, disableOnInteraction: false }}
>
{swiperList.map((event: Type_SwiperEvent, idx: number) => (
<SwiperSlide
key={idx}
onClick={() => {
if (event.url) {
window.location.href = event.url;
}
}}
>
<img src={event.img} alt="img" />
</SwiperSlide>
))}
</Swiper>
<SwiperModuleWrap>
{swiperList.map((event, idx) => (
<SwiperBtn
key={idx}
onClick={() => clickSlide(idx)}
className={idx + 1 === currentIdx ? 'active' : ''}
/>
))}
</SwiperModuleWrap>
</>
) : null}
</Wrapper>
);
}

 

const Wrapper = styled.div`
position: relative;

 

.swiper-slide {
position: relative;
width: 100%;
height: 0;
padding-bottom: 56.26%;
overflow: hidden;
z-index: 10;
}

 

img {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
`;

 

const SwiperModuleWrap = styled.div`
display: flex;
position: absolute;
bottom: 10px;
left: 50%;
transform: translate(-50%, 0%);
gap: 7px;
z-index: 100;
`;

 

const SwiperBtn = styled.button`
width: 27px;
height: 2px;
background: #78787a;
border-style: none;

 

&.active {
background: #fff;
}
`;

 

 

🤔 구현해야 하는 시간이 촉박해 swiper 라이브러리를 사용했는데, 버튼이나 페이지 네이션을 커스텀하여 사용하는 부분에서 검색하는데 많은 시간이 들어갔었습니다. (github, 스택오버플로우 최고..!) 많은 사이트들에서 메인에 이벤트들을 나열할 때 사용하고 있는 기능으로 추후에는 라이브러리 사용 없이 직접 구현해봐야겠다 생각했습니다.


https://github.com/nolimits4web/swiper/issues/3855

https://swiperjs.com/react

반응형