깃허브 주소 ▼
개요
부족한 부분 발굴
본격적으로 페이지들을 개발하기 시작했다. 그러나 다른 페이지들을 만들기 전에 우선 메인 페이지를 완성해야하기에 메인 페이지의 부족한 부분들을 확인했다.
부족한 부분들을 확인하는데, 빨간 네모박스 쳐진 공간이 너무 비어보였다. 모바일 버전에서는 이 부분이 없이 사각형 버튼 2개만 있었기에 공간을 넓히고 보니 이렇게 빈 공간이 생겼다. ▼
그래서 전에 HTML과 CSS만으로 디자인했던 임시 디자인에서는 그 부분을 오늘의 딜레마로 채워놨던게 생각이 나, 지금 프로젝트에도 적용하기로 했다. ▼
그런데 하나만 덩그러니 보여주기엔 좀 밋밋한 감이 있어, 이 부분을 무한 슬라이드로 구현해 넣기로 했다.
무한 슬라이드 구현
로직
무한 슬라이드가 어떻게 동작할 지 로직부터 생각을 해봤다. 내가 원하는 무한 슬라이드에는 오른쪽에서 왼쪽으로 가는 애니메이션이 필요하기에 아래와 같은 로직을 생각해두었다. ▼
- 보여줄 슬라이드들을 가로로 일렬로 배치.
- 매 시간이 지나면 각 슬라이드의 X축을 자신의 너비 만큼 왼쪽으로 이동.
슬라이드
스타일링
다음으로는 무한 슬라이드를 만들기 위해서는 아주 당연하게도 슬라이드가 필요하기에 슬라이드를 스타일링 해보았다. ▼
const StyledSlide = styled.div`
width: 100%;
flex-shrink: 0;
background-color: ${theme.color.primary};
display: flex;
height: 150px;
align-items: center;
justify-content: center;
cursor: pointer;
`;
여기서 `flex-shrink: 0;`을 해놓은 이유는 가로 일렬로 배치해놓은 슬라이드들이 `display: flex;`인 wrapper로 감싸져 있기 때문이다. `display:flex;`인 wrapper내부의 요소들은 각자 균일하게 그 너비를 가져가려고 하기 때문에, 각자 자신의 너비를 유지하게끔 `flex-shrink: 0;`을 넣어주었다.
슬라이드 제목은 아래와 같이 넣었다. ▼
const StyledFeatureTitle = styled.div`
${theme.fontstyle.display}
color: ${theme.color.black};
`;
그리고 애니메이션을 넣어주었다. ▼
const StyledSlideshowWrapper = styled.div`
display: flex;
transition: transform 0.5s ease-in-out;
transform: translateX(${({ currentIndex }) => -currentIndex * 100}%);
`;
마지막으로 앞서 만든 `ButtonContainer`의 오른편에 제대로 자리할 수 있게 아래와 같은 `Wrapper`로 다시 감싸주었다. ▼
const StyledSlideshowContainer = styled.div`
width: 100%;
overflow: hidden;
border-radius: ${theme.radius.radiusMd};
position: relative;
`;
컴포넌트화
앞서 만든 스타일들과 `useEffect`, `setInterval`을 사용하여 매 3초마다 왼쪽으로 이동하게 했다. ▼
function FeatureBanner({ slides }) {
const [currentIndex, setCurrentIndex] = useState(0);
useEffect(() => {
const intervalId = setInterval(() => {
setCurrentIndex((prevIndex) => (prevIndex + 1) % slides.length);
}, 3000);
return () => clearInterval(intervalId);
}, [currentIndex]);
return (
<StyledSlideshowContainer>
<StyledSlideshowWrapper currentIndex={currentIndex}>
{slides.map((slide, index) => (
<StyledSlide
key={index}
style={{ backgroundColor: slide.color }}
onClick={() => alert(`Slide ${index + 1} clicked!`)}>
<StyledFeatureTitle>{slide.title}</StyledFeatureTitle>
</StyledSlide>
))}
</StyledSlideshowWrapper>
</StyledSlideshowContainer>
);
}
그리고 임시로 슬라이드 모델을 만들어 아래와 같이 넣은 채로 export 해주었다. ▼
const slides = [
{ title: "슬라이드 1", color: theme.color.darkgray },
{ title: "슬라이드 2", color: theme.color.primary },
{ title: "슬라이드 3", color: theme.color.gray500 },
{ title: "슬라이드 4", color: theme.color.gray500 },
{ title: "슬라이드 5", color: theme.color.gray500 },
];
export default function BannerBanner() {
return (
<div>
<FeatureBanner slides={slides} />
</div>
);
}
이렇게 해서 슬라이드를 구현할 수 있었다! ▼
트러블 슈팅
문제 발생
슬라이드를 제대로 구현한 줄 알았으나 한 가지 문제가 있었으니, 마지막 인덱스에서 첫번째 인덱스로 돌아오면 오른쪽으로 빠르게 이동한다는 것이었다. ▼
내가 원하는 진정한 의미의 무한 슬라이드는 오른쪽으로 넘어가는 일 없이 무한정 왼쪽으로만 가는 것이다. 하지만 이건 결국에는 오른쪽으로 이동해버리니 오른쪽으로 가지 않게 수정을 해보았다.
첫번째 아이디어 : 이동할 때마다 뒤에 인덱스 붙이기
아이디어와 구현
두번째 아이디어는 계속해서 뒤에 인덱스를 붙이는 것이다. 예를 들면
$[1, 2, 3, 4, 5]$
라는 배열이 들어왔다고 하면, 3번 슬라이드가 지났을 때는
$[1, 2, 3, 4, 5, 1, 2, 3]$
가 된다. 이러면 말 그대로 무한 슬라이드가 된다.
문제점
하지만 이 문제는 알겠지만, 시간이 지날수록 메모리를 많이 먹게 된다. '슬라이드 메모리가 많아봐야 뭐 얼마나 많겠어' 하며 넣을 수도 있지만, 추후에 이미지도 들어가게 되면 사용 메모리가 증가하는 폭이 커지게 된다.
그래서 이 부분을 해결하고자 슬라이드를 복사하고 한 번 순회가 진행되면 전부 제거하는 로직을 넣었다.
인덱스를 증가시키다가 가장 처음에 들어왔던 슬라이드의 개수만큼 이동하게 되면 새롭게 만들어진 슬라이드에는 기존의 슬라이드만큼 다시 들어가 있게 된다. 그리고 새롭게 들어간 슬라이드로 이동한 순간, 기존에 있던 슬라이드를 모두 제거하고 맨 처음 인덱스로 이동한다.
허나, 슬라이드가 제대로 제거되지 않아 슬라이드의 전체 길이가 길어지면서 너비 계산에 문제가 생겨 이 방식을 사용하는 것은 포기했다.
두번째 아이디어 : 맨 앞의 인덱스를 뒤에 붙이고 눈속임
아이디어와 구현
두번째 아이디어는 맨 앞의 인덱스를 뒤에 붙이고 눈속임을 하는 것이다. 이게 무슨 의미인가 싶을 건데 예시를 들어보면 이렇다. 매개변수로 들어온 슬라이드가
$ [1, 2, 3, 4, 5] $
라고 해보자. 그러면 컴포넌트가 처음 렌더링 될 시점에 맨 앞의 인데스를 뒤에 붙여 새로운 인덱스를 만들어준다. 그러면 새롭게 만들어진 슬라이드는
$ [1, 2, 3, 4, 5, 1] $
이 된다. 이렇게 만들어진 슬라이드를 이제 무한 루프를 돌리는데, 마지막 인덱스가 됐을 때 애니메이션을 꺼버리면 맨 처음으로 돌아가는 애니메이션이 동작하지 않게 된다. 그렇기에 실제로는 1번 슬라이드로 이동했지만 애니메이션이 동작하지 않았기에, 사용자가 보기에는 오른쪽으로 가는 애니메이션 없이 왼쪽으로만 계속 이동하게 된다.
문제점
하지만 문제점이 하나 있었으니, 이렇게 되면 맨 처음 슬라이드에서 2배의 시간을 머무르게 된다는 것이다. 이유를 보면 이렇다. 아까와 같이 배열을
$ [1, 2, 3, 4, 5, 1] $
로 만들었다고 해보자. 그리고 각 슬라이드에서 3초를 기다린다고 해보자. 그러면
$ [1, 2, 3, 4, 5, 1] $
$ [3, 3, 3, 3, 3, 3] $
의 시간으로 머무르게 된다. 하지만 우리의 슬라이드는 실제로 보기에는
$ [1, 2, 3, 4, 5, 1, 2, 3, 4, 5, 1, ...] $
로 보이지만 실상은 1이 중복되어
$ [1, 2, 3, 4, 5, 1, 1, 2, 3, 4, 5, 1, 1, ...] $
6초를 머무르게 된다. 그렇기에 마지막 인덱스에서 머무르는 시간을 없애고 애니메이션도 해제를 해야했다.
해결
처음에는 이를 구현하기 위해 `useEffect`를 하나만 사용했다. 루프를 돌리기 위한 `useEffect`로 `setInterval`만 들어가 있었는데, 단일 `useEffect`로는 마지막 간격을 초기화할 수 가 없었다. 이미 `setInterval`은 설정이 되어있고, 마지막 인덱스에서 `setInterval`로 들어간 `interval` 변수를 `0`으로 바꾼다고 한들 이미 실행단에는 `3000`으로 넘어가있기에 소용이 없었다. 그래서 `useEffect`를 하나 더 사용하여 `setTimeout`을 적용했다.
해당 `useEffect`는 마지막 인덱스에 도달하면, 움직이는 시간동안(500ms) 첫 인덱스로 돌리고, 애니메이션을 없앤다.
즉, 이렇게 있던 슬라이드에서
$[1, 2, 3, 4, 5, 1]$
5번 슬라이드에서 마지막 슬라이드인 1번 슬라이드로 넘어가는 트랜지션이 끝남과 동시에 애니메이션을 끄고, 가장 첫 인덱스로 돌려 오른쪽으로 넘어가는 애니메이션을 없애고 대기 시간또한 없앴다. ▼
const [currentIndex, setCurrentIndex] = useState(0);
const [loopSlides, setLoopSlides] = useState([...slides, slides[0]]);
const [isAnimating, setIsAnimating] = useState(true);
const initialSlidesLength = slides.length;
const interval = 1000;
const currentIndexRef = useRef(currentIndex);
currentIndexRef.current = currentIndex;
useEffect(() => {
const intervalId = setInterval(() => {
setIsAnimating(true);
setCurrentIndex((prevIndex) => prevIndex + 1);
}, interval);
return () => clearInterval(intervalId);
}, [initialSlidesLength, slides]);
useEffect(() => {
if (currentIndex === initialSlidesLength) {
setTimeout(() => {
setIsAnimating(false);
setCurrentIndex(0);
setLoopSlides([...slides, slides[0]]);
}, 500);
}
}, [currentIndex, initialSlidesLength, slides]);
해결하여 적용한 전체 코드는 아래의 더보기를 누르면 볼 수 있다. ▼
전체 코드
import React, { useState, useEffect, useRef } from "react";
import styled from "styled-components";
import { theme } from "./ui/Theme";
const StyledSlideshowContainer = styled.div`
width: 100%;
overflow: hidden;
border-radius: ${theme.radius.radiusMd};
position: relative;
`;
const StyledSlideshowWrapper = styled.div`
display: flex;
transition: ${({ isAnimating }) =>
isAnimating ? "transform 0.5s ease-in-out" : "none"};
transform: translateX(${({ currentIndex }) => -currentIndex * 100}%);
`;
const StyledSlide = styled.div`
width: 100%;
flex-shrink: 0;
background-color: ${theme.color.primary};
display: flex;
height: 150px;
align-items: center;
justify-content: center;
cursor: pointer;
`;
const StyledFeatureTitle = styled.div`
${theme.fontstyle.display}
color: ${theme.color.black};
`;
function FeatureBanner({ slides }) {
const [currentIndex, setCurrentIndex] = useState(0);
const [loopSlides, setLoopSlides] = useState([...slides, slides[0]]);
const [isAnimating, setIsAnimating] = useState(true);
const initialSlidesLength = slides.length;
const interval = 1000;
const currentIndexRef = useRef(currentIndex);
currentIndexRef.current = currentIndex;
useEffect(() => {
const intervalId = setInterval(() => {
setIsAnimating(true);
setCurrentIndex((prevIndex) => prevIndex + 1);
}, interval);
return () => clearInterval(intervalId);
}, [initialSlidesLength, slides]);
useEffect(() => {
if (currentIndex === initialSlidesLength) {
setTimeout(() => {
setIsAnimating(false);
setCurrentIndex(0);
setLoopSlides([...slides, slides[0]]);
}, 500);
}
}, [currentIndex, initialSlidesLength, slides]);
return (
<StyledSlideshowContainer>
<StyledSlideshowWrapper currentIndex={currentIndex} isAnimating={isAnimating}>
{loopSlides.map((slide, index) => (
<StyledSlide
key={index}
style={{ backgroundColor: slide.color }}
onClick={() => alert(`Slide ${index + 1} clicked!`)}>
<StyledFeatureTitle>{slide.title}</StyledFeatureTitle>
</StyledSlide>
))}
</StyledSlideshowWrapper>
</StyledSlideshowContainer>
);
}
const slides = [
{ title: "슬라이드 1", color: theme.color.darkgray },
{ title: "슬라이드 2", color: theme.color.primary },
{ title: "슬라이드 3", color: theme.color.gray500 },
{ title: "슬라이드 4", color: theme.color.gray500 },
{ title: "슬라이드 5", color: theme.color.gray500 },
];
export default function BannerBanner() {
return (
<div>
<FeatureBanner slides={slides} />
</div>
);
}
마치며
무한 슬라이드 구현을 하는데서 시간을 좀 먹었다. `useEffect`, `setInterval`, `setTimeout` 등등 다양한 메소드들을 처음 접해봤기에 이 사용에 대해서 익혀야했고, 또 원하는대로 실행이 되지 않아 어느부분이 문제인지 찾는데도 시간이 걸렸다. 그래도 성공적으로 구현을 했으니 이제는 본격적으로 핵심 페이지들을 만들 차례다.
'Develop > React' 카테고리의 다른 글
[React][개발기] CI/CD 도입 (0) | 2024.08.08 |
---|---|
[React][개발기] 9. 리팩토링과 총 점검 (0) | 2024.07.19 |
[React][개발기] 8. Login 모달, 그리고 전체 점검의 필요성 (0) | 2024.07.18 |
[React][개발기] 7. RoutePaths, NavBarData 등 데이터를 효율적으로 (0) | 2024.07.17 |
[React][개발기] 6. Stateless하기 만들기, 페이지 만들기, 그리고 Map 활용 (0) | 2024.07.17 |