🎯 리스트 가상화란?
많은 데이터를 렌더링할 때 성능을 최적화하는 기법으로, 실제로 보이는 항목만 렌더링하여 불필요한 리소스 사용을 줄이는 방식이다.
수천, 수만 개의 리스트를 한꺼번에 렌더링하면 성능 문제가 발생하는데, 가상화를 적용하면 이를 해결할 수 있다.
🛠 구현 방식
리스트 가상화를 구현하는 방법에는 여러 가지가 있지만, 여기서는 두 가지 방식을 소개한다.
- 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
'웹 개발' 카테고리의 다른 글
Next.js 15 App Router에서 PWA 설정하기 🍍 (0) | 2025.04.11 |
---|---|
vite 플러그인 만들기 (with 간단한 예제 & vite-plugin-pages 분석) 😊 (0) | 2025.03.18 |
stylelint 🎨 스타일에도 린트 적용하기 (0) | 2025.03.01 |
Next에서 더 효과적인 modal 개발하기 (Parallel Routes & Intercepting Routes) 😎 (0) | 2025.02.21 |
pnpm patch를 사용해서 노드 모듈 수정하기 (patch-package 에러 발생 😭) (0) | 2025.02.05 |