carousel은 슬라이더 또는 슬라이더쇼라고도 불리며 광고 배너나 이미지 슬라이더 등 많은 곳에 활용되기 때문에 웹 페이지, 앱 스크린에서의 빠질 수 없는 컴포넌트이다.
flickity는 프로젝트에 매끄럽고 생동감 있는 carousel을 적용할 수 있게 해준다.
https://flickity.metafizzy.co/
React에서 flickity를 사용하려면 이 블로그(Using Flickity with React)처럼
Slider 컴포넌트를 만들어서 사용할 수도 있는데, React Flickity Component를 활용하면 더 쉽게 리액트에서 flickity를 사용할 수 있다.
import styled from 'styled-components';
import { ReactElement, useEffect, useState } from 'react';
import Flickity, { FlickityOptions } from 'react-flickity-component';
export type SwiperProps = {
contents?: Array<ReactElement>;
onChange?: (index: number) => void;
current?: number;
style?: React.CSSProperties;
} & FlickityOptions;
function Swiper({
contents,
onChange,
current,
style,
...flickityOptions
}: SwiperProps) {
const [ref, setRef] = useState<Flickity>();
useEffect(() => {
if (!ref) {
return;
}
const handleFlktyChange = (index: number) => {
onChange(index);
};
ref.on('change', handleFlktyChange);
return () => {
ref.off('change', handleFlktyChange);
};
}, [ref]);
useEffect(() => {
if (!ref || current === undefined || current === null) {
return;
}
if (current !== ref.selectedIndex) {
ref.select(current);
}
}, [current]);
return (
<Wrapper style={style}>
<Flickity
options={{
prevNextButtons: false,
...flickityOptions,
}}
flickityRef={(c) => setRef(c)}
>
{contents}
</Flickity>
</Wrapper>
);
}
export default Swiper;
const Wrapper = styled.div`
width: 100%;
overflow: hidden;
outline: none;
`;
좌우 이동 화살표는 커스텀 버튼을 사용할 것이므로 prevNextButtons: false
를 해주었고, flickityRef
에 useRef 타입이 할당될 수 없어 useState로 Flickity타입의 ref를 선언했다.
이벤트를 ref.on의 인자로는 flickity 문서에 명세 된 flickity의 이벤트가 들어간다. (https://flickity.metafizzy.co/events.html)
import React from 'react';
import styled from 'styled-components';
import { GREY } from '@src/component/atoms/colors';
export type IndicatorType = 'Default' | 'Bullet';
export type IndicatorProps = {
current: number;
size: number;
onChange?: (index: number) => void;
style?: React.CSSProperties;
type?: IndicatorType;
};
function Indicator({
current,
onChange,
size,
style,
type = 'Default',
}: IndicatorProps) {
const handleBulletClick = (index: number) => () => {
if (!onChange) {
return null;
}
onChange(index);
};
if (size <= 1) {
return null;
}
const Item = {
Default: Default,
Bullet: Bullet,
}[type];
return (
<Wrapper style={{ ...style }}>
{[...Array(size)].map((_, index) => (
<Item
key={index}
selected={current === index}
onClick={handleBulletClick(index)}
/>
))}
</Wrapper>
);
}
export default Indicator;
const Wrapper = styled.div`
display: flex;
flex-direction: row;
justify-content: center;
margin: 0.8rem auto 0;
`;
const Default = styled.div<{ selected: boolean }>`
height: 0.2rem;
border-radius: 9999px;
&:not(:last-child) {
margin-right: 0.6rem;
}
transition: all 0.5s;
cursor: pointer;
${({ selected }) => `
width: ${selected ? 1.6 : 1.2}rem;
background-color: ${selected ? GREY[900] : GREY[300]};
`}
`;
const Bullet = styled.div<{ selected: boolean }>`
width: 0.8rem;
height: 0.8rem;
border-radius: 9999px;
&:not(:last-child) {
margin-right: 0.8rem;
}
transition: all 0.5s;
cursor: pointer;
${({ selected }) => `
background-color: ${selected ? GREY[900] : GREY[300]};
`}
`;
import React, { useState } from 'react';
import styled from 'styled-components';
import { Img, Swiper } from '@src/component/atoms';
import ImageSliderControl from './control';
import Indicator from '../../indicator';
export type ImageSliderProps = {
images: string[];
style?: React.CSSProperties;
imageStyle?: React.CSSProperties;
hasIndicator?: boolean;
hasControl?: boolean;
onClick?: (index: number) => void;
};
function ImageSlider({
images,
style = {},
imageStyle = {},
hasIndicator = true,
hasControl = true,
// eslint-disable-next-line @typescript-eslint/no-empty-function
onClick = () => {},
}: ImageSliderProps) {
const [current, setCurrent] = useState(0);
return (
<Wrapper
style={style}
onClick={() => {
onClick(current);
}}
>
<Swiper
current={current}
onChange={setCurrent}
contents={images.map((image) => (
<Img
src={image}
alt={image}
width="36rem"
height="36rem"
size={512}
style={{ userSelect: 'none', ...imageStyle }}
/>
))}
style={{ width: '36rem' }}
/>
{hasControl && (
<ImageSliderControl
current={current}
onChange={setCurrent}
size={images.length}
/>
)}
{hasIndicator && (
<Indicator
current={current}
onChange={setCurrent}
size={images.length}
type="Default"
/>
)}
</Wrapper>
);
}
export default React.memo(ImageSlider);
const Wrapper = styled.div`
display: flex;
flex-direction: column;
align-items: center;
position: relative;
overflow-x: hidden;
`;
.flickity-enabled {
outline: none
}
슬라이더 클릭시 파란색 border가 생기는 것을 없애기 위해 css를 추가해주었다.