라이브러리 없이, 무한 슬라이드 구현하기
카테고리: React
태그: 무한 슬라이드
📌 구현한 슬라이드 소개
슬라이드 라이브러리를 사용하지 않고, 무한 슬라이드
를 구현했다. 슬라이드 밑에는 전체 이미지들을 담고 있는 앨범을 만들었다. 사용자가 현재 보고 있는 슬라이드와 맞게 앨범의 배경색도 변경된다. 그리고 앨범의 이미지를 클릭해도 슬라이드가 같이 넘어가도록 만들었다.
📌 무한 슬라이드 만들기
📍 슬라이드 복제하기
기본적으로 무한 슬라이드를 구현하기 위해서, 메인 슬라이드의 앞뒤로 슬라이드를 추가로 복제
했다.
위의 이미지와 같이, 제품 상세 이미지가 10장이라면 앞뒤로 10장씩을 더 만들어 총 30장의 슬라이드를 기본적으로 세팅해 놓았다.
// ProductWrapper.jsx
export default function ProductWrapper({ product: { imageList } }) {
const [banners, setBanners] = useState([]);
}
useEffect(() => {
setBanners([...imageList, ...imageList, ...imageList]);
setBanners((bannerList) => {
const result = bannerList.map((imgUrl) => ({
key: v4(),
imgUrl,
}));
return result;
});
}, []);
실제 코드를 살펴보자. 우선 부모 컴포넌트에서 제품 상세 이미지를 담고 있는 product를 props로 전달받았다. 그리고 바로 구조 분해 할당을 통해서 제품 상세 이미지가 들어있는 배열인 imageList를 변수로
선언했다.
그다음에는 useEffect에 빈 배열을 넣어, 컴포넌트의 최초 렌더링 시 setBanners() 함수를 통해 imageList를 복제하게 된다.
📍 무한 슬라이드를 위한 핵심 아이디어
이제는 슬라이드가 넘어가는 것을 관찰하고 있다가, main 슬라이드를 벗어나면
사용자가 눈치채지 못하도록 다시 main 슬라이드의 이미지로 슬라이드를 다시 이동
시키는 동작이 필요하다. 이를 위해서는 많은 상수값을 변수로 선언해야 한다. 하나씩 코드를 살펴보자.
// ProductWrapper.jsx
// 화면에 보이는 slide의 width 값
const SLIDE_WIDTH = 65;
// mainBanners를 앞뒤로 복사했으므로 mainBanners의 length만을 알기 위해서 3을 나눈다.
const BANNERS_COUNT = banners.length / 3;
// 총 banners의 length는 BANNERS_COUNT에 3을 곱하면 나온다.
const TOTAL_BANNERS_COUNT = BANNERS_COUNT * 3;
// mainSlide의 시작 index는 TOTAL_BANNERS_COUNT 중에서 11번째가 된다.
const START = (TOTAL_BANNERS_COUNT * 1) / 3 + 1;
// mainSlide의 마지막 index는 20이다.
const END = (TOTAL_BANNERS_COUNT * 2) / 3;
// previousButton을 클릭하여 mainSlide를 벗어나는 것을 인지하는 기준은 10번째 index이다.
const PREVIOUS_END = (TOTAL_BANNERS_COUNT * 1) / 3;
// nextButton을 클릭하여 mainSlide를 벗어나는 것을 인지하는 기준은 21번째 index이다.
const NEXT_START = (TOTAL_BANNERS_COUNT * 2) / 3 + 1;
위의 코드에서 SLIDE_WIDTH = 65
는 65vw를 의미한다. 내가 만든 무한 슬라이드는 한 장씩 슬라이드가 넘어가는데 이때 해당 슬라이드의 width가 65vw이다. 만약 px을 기준으로 해서 400px이라면 값이 400이 될 것이다.
위의 const 키워드로 선언한 상수값들은 제품 상세 이미지가 10장 들어있어서, 앞뒤로 슬라이드 묶음을 복제하여 TOTAL_BANNERS_COUNT의 값이 30
이라고 가정하고 설명하고 있다. 또한 PREVIOUS_END
와 NEXT_START
값으로, 사용자가 넘기고 있는 슬라이드가 main 슬라이드를 벗어났는지 확인하는 기준이 된다.
이제는 현재 사용자가 보고 있는 슬라이드의 정보가 담긴 slide 변수
와 사용자가 슬라이드를 넘기기 위해 previousButton과 nextButton을 클릭하면 발생할 이벤트 핸드러
를 소개하려고 한다. 코드는 아래와 같다.
// ProductWrapper.jsx
const [slide, setSlide] = useState({
number: START,
isMotion: true,
});
slide 객체는 현재 사용자가 보고 있는 slide의 index 번호를 나타내는 slide.nubmer 프로퍼티에 담게 된다. 그리고 슬라이드를 눌러 다음 슬라이드로 이동할 때의 motion CSS의 여부를 true, fasle로 slide.isMotion 프로퍼티에 담게 된다. 그러면 이제 isMotion의 값이 false인 경우
가 왜 필요하지 궁금증을 가지게 될 것이다.
글의 초반부부터 한 가지 반복해서 이야기하고 있는 부분이 있었다. 바로 클라이언트(사용자)가 슬라이드를 넘기다가 main 슬라이드를 벗어나면 사용자가 눈치채지 못하도록 다시 main 슬라이드의 이미지로 이동시키는 행동
이 필요하다고 강조했다. 이 상황을 더 자세히 이야기해 보자면 main 슬라이드의 영역을 index로 말해보면 11~20번까지이다. 1~10번, 21~30번의 슬라이드는 앞뒤로 복제된 슬라이드이다. 그래서 main 슬라이드에서 첫 번째 사진은 11번 사진이고 이와 같은 사진은 1번 사진과 21번 사진이 된다.
- main 슬라이드의 첫 번째 사진은 11번 사진이고, 이와 같은 사진은 1번, 21번이다.
- main 슬라이드의 마지막 사진은 20번 사진이고, 이와 같은 사진은 10번, 30번이다.
따라서 사용자가 main 슬라이드의 마지막 사진(20번)에서 nextButton을 클릭했다면, 다음 사진인 21번으로 이동하고 아까 위에서 선언한 상수값인 NEXT_START에 현재 슬라이드가 도달
했으므로 21번과 똑같은 main 슬라이드의 11번 사진으로
사용자가 눈치채지 못하도록 또 한 번 이동시킨다. 이때는 isMotion에 false
를 넣어서 motion이 없도록 하여 사용자가 눈치채지 못하도록 만들었다. 이 부분이 무한 슬라이드를 구현하기 위해 사용한 핵심적인 이론 부분이다.
📍 이벤트 핸들러 소개
📎 goToBanner() 함수
// ProductWrapper.jsx
export default function ProductWrapper() {
const [slide, setSlide] = useState({
number: START,
isMotion: true,
});
const goToBanner = useCallback(({ targetName, isMotion }) => {
setSlide(updateSlide({ targetName, isMotion }));
});
return <Slide goToBanner={goToBanner} />;
}
// Slide.jsx
function handleClickArrowButtons({ currentTarget: { title: targetName } }) {
if (targetName === "previousArrow") {
return isPassTheFirstSlide
? goToMainEndSlide({ targetName, isMotion: false })
: goToBanner({ targetName, isMotion: true });
}
return isPassTheLastSlide
? goToMainStartSlide({ targetName, isMotion: false })
: goToBanner({ targetName, isMotion: true });
}
goToBanner() 함수는 previousArrow 버튼이나 nextArrow 버튼을 클릭할 때 실행되는 함수이다. slide의 버튼을 클릭하면 Slide.jsx의 함수인 handleClickArrowButtons() 함수가 실행된다. 이때 슬라이드가, 우리가 관찰하고 있는 지점에 도달하지 않았을 때
바로 goToBanner() 함수가 실행된다.
위의 코드(ProductWrapper.jsx)에서는 테스트 코드 작성을 위해 함수를 모듈화하여 관심사를 분리시켰으므로 함수가 추상화되어있다. 더 자세한 실제 코드는 아래와 같다.
// utils.js
export function updateSlide({ targetName, isMotion }) {
if (targetName === "previousArrow") {
return ({ number }) => ({
number: number - 1,
isMotion,
});
}
return ({ number }) => ({
number: number + 1,
isMotion,
});
}
// ProductWrapper.jsx
const goToBanner = useCallback(({ targetName, isMotion }) => {
setSlide(updateSlide({ targetName, isMotion }));
});
최종적으로 정리하면 goToBanner() 함수가 실행되는 조건은, 사용자가 버튼을 클릭하여 슬라이드가 움직이는 범위가 복제하지 않은 main 슬라이드 영역
일 때만 호출된다.
📎 goToMainEndSlide() 함수와 goToMainStartrSlide() 함수
// Slide.jsx
function handleClickArrowButtons({ currentTarget: { title: targetName } }) {
if (targetName === "previousArrow") {
return isPassTheFirstSlide
? goToMainEndSlide({ targetName, isMotion: false })
: goToBanner({ targetName, isMotion: true });
}
return isPassTheLastSlide
? goToMainStartSlide({ targetName, isMotion: false })
: goToBanner({ targetName, isMotion: true });
}
goToMainEndSlide() 함수와 goToMainStartSlide() 함수는 어느 버튼을 눌렀느냐의 차이만 있을 뿐, 동작의 원리는 완전히 똑같은 함수이다.
사용자가 위의 이미지와 같이 previousArrow 버튼(이전 버튼)을 클릭했다고 가정해 보자. 그러면 handleClickArrowButtons() 함수가 실행된다. 이때 인자로 currentTarget을 전달함으로 previousArrow 버튼을 클릭했다는 사실도 같이 전달된다. 그렇게 조건문(if 문) 안으로 코드의 흐름이 이동하여 isPassTheFirstSlide
의 값에 따라 실행되는 함수가 분기되게 된다.
// ProductWrapper.jsx
const isPassTheFirstSlide = slide.number === PREVIOUS_END;
const isPassTheLastSlide = slide.number === NEXT_START;
const goToMainEndSlide = useCallback(
({ targetName, isMotion }) => {
setSlide({
number: END,
isMotion,
});
setTimeout(() => {
goToBanner({
targetName,
isMotion: true,
});
}, 50);
},
[slide]
);
isPassTheFirstSlide를 설명하기에 앞서 먼저 디테일한 상황을 가정해 보자. main 슬라이드의 첫 번째 사진의 index는 11번이다. 그리고 previousArrow 버튼을 클릭하여 main 슬라이드를 벗어난 10번 사진(main 슬라이드의 가장 마지막 사진인 20번 사진과 같다.)에 위치하고 있다. 이때 isPassTheFirstSlide
의 값이 false에서 true가 되었다. 왜냐하면 PREVIOUS_END의 값을 10으로 미리 지정해 놓았기 때문이다.
그래서 10번 사진에서 9번 사진으로 이동시키기 위해 previousArrow 버튼을 클릭하면 isPassTheFirstSlide가 true이므로 삼항 조건 연산자에 따라 goToMainEndSlide() 함수가 실행된다. goToMainEndSlide() 함수의 로직은 위의 코드와 같다.
goToMainEndSlide() 함수가 실행된 시점의 슬라이드 상황은, 10번 이미지(복제된 previous 슬라이드의 가장 마지막 이미지)에서 이제 9번 이미지로 슬라이드가 이동해야 하는 상황이다. 그런데 goToMainEndSlide() 함수의 로직을 살펴보면, 슬라이드를 END(main 슬라이드의 마지막 이미지인 20번 이미지)로 이동
시킨다. 사용자가 눈치채지 못하도록 main 슬라이드의 이미지로
motion 효과를 주지 않고 이동시킨다. 그리고는 9번 이미지로 이동시키는 동작을 9번과 같은 사진인 19번 이미지로 한 칸 이동시킨다. 이때는 isMotion에 다시 true를 주어서 효과를 넣어준다.
- 10번 이미지(복제된 previous 슬라이드의 가장 마지막 이미지) → 20번 이미지(main 슬라이드의 가장 마지막 이미지) → 19번 이미지(isMotion의 값에 true를 넣어서 슬라이드가 넘어가는 효과를 준다.)
📍 슬라이드가 넘어가는 motion 효과
// ProductWrapper.jsx
// 초기 슬라이드를 main 슬라이드인 10번 사진에 맞추기 위한 useEffect
useEffect(() => {
function setInitialPosition() {
slideRef.current.style.transform = `translateX(-${
SLIDE_WIDTH * (START - 1)
}vw)`;
setSlide({
number: START,
isMotion: false,
});
}
setInitialPosition();
}, [banners]);
// 버튼을 눌렀을 때, 슬라이드가 넘어가는 효과를 주기 위한 useEffect
useEffect(() => {
slideRef.current.style.transform = `translateX(-${
SLIDE_WIDTH * (slide.number - 1)
}vw)`;
slideRef.current.style.transition = slide.isMotion ? "all 0.5s ease-in" : "";
}, [slide, SLIDE_WIDTH]);
첫 번째 useEffect는 화면이 렌더링 될 때, main 슬라이드의 첫 번째 이미지(10번 이미지)에 맞추기 위한 함수이다. 두 번째 useEffect는 slide가 바뀔 때마다 호출된다. 이로써, 사용자가 버튼을 누를 때마다 해당 useEffect가 실행하게 된다. 현재 slide가 나타내는 사진의 index 번호에 SLIDE_WIDTH(65vw)를 곱하여 슬라이드를 넘기게 된다.
📌 결론
무한 슬라이드를 구현하기 위한 방법은 다양할 것이다. 그중에서 나는 slide의 이미지들을 앞뒤로 묶어서 복제하는 방법을 사용하였다. 사실 이것이 효율적인 방법인지는 모르겠다. 그러나 나와 같이 무한 슬라이드를 직접 한 번 만들어보며 원리를 이해해 보고 싶은 분에게는 도움이 조금이나마 되었으면 좋겠다. 현재 위의 코드를 담고 있는 repository는 계속해서 업데이트되고 있기 때문에 GitHub에 들어가더라도 그 구조가 많이 달라질 것이다. 그러나 파일명을 주석으로 처리해 두었으니, 전체적인 코드를 보고 싶으신 분이 있다면 아래의 주소를 참고하셨으면 좋겠다.
💡 gunhee’s GitHub: shopifyStore
댓글남기기