React TypeScript 환경에서 Swiper 사용하기(image slider library)

Sohee Kwon·2022년 1월 16일
17

프론트엔드

목록 보기
1/2
post-thumbnail
post-custom-banner

앱잼 기간22.01.02 - 01.21 동안 소품샵 리뷰 및 지도 서비스 소담을 개발하고 있습니다.
각종 리뷰와 소품샵 정보를 보여주는 만큼 이미지 슬라이더가 필수적으로 사용되는데, 적절한 슬라이더 라이브러리를 찾던 중 Swiper를 사용하기로 결정했습니다.

Swiper API 공식문서 바로가기

주간 HOT 소품샵기능은 다음과 같습니다.
1. 좌우 버튼을 누르면 이미지가 한 칸씩 이동한다
2. 이미지를 더 이상 넘길 수 없는 경우, 좌우 버튼이 사라집니다

크게 어려운 기능은 아니지만, 기존의 라이브러리 스타일을 그대로 쓰는 게 아니라 어느 정도 커스텀이 필요합니다.

1. 초기 세팅하기


설치

yarn add swiper@6.8.4

그냥 yarn add swiper로 설치하면 최신버전으로 설치되는데, 최신버전에서는 swiper 모듈을 찾을 수 없다는 에러가 발생합니다. 버전을 낮춰서 설치하면 정상적으로 사용할 수 있습니다.

css import

import 'swiper/swiper.min.css';

따로 스타일을 커스텀할 예정이라 최소한의 css만 가져왔습니다. 기존 라이브러리의 다양한 스타일을 사용한다면 swiper/swiper.bundle.min.css 까지 import 하면 됩니다. 여기에는 Navigation, Pagination 등 요소의 모든 스타일이 포함되어 있습니다.

슬라이더 뼈대 만들기

import SwiperCore, { Navigation, Scrollbar } from 'swiper';
import { Swiper, SwiperSlide } from 'swiper/react';

function MainSlider(cardList) {
	SwiperCore.use([Navigation, Scrollbar]);
	
    return (
    	<Swiper>
           {cardList.map(card => <SwiperSlide key={card.id}>{card}</SwiperSlide>)}
        </Swiper>
    );
}

export default MainSlider;
  • SwiperCore.use() 안에 슬라이더에서 사용할 기능을 배열로 추가합니다. Navigation(좌우 화살표)와 Scrollbar(스크롤바)만 가져왔는데, 필요하다면 Pagination도 추가해 줍니다.
  • <Swiper>는 이미지 슬라이더의 최상단 컴포넌트입니다. 내부에 <SwiperSlide>를 원하는 만큼 추가하면 이미지 슬라이더를 매우 쉽게 만들 수 있습니다.

여기까지 작성하면 기본적인 이미지 슬라이더를 만들 수 있습니다.

2. 슬라이더 커스텀하기


여러가지 속성 추가하기

const settings = {
	spaceBetween: // px 단위 간격
    	navigation: { // 좌우 버튼 커스텀 
    		prevEl: // 이전 버튼 Ref 또는 className
        	nextEl: // 다음 버튼 Ref 또는 className
    	},
    	scrollbar: { // 스크롤바 커스텀
        	draggable: // 드래그 가능 여부
            	el: // 스크롤바 Ref 또는 className
        },
        slidesPerView: // 한 화면에 보이는 슬라이드 수
        onBeforeInit: // 이벤트 핸들러
};
...
return (
	<Swiper {...settings}>
    	...
	</Swiper>
);

<Swiper>에 직접 속성을 줄 수 있지만, 깔끔한 코드를 위해서 속성을 따로 분리하여 작성했습니다.

  • spaceBetween: 각 슬라이드 사이의 간격으로, 숫자만 입력합니다. 이 때의 단위는 px입니다.
  • navigation: 좌우 버튼을 커스텀할 수 있습니다. prevElnextEl을 따로 설정하지 않으면 기본 값이 들어갑니다. 커스텀 할 경우, className 또는 ref.current를 입력합니다. 이 부분에 대해서는 아래에서 자세히 다룰 예정입니다.
  • scrollbar: 스크롤바를 커스텀할 수 있습니다. draggable을 true로 설정하면 슬라이더를 드래그할 수 있습니다. el을 null로 설정하면 스크롤바가 보이지 않습니다.
  • slidesPerView: 한 화면에 보이는 슬라이드 수를 설정할 수 있습니다.
  • 그 외: onBeforeInit, onDragStart, onClick 등 이벤트 핸들러

속성을 따로 부여하지 않는 기본 슬라이더의 모습은 다음과 같습니다.
example1기본값은 좌우 버튼도 없고, 한 화면에 슬라이드 하나만 보이고, 드래그로 슬라이드를 넘길 수 있는 아주 별로인 기본적인 슬라이더입니다.

아래와 같은 속성을 부여하면 가장 기본적인 형태의 슬라이더가 완성됩니다. 라이브러리의 navigation 스타일을 사용하기 위해 별도의 css로 import 합니다.

import 'swiper/components/navigation/navigation.min.css';

