무한 반복 슬라이드를 만드는데 아이디어를 얻고 싶어서 다른 분들의 자료를 참고했다.
그 중 순수 자바스크립트로 구현한 무한 반복 슬라이드 영상을 주로 참고했다.
구현과정에서 참고한 내용의 핵심은,
앞 뒤로 같은 슬라이드들을 추가해 총 3개의 슬라이드를 한 줄에 배치하고,
슬라이드의 양 끝에 도달할 때마다 반대편으로 사용자가 눈치채지 못하게 이동시켜 슬라이드를 계속해서 넘기는 것처럼 만드는 것이다.
그런데, 자바스크립트가 아닌 리액트로 구현하되, 몇 가지를 고민했다.
자료에서는 transition을 0.5s로 준 뒤, 0.5초간 슬라이드가 넘어간 뒤에 반대 끝 슬라이드로 이동시켰다. 즉, setTimeout함수로 0.5초뒤 다른 슬라이드로 이동한 것이다. 이렇게 하면 구현하기는 편하지만 다음과 같은 한계가 있을 것 같았다.
자료에서는 cloneNode를 사용해 슬라이드를 복제하여 앞뒤에 추가하였다.
그런데 현재 json 데이터로 슬라이드에 들어갈 리소스가 제공되어있는 상태였기 때문에, 굳이 부모 노드를 querySelector로 선택해온 뒤, cloneNode를 한다음, appendChild를 하는 것보다 깔끔하고 효율적인 방법을 사용하는 것이 좋은 방향이라고 생각했다.
기존에 순수 자바스크립트로 슬라이드를 만들어본 적은 있었기 때문에, 이번에는 리액트 훅을 이용해서 구현하고자 했다.
const [eventsState, setEventsState] = useState([]);
let threeTimesEvents = [];
async function loadEvents() {
const response = await fetch('assets/event.json');
const events = await response.json();
threeTimesEvents = await [...events, ...events, ...events]
await setEventsState(threeTimesEvents);
}
useEffect(() => {
setInitialPosition();
},[])
json형태로 받아온 데이터 events를 3번 펼쳐 threeTImesEvents배열에 넣었다. 이것을 그대로 eventsState에 넣어 렌더링하면 총 3개의 동일한 슬라이드 묶음이 렌더링된다. cloneNode혹은 데이터를 수동으로 3번 붙여넣기 하지 않아도 된다.
슬라이드를 수정하는 일이 있을 때도 데이터에 넣기만 하면 알아서 3개가 생성된다.
받아온 event를 화면에 렌더링한다.
return (
{eventsState.map((event) => (
<li className={styles.event}>
<img className={styles.event_img}
key={event.details}
src={event.thumbnail}
alt="event_img"
/>
</li>
))}
<div className={styles.container}>
<div className={styles.event_container}
>
<div
className={styles.event_list}
ref={slideRef}
>
{eventsState.map((event) => (
<li className={styles.event}>
<img className={styles.event_img}
key={event.details}
src={event.thumbnail}
alt="event_img"
/>
</li>
))}
</div>
</div>
배너 영역인 container안에, eventcontainer를 가운데 위치시켰다.
event_container는 overflow_hidden속성을 주어 자식 요소 중 영역밖으로 벗어난 부분은 보이지 않게 처리한다.
3개의 슬라이드만 보여줄 것이기 때문에 그만큼의 너비를 설정했다.
event_list는 슬라이드를 일렬로 배치한 것으로, 이 event_list를 좌우로 움직여 보이는 슬라이드가 좌우로 넘어가도록 만든다. 동적으로 translateX값을 줄 수 있도록 useRef를 설정했다.
const SLIDE_MARGIN = 5;
const SLIDE_WIDTH = 430;
const MAX_SLIDES = 10;
const TOTAL_SLIDES = MAX_SLIDES * 3;
슬라이드의 너비는 430px, margin은 3px
총 데이터의 수 즉 보여줄 슬라이드의 갯수는 10개, 앞뒤로 추가할 슬라이드까지 총 슬라이드 묶음의 갯수는 3개이다.
function setInitialPosition() {
slideRef.current.style.transition =
`translateX(-${(SLIDE_WIDTH + SLIDE_MARGIN) * (MAX_SLIDES - 1)})px`;
}
맨 처음 렌더링할 때 event_list를 움직여 가장 첫번째 배너가 보일 수 있도록 한다.
useEffect(() => {
loadEvents();
setInitialPosition();
},[])
const [slideState, setSlideState] = useState({
number: START,
});
useEffect(()=> {
slideRef.current.style.transform= `translateX(-${slideState.number * (SLIDE_WIDTH + SLIDE_MARGIN)}px)`;
slideRef.current.style.transition = slideState.hasMotion ? 'all 0.5s ease-in-out' : '';
},[slideState])
slideState를 두번째 매개변수로 주어 slideState이 변경할 때마다 useEffect가 호출되도록 했다.
한땀한땀 정리해 그렸다...ㅎㅎ
메인 슬라이드 묶음 앞뒤로 이전 슬라이드묶음과 다음 슬라이드 묶음이 있다.
3개씩 보여줄 것이고, 그 중 가운데 번호로 업데이트 되었을 때 가운데 오도록 이동한다.
PREV_START에 도달하면 NEXT_START로 유저가 모르게 이동해 슬라이드를 계속해 넘길 수 있도록 한다.
NEXT_END에 도달하면 PREV_END로 유저가 모르게 이동해 슬라이드를 계속해서 넘길 수 있도록 한다.
반대방향으로 이동할 때도 마찬가지다.
주의해야 할점!
원치않는 무한루프가 발생할 수 있다.
양방향으로 이동이 가능해야 하기 때문에, 단순히 slideState가 특정 번호가 되면 다른 번호로 이동하게 만들었을 경우 계속해서 반복될 수 있다.
PREV_START에서 NEXT_START로 이동한 뒤
NEXT_START에서 PREV_START로 다시 이동하게 되고,
다시 NEXT_START로 갔다가, PREV_START로.....
따라서 다음과 같이 작동해야 한다.
따라서 오른쪽으로 슬라이드를 넘기고 있을 때 NEXT_START에 도달하면 PREV_START로 이동하고, 그 뒤에는 PREV_START에서 다시 NEXT_START로 이동하지 않는다.
왼쪽으로 슬라이드를 넘기고 있을 때 PREV_START에 도달하면 NEXT_START로 이동하고, 그 반대로는 움직이지 않는다.
다른 좋은 방법이 있을 수 있지만, 스스로 충분히 고민해서 만들고 싶었다.
내가 사용한 방법은 다음과 같다.
버튼을 클릭할 때마다 slideState number는 1씩 증감한다.
slideStatenumber가 바뀔때마다 memo변수는 바뀌기 전 slideState number를 저장한다.
즉, 이동해온 방향을 알 수 있도록 하는 것이다.
예를 들어
slideState가 9이고 memo가 10이라면, 10에서 9로 이동하고 있기 때문에,
즉 오른쪽에서 왼쪽으로 이동하고 있을 때
PREV_END에서 NEXT_END로 이동하도록 만든다.
이렇게 코드를 작성하면 다시 NEXT_END에서 PREV_END로 이동하지 않는다.
반대로 NEXT_END에서 PREV_END로 이동하는 경우는 왼쪽에서 오른쪽으로 이동해 다음 슬라이드 묶음의 끝에 도달했을 경우이므로
slideState가 29이고 memo가 28이면 28에서 29로 이동하고 있기 때문에,
즉 왼쪽에서 오른쪽으로 이동하고 있을 때
NEXT_END에서 PREV_END로 이동하도록 만든다.
이러한 방식으로 slideState number와 memo가 조건을 모두 충족할 경우에만 슬라이드를 이동하도록 만들어준다.
이렇게 코드를 작성하면 버튼을 클릭할 때마다 state가 업데이트 되고 다른 슬라이드 위치로 이동하므로 슬라이드 끝에 도달했을 때는 다른 위치로 이동한 뒤 액션이 끝난다. 즉, 유저가 버튼을 클릭했음에도 다음 슬라이드로 이동하지 않는 것처럼 보인다.
이문제를 해결하기 위해서는 재빨리 다른 슬라이드 위치로 이동한 다음 다시 애니메이션 효과를 주어 그 다음 슬라이드로 이동시켜주어야 한다.
function moveTo(setNumber, setMotion) {
setSlideState({
memo: slideState.number,
number: setNumber,
hasMotion: setMotion
})
}
function slideAfterMove(setNumber, setMotion) {
setTimeout(() => {
moveTo(setNumber, setMotion)
}, 50)
}
moveTo는 다른 슬라이드 위치로 이동하는 함수이다. 유저가 눈치채지 못하게 재빨리 이동해야 하므로 애니메이션 효과없이 이동할 것이다. 따라서 hasMotion은 false이다.
slideAfterMove는 다른 슬라이드 위치로 이동한 다음 그 위치에서 다음 슬라이드로 넘겨주는 함수이다.
이때는 애니메이션 효과를 적용해 부드럽게 넘어가야 하기 때문에 hasMotion은 true이다.
function handleSlideRight() {
if(slideState.number === NEXT_END && slideState.memo === NEXT_END - 1) {
moveTo(PREV_END, false);
slideAfterMove(PREV_END + 1, true);
} else if (slideState.number === NEXT_START && slideState.memo === NEXT_START - 1) {
moveTo(PREV_START, false);
slideAfterMove(PREV_START + 1, true);
} else{
moveTo(slideState.number + 1, true);
}
}
오른쪽 버튼을 클릭할 때 실행할 함수이다.
만약 slideState number가 NEXT_END이고 memo가 1작다면, 왼쪽에서 오른쪽으로 이동하고 있으므로
moveTo함수를 이용해 PREV_END로 애니메이션 효과없이 빠르게 이동해준다.
그리고 아주잠깐의 텀을 두고 slideAfterMove함수를 사용해 그 다음 위치로 애니매이션효과로 부드럽게 넘겨준다.
그 외 평범하게 다음 슬라이드로 넘기는 경우에는 moveTo함수를 사용하되, setMotion인자를 true로 전달하여 애니메이션 효과로 부드럽게 넘겨준다.