인트로
라이브러리 없이 캐러셀을 구현하려고 합니다.
현재는 앞 뒤로 움직이는 버튼이 없이 자동으로 이미지가 오른쪽에서 왼쪽으로 스와이프 되는 모습입니다.
오른쪽에서 왼쪽으로 넘어가는 애니메이션이 없을 때는 버튼을 통해 이동할 수 있었으나 애니메이션을 추가했을 때 원하는 동작을 얻지 못해 제거하였습니다.
버튼은 다음에 추가할 예정입니다.
동작 모습

코드
전체코드
import {
TMDB_IMG_URL,
CAROUSEL_DELAY,
CAROUSEL_LENGTH_LIMIT,
CATEGORY,
TRANSITION_TIME,
} from '../../Assets/ConstantValue';
function Carousel() {
const [imgArr, setImgArr] = useState(null);
const [currentIndex, setCurrentIndex] = useState(0);
const savedCallback = useRef();
const transitionStyle = `transform ${TRANSITION_TIME}ms ease 0s`;
const [slideTransiton, setSlideTransiton] = useState(transitionStyle);
const replaceSlice = idx => {
setTimeout(() => {
setSlideTransiton('');
setCurrentIndex(idx);
}, TRANSITION_TIME);
};
useEffect(() => {
if (imgArr) {
if (currentIndex === imgArr.length - 1) {
replaceSlice(1);
} else if (currentIndex === 0) {
replaceSlice(imgArr.length - 2);
}
if (currentIndex === 2) {
setSlideTransiton(transitionStyle);
}
}
}, [currentIndex]);
const moveNextIndex = () => {
setCurrentIndex((currentIndex + 1) % imgArr.length);
};
useEffect(() => {
savedCallback.current = moveNextIndex;
}, [moveNextIndex]);
useEffect(() => {
const fetch = async () => {
const res = await CATEGORY['일간'].func;
if (res.ok) {
const response = await res.clone().json();
const movies = response.results.slice(0, CAROUSEL_LENGTH_LIMIT);
const infiniteMovie = [
movies[CAROUSEL_LENGTH_LIMIT - 1],
...movies,
movies[0],
];
const carouselImgArr = infiniteMovie.map(
movie => `${TMDB_IMG_URL}/w1280${movie.backdrop_path}`,
);
setImgArr(carouselImgArr);
}
};
fetch();
const tick = () => {
savedCallback.current();
};
const timer = setInterval(tick, CAROUSEL_DELAY);
return () => clearInterval(timer);
}, []);
return (
<div className="overflow-hidden">
<Slider
className="flex"
currentindex={currentIndex}
slidetransiton={slideTransiton}
>
{imgArr && imgArr.map(imgUrl => <img src={imgUrl} />)}
</Slider>
</div>
);
}
변수
const [imgArr, setImgArr] = useState(null);
const [currentIndex, setCurrentIndex] = useState(0);
const savedCallback = useRef();
const transitionStyle = `transform ${TRANSITION_TIME}ms ease 0s`;
const [slideTransiton, setSlideTransiton] = useState(transitionStyle);
이미지링크를 담은 배열과 현재 보여줄 이미지 설정을 위한 idx를 선언합니다.
savedCallback은 setInterval을 위한 변수입니다.
동적으로 transition 시간을 바꾸기 위한 변수도 선언해주었습니다.
currentIdx
return (
<div className="overflow-hidden">
<Slider
className="flex"
currentindex={currentIndex}
slidetransiton={slideTransiton}
>
{imgArr && imgArr.map(imgUrl => <img src={imgUrl} />)}
</Slider>
</div>
);
...
//styled부분
const Slider = styled.div`
${({ slidetransiton }) => css`
transition: ${slidetransiton};
`}
${({ currentindex }) => css`
transform: translateX(${-100 * currentindex}%);
`}
`;
기본적인 캐러셀 구현 방법은 매우 간단합니다.
이미지 배열에서 currentIndex에 해당하는 idx만 보여주면 되는 거죠.
이것은 transform에 props를 전달하여 구현할 수 있습니다.
만약 index가 0이라면 0%만큼 1이라면 -100%, 2라면 -200%만큼 Slider가 X방향으로 이동되어 index에 해당하는 이미지만 보이게 되는 겁니다.
그리고 currentIndex는 setInterval를 통해서 다음 index로 넘어가면 되겠죠.
이미지배열 설정을 위한 useEffect
useEffect(() => {
const fetch = async () => {
const res = await CATEGORY['일간'].func;
if (res.ok) {
const response = await res.clone().json();
const movies = response.results.slice(0, CAROUSEL_LENGTH_LIMIT);
const infiniteMovie = [
movies[CAROUSEL_LENGTH_LIMIT - 1],
...movies,
movies[0],
];
const carouselImgArr = infiniteMovie.map(
movie => `${TMDB_IMG_URL}/w1280${movie.backdrop_path}`,
);
setImgArr(carouselImgArr);
}
};
fetch();
//setInterval부분
const tick = () => {
savedCallback.current();
};
const timer = setInterval(tick, CAROUSEL_DELAY);
return () => clearInterval(timer);
}, []);
api를 통해서 캐러셀 이미지를 설정하기 위해 fetch를 사용하였습니다.
캐러셀에서 총 5개의 이미지를 보여줄 것이기 때문에 CAROUSEL_LENGTH_LIMIT 값은 5입니다.
setInterval
이때 useEffect 내부에 index에 변화를 주는 함수를 포함하는 setInterval를 호출할 경우 처음 마운트되는 시점에 한 번만 실행되는 문제가 있습니다. 처음 세팅된 index인 0만 기억한 채로 계속해서 0 + 1 만 하는 거죠.
물론 이러한 문제는 setStated의 인자로 함수를 사용해 prevState에 접근하여 해결할 수 있습니다.
그러나 이는 state 변경에 대해서만 해결한 방법입니다.
문제를 제대로 해결하기 위해서는 useRef를 사용해야 합니다.
ref의 current 프로퍼티에 moveNextIndex 함수를 저장합니다.
moveNextIndex 함수는 캐러셀에 띄울 이미지 index를 변경시켜 주는 함수입니다.
index state가 갱신되면 컴포넌트가 리렌더링 되기 때문에 새로운 상태를 반영한 moveNextIndex가 current에 다시 저장됩니다. 다음 interval이 실행될 때는 새로운 상태를 반영한 함수가 호출됩니다.
const moveNextIndex = () => {
setCurrentIndex((currentIndex + 1) % imgArr.length);
};
useEffect(() => {
savedCallback.current = moveNextIndex;
}, [moveNextIndex]);
여기부터는 순전히 다음 이미지가 오른쪽에서 왼쪽으로 들어오는 애니메이션을 위한 부분입니다.
따로 처리하지 않을 경우 아래처럼 동작합니다.