const settings = {
	spaceBetween: 20,
    	navigation: {},
    	scrollbar: { draggable: true, el: null },
        slidesPerView: 3,
};

example2좌우 버튼도 생기고, 한 화면에 슬라이드가 3개씩 보이고, 드래그로 슬라이드를 넘길 수 있고, 슬라이드를 더 넘길 수 없으면 좌우 버튼이 비활성화 됩니다.

추가 커스텀

위의 뼈대에 스타일을 전혀 주지 않으면 슬라이더가 위의 화면처럼 나오지 않습니다. className으로 직접 접근하면 더 세세한 커스텀이 가능합니다. 현재 styled-components를 사용하고 있기 때문에, <StyledRoot> 에서 css 스타일을 추가했습니다.

import styled from 'styled-components';
...
function MainSlider(cardList) {
	...
	return (
          <StyledRoot>
            <Swiper {...settings}>
                ...
            </Swiper>
          </StyledRoot>
        );
}
...
const StyledRoot = styled.div`
	.swiper {
    		&-wrapper,
    		&-container {
      			width: 62rem;
      			margin: 0;
    		}
    		&-container {
      			margin: 0 3.2rem;
    		}
    		&-button-disabled {
      			visibility: hidden;
    		}
  	}
`;

이렇게 .swiper-wrapper, .swiper-container, .swiper-button-disabled의 스타일을 변경하면 다음과 같이 보여집니다.
example3widthmargin은 슬라이드의 크기에 맞춰서 변경하면 됩니다.

.swiper-button-disabledvisibility: hidden; 속성을 준 것은 더 이상 슬라이드를 넘길 수 없는 경우에 버튼을 숨기기 위함입니다.
display: none; 속성을 주면 추후에 버튼을 커스텀 했을 때 슬라이더가 빈 공간을 메꾸면서 움직일 수 있기 때문에 visibility 속성을 사용했습니다.

Swiper를 커스텀하면서 제일 어려웠던 부분이 바로 navigation(좌우 화살표) 커스텀이었습니다. 기존의 navigation은 슬라이더 안쪽에 있기 때문에 이걸 슬라이더 밖으로 단순히 이동시켜서는 화면에 보이지 않게 됩니다.

따라서 슬라이더 밖에 navigation 버튼을 두고 싶다면 추가적인 커스텀이 필요합니다. 커스텀하는 방법에는 크게 2가지가 있습니다.

  1. className 사용하기
  2. useRef 사용하기

className은 재사용되는 컴포넌트에는 적절하지 않은 방법입니다.

모든 슬라이더의 navigation 버튼에 같은 className이 적용되기 때문에, 버튼 하나를 누르면 모든 슬라이더가 동시에 움직이는 참사가 발생하게 됩니다. 따라서 useRef를 사용하는 것을 매우 권장드립니다.

import { useRef } from 'react';

function MainSlider(cardList) {
	const prevRef = useRef(null);
	const nextRef = useRef(null);

	const settings = {
		spaceBetween: 20,
    		navigation: {
            		prevEl: prevRef.current, // 이전 버튼
                    	nextEl: nextRef.current, // 다음 버튼
                },
    		scrollbar: { draggable: true, el: null },
        	slidesPerView: 3,
            	onBeforeInit: (swiper) => { // 초기 설정
                	swiper.params.navigation.prevEl = prevRef.current;
              		swiper.params.navigation.nextEl = nextRef.current;
                    	swiper.navigation.update();
            	},
        };
        
        return (
          <StyledRoot>
              <StyledButton ref={prevRef}>
                  이전 버튼(img 태그 등등)
              </StyledButton>
              <Swiper {...swiperSetting}>
                      {cardList.map((card) => (
                          <SwiperSlide key={card.key}>{card}</SwiperSlide>
                      ))}
              </Swiper>
              <StyledButton ref={nextRef}>
                  다음 버튼(img 태그 등등)
              </StyledButton>
          </StyledRoot>
	);
};
  • settings.navigationprevElnextEl에 각각 ref.current를 설정해 주세요.
  • settings.onBeforeInit에 초기설정 코드를 추가해 주세요. onBeforeInit 대신에 onInit을 사용해도 됩니다. navigation 객체 내용을 변경했으므로 최신 상태로 업데이트해야 합니다.
  • 커스텀 버튼에 ref를 설정해 주세요. onClick 이벤트가 발생하기 때문에 <button> 태그를 사용하는 것이 좋습니다.

이렇게 하면 잘 동작할 것 같지만, 이대로는 반드시 문제가 발생합니다 ⚠

useRef와 useEffect

useRef는 React에서 DOM 요소를 저장하기 위해 종종 사용됩니다. 이 때, 초기값으로는 보통 null을 넣고, 추후에 DOM 트리가 생성되면 HTMLElement 요소값이 ref에 저장됩니다.

React의 구조상, 모든 JavaScript 파일을 한꺼번에 불러온 후 화면에 DOM 요소들을 paint합니다. 즉, useRef에 값이 제대로 들어가기 전에 settings 값이 설정될 수 있습니다.

