css keyFrame 사용
const { current } = containerRef;
useEffect(() => {
if (current) {
setTimeout(() => {
containerRef.current.scrollLeft =
CardsWrapperRef.current?.offsetWidth / 3;
}, 1000);
}
}, [containerRef, current]);
...
return (
<CardsWrapper
isOverflow={isOverflow}
maxLength={maxLength}
ref={CardsWrapperRef}
>
{[...waffleCards, ...waffleCards, ...waffleCards]?.map(
(waffleCard, index) => (
<StyledWaffleCard
index={index}
type={type}
key={waffleCard.id + index}
waffleCardData={waffleCard}
onClickWaffleCard={handleClickWaffleCard}
onClickLikeToggle={handleClickLikeToggle}
onClickEdit={handleClickWaffleCardEdit}
onClickDelete={handleClickWaffleCardDelete}
/>
),
)}
</CardsWrapper>
);
...
const translate = keyframes`
0% {
transform:translateX(-33.3333%)
}
100% {
transform:translateX(-66.6666%)
}
`;
const CardsWrapper = styled.ul`
...
${({ isOverflow, maxLength }) => {
if (isOverflow) {
return css`
justify-content: left;
animation-name: ${translate};
animation-duration: ${maxLength / 0.8}s;
animation-delay: 1s;
animation-iteration-count: infinite;
animation-timing-function: linear;
animation-direction: normal;
&:hover {
animation-play-state: paused;
}
`;
} else {
return css`
justify-content: center;
`;
}
}};
`;
문제점
css의 keyframe+translate 속성을 사용해 이동 애니메이션과 동시에 스크롤을 조작하기에는 한계라는 생각이 들었다
그래서 setTimeout을 이용해 시간이 지날 때마다 왼쪽으로 스크롤을 연속적으로 이동시키는 방법을 사용해야겠다고 생각했다
useEffect와 setInterval을 사용해 n초마다 state를 증가시키고, state만큼 스크롤을 이동해주는 방법을 시도했으나, 스크롤 이동 시마다 렌더링이 되기 때문에 성능면에서 너무 안좋았다
생각해보니 돔에 직접 접근해서 스크롤만 이동시켜주는 것이기 때문에 state에 의존하지 않아도 됐다. setInterval콜백함수에 직접적으로 CardList의 scrollWidth를 변경시켜주는 로직을 작성해도 정상적으로 작동했다.
그러나 후에 setInterval로 상태를 변경해야하는 상황이 있을 수 있으니 useInterval훅을 만들어서 사용했다.
setInterval에 전달할 콜백함수를 ref.current속성에 할당해, 상태 변경으로 인한 리렌더링 시 콜백함수를 매번 재할당하는 방법으로 최신 상태와 컴포넌트 환경을 보장해준다.
(함수형 setState를 이용한 방법은 state를 제외한 다른 데이터는 업데이트되지 않는 문제가 있었다.)
참고자료
카드를 순환 시키기 위해선 스크롤의 중간 지점에 시작점으로 이동시키는 로직이 필요했는데
중간지점 인식이 안됐다.
원인을 알고봤더니 container.scrollLeft === container.scrollWidth/2
의 조건이라고 했을 때, container.scrollWidth/2
한 부분에서 소수점이 발생해 조건을 충족시키지 못했기 때문이다. Math.ceil(container.scrollWidth/2)
을 해주어 해결했다
문제점
가만히 둘 때에는 스크롤 중간 지점이 인식이 잘 되는데,
직접 스크롤을 할 때에는 인식이 안되어 순환되지 않았다.
해결책
순환 말고, 카드리스트 한덩이만 애니메이션 되면서 스크롤 끝에 도달하면 애니메이션 실행이 중단되도록 수정했다.
스크롤로 앞으로 당기면 다시 애니메이션이 시작되긴 하지만 카드가 많아졌을 경우 스크롤 끝에 도달했을 때 가장 앞으로 가야할 경우 스크롤을 많이 해야해서 사용성이 안 좋을 것 같았다.
사용성을 고려해서 카드 리스트의 가장 처음/ 끝으로 이동시켜주는 버튼 구현했다
const handleClickFrontButton = () => {
containerDom.scrollLeft = 0;
setIsPlayMove(true); // 스크롤 시작점으로 돌아간 후 애니메이션을 다시 실행시켜준다
};
const handleClickBackButton = () => {
containerDom.scrollLeft = containerDom.scrollWidth;
};
버튼 관련 문제점
카드리스트 컨테이너에 마우스를 올렸을 시 버튼들이 나타나도록 구현하려고 했다
카드리스트 컨테이너 뿐만 아니라 버튼들 각각에 호버 했을 때에도 본인들이 나타나도록 구현해야했는데,
상대의 버튼까지 나타나도록 할 수가 없어서 실패했다.
hook으로 분리
애니메이션 관련 코드들을 useScrollAnimation훅으로 분리해서 WaffleCardList컴포넌트의 코드와 분리했다.
setIsPlayMove
, moveScrollToFront
, moveScrollToBack
를 반환받아 특정한 이벤트 발생 시 애니메이션 재생/중지, 스크롤 가장 앞/뒤로 이동하는 동작을 실행시킬 수 있다