Skip to content

무한 Transition 애니메이션 최적화하기 2

juhyojeong edited this page Aug 20, 2024 · 3 revisions

📌 기존의 무한 Transition 동작 방식

기존의 무한 Transition 동작 방식은 다음과 같습니다.

transition.mov

서버로부터 받아온 100개의 Card 목록을 화면에서 Transition 시켜줍니다. 무한 Transition을 위해 카드 목록의 translateX를 왼쪽으로 이동시켜 주고 x가 totalWidth만큼 이동했으면 x를 다시 0으로 초기화해서 무한하게 Transition이 발생하도록 합니다.

이때 배열을 하나만 사용하면 totalWidth에 다다를 경우 배열이 끝나는 부분이 보이기 때문에, 배열 두 개를 이어붙여서 마치 무한하게 배열이 이어지는 것처럼 보이는 트릭을 사용했습니다.

배열 두 개를 이어붙이다보니, 100개의 데이터가 오면 200개의 카드를 렌더링 해줘야했습니다. 결국 초기 렌더링에 시간이 오래 걸렸고, 이 문제는 lazy loading을 사용해서 해결했습니다. >> wiki

❓ 문제점) DOM이 너무 커진다

현재 기획으로 정의된 카드의 개수는 총 100개로, 이어붙이는 배열까지 하면 200개의 List node가 DOM에 존재하게 됩니다. lazy loading으로 렌더링은 화면에 보이는 시점에 한다고 하더라도 결국 DOM에는 노드가 존재하는 것입니다.

image

위 사진을 보시면 Li node가 상당히 많이 존재하는 것을 확인할 수 있습니다.

만약 기획이 변경되어 데이터가 1000개, 10000개가 내려온다면 DOM은 그만큼 더 방대해지고 렌더링 시 성능도 안좋아질 수 있습니다.

❗️ 해결 방안) 화면에 보이는 만큼만 DOM에 렌더링하자 (Windowing)

DOM에 Li node들을 위치시키고 화면에 보일때 카드를 렌더링하는 이전 방식과는 다르게 아예 화면에 보이는 만큼의 Li node와 카드를 렌더링 시켜두는 Windowing 방식입니다.

    const visibleCardCount = useMemo(() => {
        const width = window.innerWidth;
        const cardWidth = CASPER_CARD_SIZE[CASPER_SIZE_OPTION.SM].CARD_WIDTH;

        return Math.ceil(width / cardWidth);
    }, []);

    const visibleCardList = useMemo(() => {
        return expandedCardList.slice(
            visibleCardListIdx,
            visibleCardListIdx + visibleCardCount * 2
        );
    }, [ ... ]);

    const handleUpdateAnimation = (latest: ResolvedValues) => {
        if (isEndCard(parseInt(String(latest.x)))) {
            let nextIdx = visibleCardListIdx + visibleCardCount;

            // 만약 nextIdx가 cardList의 길이를 초과하면 배열의 처음부터 다시 index를 카운트하도록 함
            if (nextIdx >= cardList.length) {
                nextIdx = nextIdx % cardList.length;
            }

            setVisibleCardListIdx(nextIdx);
            startAnimation(initialX);
        }
    };

    return (
        <AnimatePresence>
            <motion.ul
                onUpdate={handleUpdateAnimation}
            >
                {visibleCardList.map((card, idx) => (
                    <TransitionCasperCardItem
                        key={card.id}
                        cardItem={card}
                        id={card.id}
                    />
                ))}
            </motion.ul>
        </AnimatePresence>
    );

간단한 핵심 코드는 위와 같습니다. 화면 viewport와 카드 width를 기준으로 현재 화면에 꽉 채워서 보일 수 있는 카드 개수인 visibleCardCount를 계산합니다. visibleCardCount * 2개 만큼 카드를 렌더링해두고 UL node의 x를 transition 시키다가, totalWidth에 다다르면 이전 index + visibleCardCount로 list index를 바꿔서 카드를 갈아끼우고 x를 초기화 시켜서 다시 처음부터 x를 transition 시켜줍니다.

image

이제 DOM에 추가되는 카드 목록(visibleCardList)의 개수는 총 visibleCardCount * 2입니다. cardList.length * 2만큼 추가되었던 이전에 비해서 확연히 줄어든 개수입니다.

lazy loading은?

그렇다면 lazy loading과 Windowing 방식을 함께 사용할 필요가 있을까에 대해 고민을 해보았습니다. 일단 Windowing도 lazy loading을 사용한 의도처럼 화면에 보이는 카드 목록만큼 렌더링하는 것이므로 굳이 lazy loading까지 필요하지는 않았습니다. 또한 lazy loading을 하니 Windowing에서 카드 값을 갈아끼우는 과정에서 화면에 교차하는지 여부를 새로 계산하며 깜빡이는 현상이 발생했습니다.

default.mov

결국 Windowing 방식과 lazy loading 두 가지를 동시에 가져갈 수는 없겠다고 생각했습니다.

lazy loading만 가져가는 경우 화면에 보이는 카드만 렌더링하면서 효율적인 초기 로딩 시간을 가져갈 수는 있지만, 확장성을 고려해봤을때 DOM node가 방대해질 수 있는 위험이 있기 때문에 lazy loading 보다는 Windowing 방식을 선택하는 것이 맞다고 생각하게 되었습니다.

default.mov

따라서 앞서 설명한대로 Windowing을 적용하게 되었습니다.

📚 학습 정리

🗂️ 멘토링

Clone this wiki locally