[리팩토링] 카드 리스트 애니메이션 구현 시도 (1)

yoon Y·2022년 3월 14일
0

[2nd_Project] WaffleCard

목록 보기
13/15

요구사항

  • 카드들이 자동으로 옆으로 이동하면서 순환하는 애니메이션
  • isOverflow가 true일 때 애니메이션 실행
  • 카드 컨테이너에 마우스 hover시 애니메이션 중지, 해제하면 다시 실행
  • 스크롤 하면 애니메이션 멈추고 스크롤 되도록

기능 구현 방법

  • 전체 카드 각각을 초마다 특정한 범위만큼 이동시켜준다 (transform:translate)
  • 첫번째 카드 앞에 마지막 카드 복제, 마지막 카드 뒤에 첫번째 카드를 복제한다
  • 마지막 카드 뒤에 복제한 첫번째 카드가 나오는 것까지 애니메이션을 걸어주고 끝나면 원본 카드위치로 이동한다
  • css keyframe animation을 이용한다

구현 시도1

css keyFrame 사용

  • data를 3번 복사해서 3세트의 카드리스트 컴포넌트들을 생성
  • 처음 렌더링 시 1세트의 카드리스트 너비만큼 스크롤을 당김(그래야 역방향으로 스크롤 가능)
  • keyFrame을 이용해 2세트~3세트 반복하며(순환) 카드들이 왼쪽으로 이동하도록 애니메이션 설정
     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;
              `;
            }
          }};
	     `;

문제점

  • 3세트 카드들이 나올 때 화면에 늦게 그려진다 (Html상에는 존재한다)
  • 카드 리스트가 애니메이션으로 이동한 만큼(translate로 이동)이 미리 당긴 스크롤에서 제외된다


구현 시도2

css의 keyframe+translate 속성을 사용해 이동 애니메이션과 동시에 스크롤을 조작하기에는 한계라는 생각이 들었다
그래서 setTimeout을 이용해 시간이 지날 때마다 왼쪽으로 스크롤을 연속적으로 이동시키는 방법을 사용해야겠다고 생각했다

useEffect + setInterval + state

useEffect와 setInterval을 사용해 n초마다 state를 증가시키고, state만큼 스크롤을 이동해주는 방법을 시도했으나, 스크롤 이동 시마다 렌더링이 되기 때문에 성능면에서 너무 안좋았다

useInterval

생각해보니 돔에 직접 접근해서 스크롤만 이동시켜주는 것이기 때문에 state에 의존하지 않아도 됐다. setInterval콜백함수에 직접적으로 CardList의 scrollWidth를 변경시켜주는 로직을 작성해도 정상적으로 작동했다.

그러나 후에 setInterval로 상태를 변경해야하는 상황이 있을 수 있으니 useInterval훅을 만들어서 사용했다.
setInterval에 전달할 콜백함수를 ref.current속성에 할당해, 상태 변경으로 인한 리렌더링 시 콜백함수를 매번 재할당하는 방법으로 최신 상태와 컴포넌트 환경을 보장해준다.
(함수형 setState를 이용한 방법은 state를 제외한 다른 데이터는 업데이트되지 않는 문제가 있었다.)
참고자료

Math.ceil

카드를 순환 시키기 위해선 스크롤의 중간 지점에 시작점으로 이동시키는 로직이 필요했는데
중간지점 인식이 안됐다.
원인을 알고봤더니 container.scrollLeft === container.scrollWidth/2의 조건이라고 했을 때, container.scrollWidth/2한 부분에서 소수점이 발생해 조건을 충족시키지 못했기 때문이다. Math.ceil(container.scrollWidth/2)을 해주어 해결했다


구현 시도3

문제점
가만히 둘 때에는 스크롤 중간 지점이 인식이 잘 되는데,
직접 스크롤을 할 때에는 인식이 안되어 순환되지 않았다.

해결책
순환 말고, 카드리스트 한덩이만 애니메이션 되면서 스크롤 끝에 도달하면 애니메이션 실행이 중단되도록 수정했다.
스크롤로 앞으로 당기면 다시 애니메이션이 시작되긴 하지만 카드가 많아졌을 경우 스크롤 끝에 도달했을 때 가장 앞으로 가야할 경우 스크롤을 많이 해야해서 사용성이 안 좋을 것 같았다.

사용성을 고려해서 카드 리스트의 가장 처음/ 끝으로 이동시켜주는 버튼 구현했다

  const handleClickFrontButton = () => {
    containerDom.scrollLeft = 0;
    setIsPlayMove(true); // 스크롤 시작점으로 돌아간 후 애니메이션을 다시 실행시켜준다
  };

  const handleClickBackButton = () => {
    containerDom.scrollLeft = containerDom.scrollWidth;
  };

버튼 관련 문제점
카드리스트 컨테이너에 마우스를 올렸을 시 버튼들이 나타나도록 구현하려고 했다
카드리스트 컨테이너 뿐만 아니라 버튼들 각각에 호버 했을 때에도 본인들이 나타나도록 구현해야했는데,
상대의 버튼까지 나타나도록 할 수가 없어서 실패했다.

hook으로 분리
애니메이션 관련 코드들을 useScrollAnimation훅으로 분리해서 WaffleCardList컴포넌트의 코드와 분리했다.

  • setIsPlayMove, moveScrollToFront, moveScrollToBack를 반환받아 특정한 이벤트 발생 시 애니메이션 재생/중지, 스크롤 가장 앞/뒤로 이동하는 동작을 실행시킬 수 있다
  • 파라미터로 deps배열을 받는데, deps의 요소들을 useEffect의 의존으로 걸어 deps요소의 변화에 따라 애니메이션이 (처음부터)재실행되도록 구현했다
profile
#프론트엔드

0개의 댓글