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

yoon Y·2022년 3월 29일
0

[2nd_Project] WaffleCard

목록 보기
14/15

Intersection Observer도입

문제점
스크롤이 끝나면 useInterval을 중단 시키는 코드를 useInterval의 콜백함수 안에 작성해줬다.
문제점은 delay초마다 스크롤이 끝난지 확인하는 로직이 실행되어 비효율적이다.

// WaffleCardList.tsx
useInterval(
   () => {
     if (!(targetDom instanceof Element)) return;

     const { scrollLeft, clientWidth } = targetDom;
     const currentScrolledWidth = Math.ceil(scrollLeft + clientWidth);
     const isRenderedCards = currentScrolledWidth > targetDom.clientWidth;
     const isFinishScroll = currentScrolledWidth === targetDom.scrollWidth;
     isRenderedCards && isFinishScroll && setIsPlayMove(false); 
     targetDom.scrollLeft += 1;
   },
   15,
   isPlayMove,
 );

해결책
intersection observer를 도입하여, 마지막 카드를 감시 target으로 걸어준다.
마지막 카드가 전부 보여질 때 useInterval을 중단시키는 콜백함수를 실행시키는 방법이다.

하지만 마지막 카드에 ref를 걸어주려면 forwardRef를 사용해야하는데 코드가 복잡해질 것 같아서
카드리스트에 새로운 div태그를 추가해줬다.

tab이 변경될 때마다 카드리스트와 div가 다시 렌더링되기 때문에
callback ref를 이용해 div가 새로 생성 될 때마다 Element가 target에 자동으로 할당되도록 했다.
하지만 새로 생성되는건 가상돔이고, 변경된 부분이 없기 때문에 돔은 새로 생성되지 않았다.
그래서 content로 type을 주어 tab이 바뀔 때마다 변경 부분이 생기도록 했다.

// useScrollAnimation.ts
  const useScrollAnimation = (
  observeTargetDom: HTMLElement | null,
  deps?: ('total' | 'my' | 'like' | undefined)[],
): ReturnTypes => {
  const [isPlaying, setIsPlaying] = useState(true);
  // ↓ 마지막 요소가 교차 중일 때,(스크롤이 끝났을 때) MouseLeave이벤트로 인한 useInterval(애니메이션) 재실행을 막도록 상태를 추가해줬다.
  const [isIntersecting, setIsIntersecting] = useState(false);
  const [observeTarget, setObserveTarget] = useState<HTMLDivElement | null>(
    null,
  );

  const onIntersect: IntersectionObserverCallback = ([entry]) => {
    if (entry.isIntersecting) {
      setIsIntersecting(true);
      setIsPlaying(false);
    } else {
      setIsIntersecting(false);
    }
  };

  useEffect(() => {
    let observer: IntersectionObserver;
    if (observeTarget) {
      observer = new IntersectionObserver(onIntersect, {
        threshold: 0.1,
      });
      observer.observe(observeTarget);
    }
    return () => observer && observer.disconnect();
  }, [observeTarget]);

// WaffleCardList.tsx
...  
const [
    isPlaying,
    setIsPlaying,
    isIntersecting,
    setObserveTarget,
    moveScrollToFront,
    moveScrollToBack,
  ] = useScrollAnimation(cardsListRef.current, [type]);         


return (
          <>
           {waffleCards?.map(waffleCard => (
              <StyledWaffleCard
               type={type === 'my' ? 'my' : 'basic'}
               key={waffleCard.id}
               waffleCardData={waffleCard}
               onClickWaffleCard={handleClickWaffleCard}
               onClickLikeToggle={handleClickLikeToggle}
               onClickEdit={handleClickWaffleCardEdit}
               onClickDelete={handleClickWaffleCardDelete}
               />
              ))}
              <div ref={setTarget}>{type}</div> // 여기
           </>
          );

실제 원인 발견

하지만 새로 생성되는건 가상돔이고, 변경된 부분이 없기 때문에 돔은 새로 생성되지 않았다.
그래서 content로 type을 주어 tab이 바뀔 때마다 변경 부분이 생기도록 했다.

이것이 문제가 아니였다!
탭이 바뀌는 것과는 상관없이 마지막 요소는 돔이 새로 생성되지 않고 유지되고, 그렇다고 하더라도
교차될 때마다 인식되어 지정한 코드가 실행되어야 하는 게 맞다.

2가지 원인이 있었다
1. 빈 태그는 interSection observer사용 시 교차를 인지하지 못한다.
2. flex적용된 부모 요소의 자식인 flex-item은 가로 정렬 시 본인의 content너비만큼으로width가 설정된다.

  • 직접 width에 값을 설정해도 0으로 먹힌다.
  • 그래서 flex-item이 빈 태그일 시 content를 넣지 않는 이상 무조건 width가 0이 된다.

해결책

CardList의 마지막 태그를 빈태그로 두어서 observer target으로만 쓰려고 했었는데
원하는 대로 동작하지 않은 이유였다.
→ margin을 약간 적용해서 해결했다.

       return (
                <>
                  {waffleCards?.map(waffleCard => (
                    <StyledWaffleCard
                      type={type === 'my' ? 'my' : 'basic'}
                      key={waffleCard.id}
                      waffleCardData={waffleCard}
                      onClickWaffleCard={handleClickWaffleCard}
                      onClickLikeToggle={handleClickLikeToggle}
                      onClickEdit={handleClickWaffleCardEdit}
                      onClickDelete={handleClickWaffleCardDelete}
                    />
                  ))}
                  <LastItem ref={setObserveTarget}></LastItem>
                </>
              );

      const LastItem = styled.div`
       margin: 0 5px;
      `;
profile
#프론트엔드

0개의 댓글