const infiniteMovie = [
movies[CAROUSEL_LENGTH_LIMIT - 1],
...movies,
movies[0],
];
위 코드는 무한으로 슬라이드 되는 캐러셀을 위해서 기존 배열이 [1,2,3,4,5] 였다면 [5,1,2,3,4,5,1]와 같이 마지막 배열의 데이터를 배열 처음에 추가, 기존 배열에서 첫 번째 데이터를 마지막에 추가한 모습입니다.
여기서 slideTransition 변수가 필요합니다. TRANSITION_TIME은 500으로 설정되어 있습니다.
//컴포넌트 내부
const transitionStyle = `transform ${TRANSITION_TIME}ms ease 0s`;
const [slideTransiton, setSlideTransiton] = useState(transitionStyle);
...
return (
<div className="overflow-hidden">
<Slider
className="flex"
currentindex={currentIndex}
slidetransiton={slideTransiton}
>
{imgArr && imgArr.map(imgUrl => <img src={imgUrl} />)}
</Slider>
</div>
);
...
//컴포넌트 밖
const Slider = styled.div`
${({ slidetransiton }) => css`
transition: ${slidetransiton};
`}
${({ currentindex }) => css`
transform: translateX(${-100 * currentindex}%);
`}
`;
초기에는 slidetransition 값으로 TRANSITION_TIME이 500인 transitionStyle이 전달됩니다.
그리고 마지막 배열에 도착했을 때 index값을 1로 바꿔줍니다. 배열이 [5.png,1.png,2.png,3.png,4.png,5.png,1.png]라고 했을 때 6번째 index 즉 1에 도착했을 때 인덱스 1번의 1로 currentIndex를 갱신하는 겁니다.
그리고 이때 transition 프로퍼티의 값으로 빈 문자열을 전달합니다.
빈 문자열을 전달하면 애니메이션 없이 index 1번 이미지로 변경됩니다. (하지만 배열을 보면 알 수 있듯 6번째 index의 이미지와 인덱스 1번의 이미지가 같죠.)
이후 transition 프로퍼티 값을 기존 transitionStyle로 변경해 줍니다.
const replaceSlice = idx => {
setTimeout(() => {
setSlideTransiton('');
setCurrentIndex(idx);
}, TRANSITION_TIME);
};
useEffect(() => {
if (imgArr) {
if (currentIndex === imgArr.length - 1) {
replaceSlice(1);
} else if (currentIndex === 0) {
replaceSlice(imgArr.length - 2);
}
if (currentIndex === 2) {
setSlideTransiton(transitionStyle);
}
}
}, [currentIndex]);
currentIndex를 출력한 결과입니다. 6에서 바로 1로 바뀌는 모습을 볼 수 있습니다.

이를 통해 버튼 없는 캐러셀을 구현할 수 있었습니다. 캐러셀(2)에서는 앞 뒤로 이동하는 버튼이 있고 hover 시 멈추는 캐러셀을 구현해보도록 하겠습니다.
https://ye-yo.github.io/react/2022/01/21/infinite-carousel.html 블로그를 참고하여 구현하였습니다.
'내가 해냄' 카테고리의 다른 글
| Docker로 healthcheck하기 (Docker Compose) (2) | 2024.10.21 |
|---|---|
| 웹 성능 최적화(1) (0) | 2023.11.30 |
| 이메일 클릭 시 강제 복사 시키기 (0) | 2023.04.12 |
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!