라이브러리 없이 아래 나열된 기능을 기준으로 이미지 슬라이드 구현을 해보았다.
<
(이전), >
(다음) 버튼 클릭으로 슬라이드 이미지 전환무한 루프
로 마지막 이미지에서 다음 이미지로 터치 또는 클릭 시 자연스럽게 첫 번째 이미지로 이미지 전환, 첫 번째 이미지에서 이전 이미지로 터치 또는 클릭 시 마지막 이미지로 자연스럽게 이미지 전환아래와 같이 이미지 링크(string)를 배열로 받을 수 있는 컴포넌트로 구현하였다.
import React, { FC } from 'react';
import { SwiperImage } from 'components';
const Sample: FC = () => {
/* ... 생략 ...*/
const data = ['이미지 url 링크 1', '이미지 url 링크 2'];
return (
<div>
<SwiperImage data={data} />
</div>
);
};
export default Sample;
overflow: hidden
속성으로 주황색 네모칸을 벗어나는 이미지들은 숨김 처리한다.display: flex
속성으로 가로로 이미지를 나열한다.transform: translateX(-${보일 이미지 인덱스}00%)
로 이동된다.transition: all 0.4s ease-in-out
속성과 주어 자연스러운 모션을 준다.아래는 위의 설명을 구현한 소스이다.
import classNames from 'classnames';
import React, { FC, useEffect, useRef, useState } from 'react';
import { IoIosArrowBack, IoIosArrowForward } from 'react-icons/io';
interface Props {
data: string[];
}
const SwiperImage: FC<Props> = ({ data }) => {
const ref = useRef<HTMLDivElement>(null);
const [imageList, setImageList] = useState([
data[data?.length - 1],
...data,
data[0],
]);
const [currentImgIndex, setCurrentImgIndex] = useState(1);
const [style, setStyle] = useState({
transform: `translateX(-${currentImgIndex}00%)`,
transition: `all 0.4s ease-in-out`,
});
const nextSlide = () => {
setCurrentImgIndex(currentImgIndex + 1);
setStyle({
transform: `translateX(-${currentImgIndex + 1}00%)`,
transition: `all 0.4s ease-in-out`,
});
};
const prevSlide = () => {
setCurrentImgIndex(currentImgIndex - 1);
setStyle({
transform: `translateX(-${currentImgIndex - 1}00%)`,
transition: `all 0.4s ease-in-out`,
});
};
return (
<div className="relative">
<div
className="overflow-hidden max-w-[480px] min-w-[280px] w-full bg-black"
>
<div ref={ref} style={style} className={`flex`}>
{imageList?.map((el, i) => {
return (
<img
key={i}
src={el}
className={'w-auto h-auto object-contain'}
/>
);
})}
</div>
</div>
<div className="absolute w-full flex justify-between top-[50%]">
<button className="text-white text-xl" onClick={prevSlide}>
<IoIosArrowBack />
</button>
<button className="text-white text-xl" onClick={nextSlide}>
<IoIosArrowForward />
</button>
</div>
{/* ... 생략 ... */}
</div>
);
};
export default SwiperImage;
여기서 첫 번째 이미지에서 사용자가 이전 버튼을 또 클릭하게 되면 마지막 이미지로 전환되지만 자연스럽지 못하게 휘리릭 마지막으로 넘어간다.
이미지 1
) 왼쪽에 트릭을 주기 위한 가짜 마지막 이미지(이미지 4
)를 두고 마지막 이미지(이미지 4
) 오른쪽에도 트릭을 주기 위한 가짜 첫 번째 이미지(이미지 1
)를 둔다.이미지 4
에서 다음 버튼을 클릭할 경우 오른쪽에 있는 trick 이미지 1
으로 넘어가는데, 여기서 settimeout
으로 빠르게 transition: 0ms
으로 효과 없이 사용자가 눈치채지 못하도록 진짜 이미지 1
으로 이동한다.이미지 1
에서 다음 버튼을 클릭할 경우 오른쪽에 있는 trick 이미지 4
으로 넘어가는데, 여기서 settimeout
으로 빠르게 transition: 0ms
으로 효과 없이 사용자가 눈치채지 못하도록 진짜 이미지 4
으로 이동한다.0
과 인덱스 imageList[imageList.length - 1]
는 trick 이미지이기 때문에 가장 첫 번째 인덱스와 마지막 인덱스를 제외한 이미지들을 보여준다.아래는 위 내용을 구현한 소스이다.
import classNames from 'classnames';
import React, { FC, useEffect, useRef, useState } from 'react';
import { IoIosArrowBack, IoIosArrowForward } from 'react-icons/io';
interface Props {
data: string[];
}
const SwiperImage: FC<Props> = ({ data }) => {
const ref = useRef<HTMLDivElement>(null);
const [imageList, setImageList] = useState([
data[data?.length - 1],
...data,
data[0],
]);
const [currentImgIndex, setCurrentImgIndex] = useState(1);
const [touch, setTouch] = useState({
start: 0,
end: 0,
});
const [style, setStyle] = useState({
transform: `translateX(-${currentImgIndex}00%)`,
transition: `all 0.4s ease-in-out`,
});
const nextSlide = () => {
setCurrentImgIndex(currentImgIndex + 1);
setStyle({
transform: `translateX(-${currentImgIndex + 1}00%)`,
transition: `all 0.4s ease-in-out`,
});
};
const prevSlide = () => {
setCurrentImgIndex(currentImgIndex - 1);
setStyle({
transform: `translateX(-${currentImgIndex - 1}00%)`,
transition: `all 0.4s ease-in-out`,
});
};
useEffect(() => {
if (currentImgIndex === 0) {
setCurrentImgIndex(imageList.length - 2);
setTimeout(function () {
setStyle({
transform: `translateX(-${imageList.length - 2}00%)`,
transition: '0ms',
});
}, 500);
}
if (currentImgIndex >= imageList?.length - 1) {
setCurrentImgIndex(1);
setTimeout(() => {
setStyle({
transform: `translateX(-${1}00%)`,
transition: '0ms',
});
}, 500);
}
}, [currentImgIndex, imageList.length]);
useEffect(() => {
setStyle({
transform: `translateX(-${1}00%)`,
transition: '0ms',
});
}, [imageList]);
return (
<div className="relative">
<div
className="overflow-hidden max-w-[480px] min-w-[280px] w-full bg-black"
>
<div ref={ref} style={style} className={`flex`}>
{imageList?.map((el, i) => {
return (
<img
key={i}
src={el}
className={'w-auto h-auto object-contain'}
/>
);
})}
</div>
</div>
<div className="absolute w-full flex justify-between top-[50%]">
<button className="text-white text-xl" onClick={prevSlide}>
<IoIosArrowBack />
</button>
<button className="text-white text-xl" onClick={nextSlide}>
<IoIosArrowForward />
</button>
</div>
{/* ... 생략 ... */}
</div>
);
};
export default SwiperImage;
onTouchStart
e.touches[0].pageX
처음 터치한 위치를 기억해 둔다.onTouchMove
transition: 0ms
으로 이동할 위치로 이동한다 const current = ref.current.clientWidth * currentImgIndex;
const result = -current + (e.targetTouches[0].pageX - touch.start);
// 터치이동 시 이동되는 위치
setStyle({
transform: `translate3d(${result}px, 0px, 0px)`,
transition: '0ms',
});
onTouchEnd
const end = e.changedTouches[0].pageX;
if (touch.start > end) {
nextSlide();
} else {
prevSlide();
}
setTouch({
...touch,
end,
});
import classNames from 'classnames';
import React, { FC, useEffect, useRef, useState } from 'react';
import { IoIosArrowBack, IoIosArrowForward } from 'react-icons/io';
interface Props {
data: string[];
}
const SwiperImage: FC<Props> = ({ data }) => {
const ref = useRef<HTMLDivElement>(null);
const [imageList] = useState([data[data?.length - 1], ...data, data[0]]);
const [currentImgIndex, setCurrentImgIndex] = useState(1);
const [touch, setTouch] = useState({
start: 0,
end: 0,
});
const [style, setStyle] = useState({
transform: `translateX(-${currentImgIndex}00%)`,
transition: `all 0.4s ease-in-out`,
});
const nextSlide = () => {
setCurrentImgIndex(currentImgIndex + 1);
setStyle({
transform: `translateX(-${currentImgIndex + 1}00%)`,
transition: `all 0.4s ease-in-out`,
});
};
const prevSlide = () => {
setCurrentImgIndex(currentImgIndex - 1);
setStyle({
transform: `translateX(-${currentImgIndex - 1}00%)`,
transition: `all 0.4s ease-in-out`,
});
};
useEffect(() => {
if (currentImgIndex === 0) {
setCurrentImgIndex(imageList.length - 2);
setTimeout(function () {
setStyle({
transform: `translateX(-${imageList.length - 2}00%)`,
transition: '0ms',
});
}, 500);
}
if (currentImgIndex >= imageList?.length - 1) {
setCurrentImgIndex(1);
setTimeout(() => {
setStyle({
transform: `translateX(-${1}00%)`,
transition: '0ms',
});
}, 500);
}
}, [currentImgIndex, imageList.length]);
return (
<div className="relative">
<div
className="overflow-hidden max-w-[480px] min-w-[280px] w-full bg-black"
onTouchStart={(e) => {
setTouch({
...touch,
start: e.touches[0].pageX,
});
}}
onTouchMove={(e) => {
if (ref?.current) {
const current = ref.current.clientWidth * currentImgIndex;
const result = -current + (e.targetTouches[0].pageX - touch.start);
setStyle({
transform: `translate3d(${result}px, 0px, 0px)`,
transition: '0ms',
});
}
}}
onTouchEnd={(e) => {
const end = e.changedTouches[0].pageX;
if (touch.start > end) {
nextSlide();
} else {
prevSlide();
}
setTouch({
...touch,
end,
});
}}
>
<div ref={ref} style={style} className={`flex`}>
{imageList?.map((el, i) => {
return (
<img
key={i}
src={el}
className={'w-auto h-auto object-contain'}
/>
);
})}
</div>
</div>
<div className="absolute w-full flex justify-between top-[50%]">
<button className="text-white text-xl" onClick={prevSlide}>
<IoIosArrowBack />
</button>
<button className="text-white text-xl" onClick={nextSlide}>
<IoIosArrowForward />
</button>
</div>
<div className="text-gray-500 mt-4 text-center flex justify-center">
{data.map((el, i) => {
return (
<div
key={i}
className={classNames(
'bg-gray-200 h-[6px] w-[6px] mr-1 rounded',
{
'bg-rose-200': i + 1 === currentImgIndex,
}
)}
/>
);
})}
</div>
</div>
);
};
export default SwiperImage;