
프로젝트를 하다보면 캐러셀, 슬라이더 UI 를 구현할 일이 있다. 물론 직접 구현한다는 선택지도 있겠지만 이미 잘 만들어진 패키지를 활용하는게 효율성이나 완성도 측면에서도 더 좋다고 생각했다.
캐러셀을 쉽게 구현할 수 있도록 도와주는 라이브러리가 많다. 아래 라이브러리 외에도 다양한 선택지가 있다.
React Slick 은 예전에 사용해 본 경험이 있어서 이번에도 가장 먼저 고려했던 라이브러리인데 이번 프로젝트와는 맞지 않는 것 같아 선택하지 않았다.
이전에는 버튼으로만 슬라이드를 이동시켜서 몰랐는데 스와이프 제스처를 할 때 부자연스럽게 느껴지는 부분이 있었고, 모바일이나 데스크탑 환경에서 모두 쉽게 사용할 수 있는 UI 가 중요했기 때문에 React Slick 을 사용하지 않기로 했다.
다른 웹 페이지에서는 어떤 라이브러리를 사용하는지 궁금해서 Chrome DevTools 를 찾아봤고, Swiper 를 사용하고 있음을 알게되었다. 마침 원하는 감도의 캐러셀이었기 때문에 Swiper 를 사용하기로 결정했다.
참고로 fullPage.js 는 위 라이브러리들과 조금 결이 다른데 전체 페이지가 하나의 캐러셀이 되어 스크롤에 따라 슬라이드가 움직이도록 하는 라이브러리다. 기업 소개 페이지나 랜딩 페이지에서 많이 볼 수 있는 UI 를 쉽게 구현 가능하다. 이런 걸 (출처: @tutsplus) 쉽게 만들 수 있다.
npm i swiper
Swiper 는 컨테이너 역할을 하는 <Swiper /> 컴포넌트와 각 아이템 역할을 하는 <SwiperSlide /> 컴포넌트로 구성된다.
import { Swiper, SwiperSlide } from 'swiper/react';
import 'swiper/css';
export const Example = () => {
return (
<Swiper>
<SwipeSlide>슬라이드 1</SwipeSlide>
<SwipeSlide>슬라이드 2</SwipeSlide>
<SwipeSlide>슬라이드 3</SwipeSlide>
</Swiper>
}
위 데모 페이지에서 여러 예시를 볼 수 있다. 특히 각 예시마다 CodeSandbox 가 있어서 React, Vue 등에서 어떻게 구현하는지 코드로 볼 수 있는데, 이 예시를 보면 어떻게 원하는 UI 를 구현할지 쉽게 감을 잡을 수 있다.
Swiper 공식 문서를 보면 알 수 있지만, Swiper 는 다양한 모듈을 제공한다. 그 중 화살표 버튼으로 슬라이더를 컨트롤 할 수 있는 모듈이 바로 Navigation 모듈이다.
import { Navigation } from 'swiper/modules';
import 'swiper/css/navigation';
<Swiper modules={[Navigation]}>
위처럼 swiper/modules 에서 원하는 모듈을 import 해서 <Swiper> modules 로 지정해 주면 된다.
또한 swiper/css/navigation 을 import 해서 사용한다면 기본 스타일을 그대로 사용할 수 있다. 만약 네비게이션 버튼을 커스텀해서 사용한다면 해당 파일은 import 할 필요가 없다. (단, 레이아웃을 결정하는 'swiper/css' 파일은 import 해야 한다.)
네비게이션 버튼을 커스텀 하는 방법은 관련 클래스의 스타일을 재정의하는 방법도 있지만, 완전히 커스텀하고 싶다면 별도의 버튼 컴포넌트를 만들어 ref 로 연결하는 방법을 추천한다. 이렇게 하면 기본 네비게이션 버튼 스타일에 완전히 독립적이게 버튼을 구성할 수 있다.
타입스크립트 사용 시 다음과 같이 swiperRef 타입을 지정해 준다.
import { SwiperClass } from 'swiper/react';
interface Props {
swiperRef: React.RefObject<SwiperClass | null>;
}
방향을 props 로 받아 다음과 같이 재사용 가능한 컴포넌트를 만들자.
import { SwiperClass } from 'swiper/react';
interface Props {
swiperRef: React.RefObject<SwiperClass | null>;
direction: 'prev' | 'next';
}
export const NavigationButton = ({ sliderRef, direction }: Props) => {
const isPrev = direction === 'prev';
const onClick = () => {
if (isPrev) {
swiperRef.current?.slidePrev();
} else {
swiperRef.current?.slideNext();
}
};
return (
<div className={/* 스타일 커스텀 */}>
<button
className={/* 스타일 커스텀 */}
onClick={onClick}
>
{/* 버튼 컨텐츠 */}
</button>
</div>
);
};
위 코드의 포인트는 swiperRef.current 로 swiper 객체의 프로퍼티와 메서드에 접근할 수 있다는 점이다. 이를 활용하면 현재 슬라이더가 beginning 상태인지, end 상태인지도 알 수 있다.
참고로 NavigationButton 의 컨테이너는 absolute 로 설정하고 Swiper 컨테이너를 relative 로 설정하여 버튼을 원하는 곳에 위치시킬 수 있다.
// 스타일링 예시 (tailwind css)
<div
className={`
absolute top-0 bottom-0 ${isPrev ? 'left-1' : 'right-1'}
z-20 flex justify-center items-center
`}
>
<button
className="rounded-full p-2 bg-white shadow-sm cursor-pointer"
onClick={onClick}
>
{isPrev ? <FaChevronLeft /> : <FaChevronRight />}
</button>
</div>
가장 중요한 <Swiper /> 와 버튼을 연결하는 부분이다. <Swiper /> 의 onSwiper 를 이용해서 해당 컴포넌트가 마운트 될 때 swiperRef.current 를 다음과 같이 연결한다.
const swiperRef = useRef<SwiperClass>(null);
const onSwiper = (swiper: SwiperClass) => {
sliderRef.current = swiper;
};
return (
<div className="relative">
<NavigationButton
direction="prev"
swiperRef={swiperRef}
/>
<Swiper
onSwiper={onSwiper}
navigation={true}
modules={[Navigation]}
>
{children}
</Swiper>
<NavigationButton
direction="next"
swiperRef={swiperRef}
/>
</div>
);
마우스가 슬라이더 위에 올라갈 때만 네비게이션 버튼을 보여주고 싶다면 컨테이너에 onMouseEnter onMouseLeave 이벤트 핸들러를 지정하여 isHover 상태를 관리하면 된다. 그리고 위에서 커스텀했던 네비게이션 버튼에서 isHover 상태를 받아 조건부 렌더링 할 수 있게 수정해 보자.
export const HoverSwiper = ({ children }: {children: React.ReactNode}) => {
const [isHover, setIsHover] = useState(false);
const onMouseEnter = () => {
setIsHover(true);
};
const onMouseLeave = () => {
setIsHover(false);
};
return (
<div
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
>
<NavigationButton
direction="prev"
swiperRef={swiperRef}
isVisible={isHover}
/>
{/* 기존 코드 */}
<NavigationButton
direction="next"
swiperRef={swiperRef}
isVisible={isHover}
/>
</div>
);
}
interface Props {
swiperRef: React.RefObject<SwiperClass | null>;
direction: 'prev' | 'next';
isVisible: boolean;
}
export const NavigationButton = ({ sliderRef, direction, isVisible }: Props) => {
if (!isVisible) return null;
// 기존 코드
// ...
};
이동할 수 없는 방향의 버튼은 클릭할 수 없도록 혹은 보이지 않도록 설정하여 사용성을 더 개선할 수 있다.
즉, Swiper 의 상태에 따라 리렌더링 되어야 하기 때문에 isBeginning 과 isEnd 를 state 로 관리하면 된다.
interface Props {
children: React.ReactNode;
}
export const HoverSwiper = ({ children }: Props) => {
const sliderRef = useRef<SwiperClass>(null);
const [isHover, setIsHover] = useState(false);
const [isBeginning, setIsBeginning] = useState(true);
const [isEnd, setIsEnd] = useState(true);
const onMouseEnter = () => {
setIsHover(true);
};
const onMouseLeave = () => {
setIsHover(false);
};
const onSwiper = (swiper: SwiperClass) => {
sliderRef.current = swiper;
// 마운트 시 isBeginning, isEnd 초기값 설정
setIsBeginning(swiper.isBeginning);
setIsEnd(swiper.isEnd);
};
// slide 가 바뀔 때마다 isBeginning, isEnd 업데이트
const onSlideChange = (swiper: SwiperClass) => {
setIsBeginning(swiper.isBeginning);
setIsEnd(swiper.isEnd);
};
return (
<div
className="relative"
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
>
<NavigationButton
direction="prev"
sliderRef={sliderRef}
isVisible={isHover}
disabled={isBeginning}
/>
<Swiper
onSwiper={onSwiper}
onSlideChange={onSlideChange}
navigation={true}
modules={[Navigation]}
slidesPerView={2}
spaceBetween={16}
>
{children}
</Swiper>
<NavigationButton
direction="next"
sliderRef={sliderRef}
isVisible={isHover}
disabled={isEnd}
/>
</div>
);
};
disabled 에 따른 스타일 혹은 렌더링은 NavigationButton 에서 관리하면 된다. 간단히 렌더링을 안 하는 방식은 disabled 일 때 null 을 반환한다.
Swiper 의 breakpoints prop 을 이용하면 다음과 같이 window width 에 따라 props 를 변경할 수 있다.
// 예시
<Swiper
slidesPerView={1}
spaceBetween={8}
breakpoints={{
// when window width is >= 480px
480: {
slidesPerView: 2,
spaceBetween: 16,
}
// when window width is >= 768px
768: {
slidesPerView: 3,
spaceBetween: 16,
},
}}
>
위 코드에서 스타일과 애니메이션만 조금 추가한 완성본이다.

<SwiperSlide> 가 감싸는 각 아이템이 어떻게 구현되어 있냐에 따라 차이가 있을 수 있는데 필자는 다음과 같이 .swiper-slide 클래스의 스타일을 수정했다.
/* globals.css*/
.swiper-slide {
width: fit-content;
}
팁이라고 하기도 애매하지만(?) 만약 원하는대로 스타일링이 잘 안 된다면 Chrome DevTools 의 Elements 패널에서 swiper 의 기본 스타일을 직접 변경해보면서 swiper-slide 클래스를 수정할 것을 권한다.
이 부분은 본문에서는 다루지 않았지만, Navigation 모듈 없이 <Swiper> 컨테이너에 gradient 효과만 추가한 것이다.

동작을 정의하자면 다음과 같다.
기존에 <InternalGradient> 를 개발해뒀기 때문에 다음과 같이 (1) 움직이는지 여부 와 (2) 그래디언트가 생겨야 할 방향 만 state 로 추가했다.
export const GradientSwiper = ({ children }: Props) => {
const [isMoving, setIsMoving] = useState(false);
const [gradientDirs, setGradientDirs] = useState<('left' | 'right')[]>([]);
const updateGradientDirs = (swiper: SwiperClass) => {
const { isBeginning, isEnd } = swiper;
if (isBeginning && isEnd) {
setGradientDirs([]); // 외부 슬라이드 없음
} else if (isBeginning) {
setGradientDirs(['right']); // 오른쪽만 슬라이드 있음
} else if (isEnd) {
setGradientDirs(['left']); // 왼쪽만 슬라이드 있음
} else {
setGradientDirs(['left', 'right']); // 왼쪽, 오른쪽 모두 슬라이드 있음
}
};
// 초기 그래디언트 방향
const onSwiper = (swiper: SwiperClass) => {
updateGradientDirs(swiper);
};
// 슬라이드 변함
const onSlideChange = (swiper: SwiperClass) => {
updateGradientDirs(swiper);
setIsMoving(false);
};
// 슬라이더 움직임
const onSliderMove = () => {
setIsMoving(true);
};
useEffect(() => {
if (gradientDirs.length === 0) {
return;
}
// isMoving -> gradient 양쪽
if (isMoving) {
setGradientDirs(['left', 'right']);
}
}, [gradientDirs.length, isMoving]);
return (
<InternalGradient directions={gradientDirs}>
<Swiper
slidesPerView={'auto'}
spaceBetween={8}
onSwiper={onSwiper}
onSlideChange={onSlideChange}
onSliderMove={onSliderMove}
>
{children}
</Swiper>
</InternalGradient>
);
};