-
Notifications
You must be signed in to change notification settings - Fork 1
무한 Transition 애니메이션 최적화하기 2
기존의 무한 Transition 동작 방식은 다음과 같습니다.
transition.mov
서버로부터 받아온 100개의 Card 목록을 화면에서 Transition 시켜줍니다. 무한 Transition을 위해 카드 목록의 translateX를 왼쪽으로 이동시켜 주고 x가 totalWidth만큼 이동했으면 x를 다시 0으로 초기화해서 무한하게 Transition이 발생하도록 합니다.
이때 배열을 하나만 사용하면 totalWidth에 다다를 경우 배열이 끝나는 부분이 보이기 때문에, 배열 두 개를 이어붙여서 마치 무한하게 배열이 이어지는 것처럼 보이는 트릭을 사용했습니다.
배열 두 개를 이어붙이다보니, 100개의 데이터가 오면 200개의 카드를 렌더링 해줘야했습니다. 결국 초기 렌더링에 시간이 오래 걸렸고, 이 문제는 lazy loading을 사용해서 해결했습니다. >> wiki
현재 기획으로 정의된 카드의 개수는 총 100개로, 이어붙이는 배열까지 하면 200개의 List node가 DOM에 존재하게 됩니다. lazy loading으로 렌더링은 화면에 보이는 시점에 한다고 하더라도 결국 DOM에는 노드가 존재하는 것입니다.
위 사진을 보시면 Li node가 상당히 많이 존재하는 것을 확인할 수 있습니다.
만약 기획이 변경되어 데이터가 1000개, 10000개가 내려온다면 DOM은 그만큼 더 방대해지고 렌더링 시 성능도 안좋아질 수 있습니다.
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 시켜줍니다.
이제 DOM에 추가되는 카드 목록(visibleCardList)의 개수는 총 visibleCardCount * 2
입니다. cardList.length * 2
만큼 추가되었던 이전에 비해서 확연히 줄어든 개수입니다.
그렇다면 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을 적용하게 되었습니다.
- 페이지가 렌더링 된 후 스크롤을 최상단으로 올리기
- DOM을 이미지로 저장하기
- Context API에서 불필요한 리렌더링을 줄이는 방법
- 무한 Transition 애니메이션
- Github Action 워크플로우 정의하기
- 무한 Transition 애니메이션 최적화하기
- 무한 Transition 애니메이션 최적화하기 2
- 무한 Transition 애니메이션 최적화하기 3
- fetch timeout 구현하기
- ErrorBoundary 구현하기
- 뒤로가기 confirm 로직 구현하기
- 선착순 밸런스 게임 상태 관리
- Modal 내부 컴포넌트에서 무한 스크롤이 제대로 동작 안되는 문제
- useToggleContents 훅 기본값 설정 및 조건부 사용법
- 폰트 굵기 적용 이슈
- SVG 내부 stroke 속성 값 제어를 위한 SVGIcon Util 함수 및 SVGR 사용 과정
- tailwindCSS의 @apply를 cva로 바꾸기
- 스크롤 내려갈 때 해당 섹션의 요소들 인터렉션 동작
- 공통 컴포넌트 내부에 애니메이션을 넣는 것에 대한 고민
- 특정 컴포넌트 위치에서 헤더 스타일 다르게 적용하는 방법
- 스크롤 거꾸로 올릴 때 IntersectionObserver가 뷰포트 감지 못하는 현상
- 선착순 밸런스 게임 최종 결과 계산에 대한 고민 (08.14)
- 프로그래스바 공통 컴포넌트로 분리
- (08.23 기준) 선착순 서버 시간 연동 문제
- 게임 종료된 상태에서 사용자 게임 참여 여부에 따른 FinalResult 분기 처리
- FinalResult 컴포넌트의 “당신의 선택” 카테고리 설정 이슈
- 게임 접속 시 게임 현재 진행 상태 초기화 및 카운트 다운 설정
- 선착순 밸런스 게임 UX 개선