우아콘의 한 세션에서 앱 내 웹 뷰에서 리스트 컴포넌트를 리스트 가상화 (List Virtualization)를 통해 최적화 했다라는 이야기를 들었다.
그 발표를 계기로 리스트 가상화에 대해서 궁금해졌고, 이게 실제로 어떤 원리로 최적화가 이루어질 수 있는지 알아보자.
리스트 가상화는 동적 리스트를 랜더링할 때 전체 리스트를 랜더링하지 않고 화면에 보이는 콘텐츠만 랜더링하는 방식을 말한다.
TanStack Virtual 구현:
// packages/virtual-core/src/index.ts
private calculateRange() {
// 이진 탐색으로 startIndex 찾기
const startIndex = this.findStartIndex(scrollOffset);
// 뷰포트 끝까지 endIndex 확장
const endIndex = this.findEndIndex(startIndex, scrollOffset + outerSize);
// overscan 적용 (스크롤 시 깜빡임 방지)
return {
startIndex: Math.max(0, startIndex - overscan),
endIndex: Math.min(count - 1, endIndex + overscan)
};
}
TanStack Virtual 구현:
// 전체 리스트의 가상 높이 계산
getTotalSize() {
const measurements = this.getMeasurements();
return measurements[measurements.length - 1]?.end ?? 0;
}
TanStack Virtual 구현:
// 각 아이템의 크기 측정 및 캐싱
measureElement(element: HTMLElement, index: number) {
const size = element.getBoundingClientRect().height;
// 캐시에 저장
this.itemSizeCache.set(index, size);
// 예상 크기와 다르면 재계산 트리거
if (this.measurementsCache[index]?.size !== size) {
this.notify(false); // 3번부터 다시 계산
}
}
// 측정값 활용
getMeasurements() {
return this.options.count.map((_, index) => {
// 캐시에 있으면 사용, 없으면 추정값 사용
const size = this.itemSizeCache.get(index)
?? this.options.estimateSize(index);
return { start, size, end: start + size };
});
}
실제로 리스트 가상화가 어떻게 향상된 사용자 경험을 제공하는지 직접 프로젝트를 통해서 알아봤다.
리스트 가상화 체험해보기 : list-virtualization
Repo 주소 : https://github.com/SangWoo9734/list-virtualization.git
1. FPS (Frames Per Second) 측정
let frameCount = 0;
let lastTime = performance.now();
const measureFPS = () => {
frameCount++;
const currentTime = performance.now();
const elapsed = currentTime - lastTime;
if (elapsed >= 1000) { // 1초마다
const fps = Math.round((frameCount * 1000) / elapsed);
// fps 업데이트
}
requestAnimationFrame(measureFPS);
};
requestAnimationFrame이 호출될 때마다 프레임 카운트 증가2. DOM 노드 개수 측정
const domNodes = document.getElementsByTagName('*').length;
3. 메모리 사용량 측정
if ('memory' in performance) {
const memory = performance.memory;
const usedMB = memory.usedJSHeapSize / 1048576;
}
리스트 요소 100개

리스트 요소 1000개

리스트 요소 10000개

이제 여기서 리플로우 / 리페인트를 발생시켜보면 조금 더 체감이 될 것 같다.
10000개를 기준으로 2초마다 반복적으로 스타일을 변경하도록 해서 부하를 추가해서 확인해보았다.


가상화 전에는 스타일 변경이 시작되자마자 스크롤이 되지 않을 정도로 부하가 많이 걸리는 느낌이 확실히 들었다.
반면에 가상화를 적용했을 때는 부하를 주기 전과 거의 동일한 느낌으로 자연스러웠다.
이런 경험 뿐만 아니라 메모리 사용량도 차이가 많이 나는 것을 확인할 수 있었다.
다만 가상화를 적용한 리스트에서 약간 어색했던 부분은 리스트 전체 요소에 스타일이 적용되도록 해 두었는데 가상화 리스트의 경우 리스트 밖의 요소가 리스트에 보일때 랜더링을 하기 때문에 기존에 리스트에 있던 요소와 스타일이 변하는 타이밍이 달라지는 부분이 있었다.
DOM 노드가 많을수록:
가상화는 "DOM 노드 개수를 일정하게 유지"하여 이 문제를 해결
https://patterns-dev-kr.github.io/performance-patterns/list-virtualization/