-
리스트 가상화(Virtualization) - 성능 최적화를 위한 필수 기술 (많은 데이터 표현 시 최적화)웹 개발 2025. 7. 4. 23:46
🎯 리스트 가상화란?
많은 데이터를 렌더링할 때 성능을 최적화하는 기법으로, 실제로 보이는 항목만 렌더링하여 불필요한 리소스 사용을 줄이는 방식이다.
수천, 수만 개의 리스트를 한꺼번에 렌더링하면 성능 문제가 발생하는데, 가상화를 적용하면 이를 해결할 수 있다.
🛠 구현 방식
리스트 가상화를 구현하는 방법에는 여러 가지가 있지만, 여기서는 두 가지 방식을 소개한다.
- absolute 포지셔닝 방식
- 리스트 전체 높이를 유지하면서 보이는 아이템만 absolute 위치 조정하여 렌더링.
- 빠른 성능을 보장하지만, 일부 스타일이 변경될 경우 위치 계산이 틀어질 가능성이 있음.
- 패딩(Padding) 방식
- 위/아래에 div로 패딩을 추가하여 스크롤 위치를 조정.
- 스타일 변경에 강하고, absolute 없이 구현 가능하여 유지보수성이 좋음.
📌 absolute 포지셔닝 방식 구현 예제
일단, absolute 방식을 먼저 구현해보자
<!DOCTYPE html> <html lang="ko"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Virtualized List</title> <style> #container { height: 100vh; overflow-y: auto; position: relative; border: 1px solid #ddd; } #spacer { position: relative; width: 100%; } .item { position: absolute; height: 50px; width: 100%; border-bottom: 1px solid #ddd; display: flex; align-items: center; padding-left: 10px; background: white; } </style> </head> <body> <div id="container"> <div id="spacer"></div> </div> <script> const container = document.getElementById("container"); const spacer = document.getElementById("spacer"); const itemHeight = 50; const bufferCount = 5; const totalItems = 10000; const visibleCount = Math.ceil(window.innerHeight / itemHeight) + bufferCount * 2; spacer.style.height = `${totalItems * itemHeight}px`; let startIdx = 0; function renderItems() { const scrollTop = container.scrollTop; startIdx = Math.max(0, Math.floor(scrollTop / itemHeight) - bufferCount); const endIdx = Math.min(totalItems, startIdx + visibleCount); container.innerHTML = ""; container.appendChild(spacer); for (let i = startIdx; i < endIdx; i++) { const item = document.createElement("div"); item.className = "item"; item.textContent = `Item ${i + 1}`; itehttp://m.style.top = `${i * itemHeight}px`; container.appendChild(item); } } container.addEventListener("scroll", renderItems); renderItems(); </script> </body> </html>
📌 설명
spacer(div)를 전체 리스트 크기에 맞춰 설정해 스크롤바를 유지할 수 있다.
scroll 이벤트가 발생하면 innerHTML을 갱신해 필요한 요소만 DOM에 추가한다.
absolute로 아이템 위치를 조정해 원래 리스트처럼 보이게 한.이 absolute 코드를 React로 바꿔보면 다음과 같이 구현할 수 있다.
import { useEffect, useRef, useState } from 'react'; const itemHeight = 50; // 각 아이템 높이 const bufferCount = 5; // 추가적으로 렌더링할 여유 아이템 수 const VirtualizedList = ({ items }: { items: string[] }) => { const containerRef = useRef<HTMLDivElement>(null); const [startIdx, setStartIdx] = useState(0); const visibleCount = Math.ceil(window.innerHeight / itemHeight) + bufferCount * 2; const handleScroll = () => { if (!containerRef.current) return; const scrollTop = containerRef.current.scrollTop; const newStartIdx = Math.max(0, Math.floor(scrollTop / itemHeight) - bufferCount); setStartIdx(newStartIdx); }; useEffect(() => { const container = containerRef.current; if (container) { container.addEventListener('scroll', handleScroll); } return () => { if (container) container.removeEventListener('scroll', handleScroll); }; }, []); const endIdx = Math.min(items.length, startIdx + visibleCount); const visibleItems = items.slice(startIdx, endIdx); return ( <div ref={containerRef} style={{ height: '100vh', overflowY: 'auto', position: 'relative', }} > <div style={{ height: items.length * itemHeight, position: 'relative' }}> {visibleItems.map((item, index) => ( <div key={startIdx + index} style={{ position: 'absolute', top: (startIdx + index) * itemHeight, height: itemHeight, width: '100%', borderBottom: '1px solid #ddd', display: 'flex', alignItems: 'center', paddingLeft: '10px', }} > {item} </div> ))} </div> </div> ); }; export default function App() { const items = Array.from({ length: 10000 }, (_, i) => `Item ${i + 1}`); return <VirtualizedList items={items} />; }📌 설명
전체 리스트 높이(height: items.length * itemHeight)를 유지 한다.
스크롤 위치에 따라 현재 필요한 데이터만 slice로 잘라서 렌더링한다
absolute 위치를 이용해 아이템이 원래 위치에 있는 것처럼 보이게 처리해준다.📌 패딩(Padding) 방식 구현 예제
이제 padding 방식으로 다시 구현해보자.
<!DOCTYPE html> <html lang="ko"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Virtualized List</title> <style> #container { height: 100vh; overflow-y: auto; border: 1px solid #ddd; position: relative; } .item { height: 50px; border-bottom: 1px solid #ddd; display: flex; align-items: center; padding-left: 10px; background: white; } </style> </head> <body> <div id="container"> <div id="padding-top"></div> <div id="list"></div> <div id="padding-bottom"></div> </div> <script> const container = document.getElementById("container"); const list = document.getElementById("list"); const paddingTop = document.getElementById("padding-top"); const paddingBottom = document.getElementById("padding-bottom"); const itemHeight = 50; const bufferCount = 5; const totalItems = 10000; const visibleCount = Math.ceil(window.innerHeight / itemHeight) + bufferCount * 2; let startIdx = 0; function renderItems() { const scrollTop = container.scrollTop; startIdx = Math.max(0, Math.floor(scrollTop / itemHeight) - bufferCount); const endIdx = Math.min(totalItems, startIdx + visibleCount); paddingTop.style.height = `${startIdx * itemHeight}px`; paddingBottom.style.height = `${(totalItems - endIdx) * itemHeight}px`; list.innerHTML = ""; for (let i = startIdx; i < endIdx; i++) { const item = document.createElement("div"); item.className = "item"; item.textContent = `Item ${i + 1}`; list.appendChild(item); } } container.addEventListener("scroll", renderItems); renderItems(); </script> </body> </html>📌 설명
스크롤 위치에 따라 화면에 보여줄 아이템만 동적으로 생성하고, 나머지는 padding으로 공간만 확보할 수 있다.
scroll 이벤트가 발생할 때마다 renderItems() 함수가 호출되어 보여줄 범위를 갱신해준다.
이제, React로 구현해보자.
import { useEffect, useRef, useState } from 'react'; const itemHeight = 50; const bufferCount = 5; const VirtualizedList = ({ items }: { items: string[] }) => { const containerRef = useRef<HTMLDivElement>(null); const [startIdx, setStartIdx] = useState(0); const visibleCount = Math.ceil(window.innerHeight / itemHeight) + bufferCount * 2; const handleScroll = () => { if (!containerRef.current) return; const scrollTop = containerRef.current.scrollTop; const newStartIdx = Math.max(0, Math.floor(scrollTop / itemHeight) - bufferCount); setStartIdx(newStartIdx); }; useEffect(() => { const container = containerRef.current; if (container) { container.addEventListener('scroll', handleScroll); } return () => { if (container) container.removeEventListener('scroll', handleScroll); }; }, []); const endIdx = Math.min(items.length, startIdx + visibleCount); const visibleItems = items.slice(startIdx, endIdx); return ( <div ref={containerRef} style={{ height: '100vh', overflowY: 'auto', }} > <div style={{ height: startIdx * itemHeight }} /> {visibleItems.map((item, index) => ( <div key={startIdx + index} style={{ height: itemHeight, borderBottom: '1px solid #ddd', display: 'flex', alignItems: 'center', paddingLeft: '10px', }} > {item} </div> ))} <div style={{ height: (items.length - endIdx) * itemHeight }} /> </div> ); }; export default function App() { const items = Array.from({ length: 10000 }, (_, i) => `Item ${i + 1}`); return <VirtualizedList items={items} />; }📌 설명상단과 하단에 빈 div로 공간을 확보해 전체 리스트처럼 보이게 하며, 실제 DOM에 그려지는 항목 수를 최소화한다.
scroll 이벤트를 감지해 startIdx를 갱신하고, 그에 따라 items.slice()로 보여줄 항목 범위를 조절한다.
🚀 요약하자면....
리스트 가상화를 적용하면 다음과 같은 장점이 있다.
✅ 렌더링 최적화 - 보이는 아이템만 렌더링하여 DOM 부하를 줄임
✅ 빠른 스크롤 성능 - 수천 개의 아이템을 다뤄도 성능 저하가 없음
✅ 유지보수 용이 - absolute 방식과 padding 방식 중 선택하여 적용 가능
패딩 방식은 스타일 변경에도 강하고, absolute 방식보다 자연스럽게 동작하기 때문에 유연한 UI를 만들 때 유리하다.
많은 데이터를 리스트로 표현할 때 필연적으로 최적화를 고민할 수 있는데, 라이브러리를 사용할 수도 있지만 때로는 직접 코드를 작성해 사용하는 것도 좋은 방법이 될 수 있다 🌽
+ 라이브러리로는 TanStack Virtual, react-window, react-virtualized 등을 많이 사용한다 🥙
🤗 실제 작성한 코드
https://github.com/citron03/practice-next-15/commit/1a955ec260c475523a5acf3cfc19fd507a755249
feat(list): make virtual list two version (padding, absolute) · citron03/practice-next-15@1a955ec
+ const items = Array.from({ length: 10000 }, (_, i) => `Item ${i + 1}`);
github.com
'웹 개발' 카테고리의 다른 글
signal에 대해서 알아보고, state와 비교 (React에서는 signal이 부적합한 이유) ⚾️ (3) 2025.08.07 [FE 최적화 기술] Throttle, Debounce, useDeferredValue, useTransition 쉽게 정리하기 😊 (0) 2025.07.21 Next.js 15 App Router에서 PWA 설정하기 🍍 (0) 2025.04.11 vite 플러그인 만들기 (with 간단한 예제 & vite-plugin-pages 분석) 😊 (0) 2025.03.18 stylelint 🎨 스타일에도 린트 적용하기 (0) 2025.03.01 - absolute 포지셔닝 방식