const settings = {
		spaceBetween: 20,
    		navigation: {
            		prevEl: prevRef.current, // 이전 버튼
                    	nextEl: nextRef.current, // 다음 버튼
                },
    		scrollbar: { draggable: true, el: null },
        	slidesPerView: 3,
            	onBeforeInit: (swiper) => { // 초기 설정
                	swiper.params.navigation.prevEl = prevRef.current;
              		swiper.params.navigation.nextEl = nextRef.current;
                    	swiper.navigation.update();
            	},
        };

여기서 prevRef.currentnextRef.current 값이 null이 될 가능성이 매우 높습니다. 둘 중 하나는 HTMLElement이고 하나는 null이 될 수도 있습니다. 정확한 값이 보장되지 않는 것이죠.

따라서 useEffect를 사용하여 컴포넌트가 마운트될 때 settings 값을 정해줘야 안정적으로 DOM 요소를 불러올 수 있습니다. 그리고 settings는 useState를 사용해서 상태관리 했습니다.

const [swiperSetting, setSwiperSetting] = useState(null);

useEffect(() => {
	if (!swiperSetting) {
     		const settings = { ... };
     		setSwiperSetting(settings);
     	}
}, [swiperSetting]);
...
return (
	<StyledRoot>
            {swiperSetting && (
              <Swiper {...settings}>
                  ...
              </Swiper>
            )}
         </StyledRoot>

);

useEffect 내부에서 swiperSetting 값이 null이라면 setSwiperSetting(settings)를 통해서 값을 넣어줍니다. 컴포넌트가 최초로 마운트될 때 한 번 실행됩니다.
그리고 swiperSetting 값이 있을 때 <Swiper> 컴포넌트가 화면에 보여져야 정상적인 동작을 할 수 있기 때문에 return 부분에 추가적인 분기처리를 해줍니다.

3. TypeScript 적용하기


위의 코드를 모두 모아서 TypeScript를 적용하면 다음과 같습니다. Next를 사용했기 때문에 <img> 대신에 <Image>를 사용했습니다.

import 'swiper/swiper.min.css';

import Image from 'next/image';
import { ReactElement, useEffect, useRef, useState } from 'react';
import styled from 'styled-components';
import SwiperCore, { Navigation, Scrollbar } from 'swiper';
import { Swiper, SwiperSlide } from 'swiper/react';

interface MainSliderProps {
  cardList: ReactElement[]; // 슬라이드에는 컴포넌트가 들어갑니다
  slidesPerView: 3 | 4; // 한 번에 보이는 카드 수
}

function MainSlider(props: MainSliderProps) {
  const { cardList, slidesPerView } = props;

  SwiperCore.use([Navigation, Scrollbar]);

  const prevRef = useRef<HTMLButtonElement>(null);
  const nextRef = useRef<HTMLButtonElement>(null);
  const [swiperSetting, setSwiperSetting] = useState<Swiper | null>(null);

  useEffect(() => {
    if (!swiperSetting) {
      setSwiperSetting({
        spaceBetween: 24,
        navigation: {
          prevEl: prevRef.current, // 이전 버튼
          nextEl: nextRef.current, // 다음 버튼
        },
        scrollbar: { draggable: true, el: null },
        slidesPerView,
        onBeforeInit: (swiper: SwiperCore) => {
          if (typeof swiper.params.navigation !== 'boolean') {
            if (swiper.params.navigation) {
              swiper.params.navigation.prevEl = prevRef.current;
              swiper.params.navigation.nextEl = nextRef.current;
            }
          }
          swiper.navigation.update();
        },
      });
    }
  }, [swiperSetting, slidesPerView]);

  return (
    <StyledRoot>
        <StyledButton ref={prevRef}>
          <Image src={'/assets/ic_prev.svg'} width={12} height={24} alt="prev" />
        </StyledButton>
        {swiperSetting && (
          <Swiper {...swiperSetting}>
            {cardList.map((card) => (
              <SwiperSlide key={card.key}>{card}</SwiperSlide>
            ))}
          </Swiper>
        )}
        <StyledButton ref={nextRef}>
          <Image src={'/assets/ic_next.svg'} width={12} height={24} alt="next" />
        </StyledButton>
    </StyledRoot>
  );
}

const StyledRoot = styled.div`
  width: 128.8rem;
  display: flex;
  justify-content: center;
  align-items: center;
  margin-left: -8.8rem;
  button {
    padding: 0;
    background: none;
    border: none;
  }
  .swiper {
    &-wrapper,
    &-container {
      width: 120rem;
      margin: 0;
    }
    &-container {
      margin: 0 3.2rem;
    }
    &-button-disabled {
      visibility: hidden;
    }
  }
`;

export default MainSlider;

그래서 완성된 결과는 다음과 같습니다.
slider

profile
Web Frontend Developer
post-custom-banner

2개의 댓글

comment-user-thumbnail
2022년 1월 21일

멋지당

답글 달기
comment-user-thumbnail
2022년 11월 1일

궁금한것이 이렇게 한다면 슬라이드 버튼은 사라지지만 슬라이드 기능은 작동하는건가요 ?

답글 달기