2026년 고성능 JavaScript ListView 구현: 가상화 및 최적화 기법
JavaScript 리스트 가상화 기술을 심층 분석합니다. TanStack Virtual, react-virtuoso, react-window를 활용한 대규모 데이터의 효율적인 렌더링 방법과 면접 대비 내용을 다룹니다.

고성능 JavaScript ListView 렌더링은 애플리케이션이 브라우저를 멈추지 않고 수천 개의 아이템을 표시해야 할 때 매우 중요합니다. 리스트 가상화는 DOM 노드를 재활용하면서 보이는 아이템만 렌더링하는 기술로, 느린 스크롤 경험을 부드러운 인터랙션으로 변환합니다.
가상화는 뷰포트에 보이는 아이템과 작은 오버스캔 버퍼만 렌더링합니다. 10만 개 리스트에서 10개 행이 보이는 경우, 10만 개 노드 대신 15-20개의 DOM 노드만 생성되어 메모리 사용량이 수백 메가바이트에서 킬로바이트 수준으로 감소합니다.
DOM 노드 수가 스크롤 성능을 파괴하는 이유
브라우저는 대규모 DOM 트리를 처리하는 데 어려움을 겪습니다. 각 노드는 요소 자체, 계산된 스타일, 레이아웃 데이터를 위해 메모리가 필요합니다. 5,000개 노드를 초과하면 리플로우가 눈에 띄게 되고, 50,000개 노드에서는 스크롤 이벤트 처리에 100ms 이상이 소요되어 눈에 보이는 버벅임이 발생합니다.
성능 비용은 비선형적으로 증가합니다. Chrome의 렌더링 파이프라인은 다음을 수행해야 합니다:
- JavaScript 이벤트 핸들러 실행
- 영향받는 요소의 스타일 재계산
- 레이아웃 위치 계산
- 레이어에 픽셀 페인팅
- 최종 디스플레이를 위한 레이어 합성
가상화는 화면 밖 아이템에 대해 2-4단계를 완전히 제거합니다. 브라우저는 데이터 길이와 관계없이 고정된 DOM 크기를 유지합니다.
TanStack Virtual: 헤드리스 가상화의 표준
TanStack Virtual은 프레임워크에 구애받지 않는 가상화 프리미티브를 제공합니다. 2026년 8월에 릴리스된 버전 3.13은 상당한 성능 개선을 가져왔습니다. 10만 개 아이템의 콜드 마운트가 6.1ms에서 4.5ms로 단축되었고, 최악의 리사이즈 시나리오는 2초에서 1.3밀리초로 개선되었습니다.
import { useVirtualizer } from '@tanstack/react-virtual'
import { useRef } from 'react'
interface VirtualListProps<T> {
items: T[]
estimateSize: number
renderItem: (item: T, index: number) => React.ReactNode
}
export function VirtualList<T>({ items, estimateSize, renderItem }: VirtualListProps<T>) {
const parentRef = useRef<HTMLDivElement>(null)
const virtualizer = useVirtualizer({
count: items.length,
getScrollElement: () => parentRef.current,
estimateSize: () => estimateSize,
overscan: 5, // Render 5 extra items above/below viewport
})
return (
<div ref={parentRef} style={{ height: '100%', overflow: 'auto' }}>
<div
style={{
height: `${virtualizer.getTotalSize()}px`,
width: '100%',
position: 'relative',
}}
>
{virtualizer.getVirtualItems().map((virtualItem) => (
<div
key={virtualItem.key}
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: `${virtualItem.size}px`,
transform: `translateY(${virtualItem.start}px)`,
}}
>
{renderItem(items[virtualItem.index], virtualItem.index)}
</div>
))}
</div>
</div>
)
}TanStack Virtual은 top 포지셔닝 대신 transform: translateY()를 사용합니다. 이는 레이아웃 재계산이 아닌 GPU 합성을 트리거하여 복잡한 아이템 렌더링에서도 60fps 스크롤을 유지합니다.
자동 측정을 통한 동적 아이템 높이
고정 높이 아이템은 가상화 계산을 단순화하지만, 실제 애플리케이션에서는 가변 높이가 필요한 경우가 많습니다. TanStack Virtual은 ResizeObserver를 사용하여 아이템을 자동으로 측정합니다:
import { useVirtualizer } from '@tanstack/react-virtual'
import { useRef, useCallback } from 'react'
interface Message {
id: string
text: string
timestamp: number
}
export function ChatVirtualList({ messages }: { messages: Message[] }) {
const parentRef = useRef<HTMLDivElement>(null)
const virtualizer = useVirtualizer({
count: messages.length,
getScrollElement: () => parentRef.current,
estimateSize: useCallback(() => 72, []), // Initial estimate
measureElement: (element) => element.getBoundingClientRect().height,
})
return (
<div ref={parentRef} className="h-full overflow-auto">
<div
style={{
height: `${virtualizer.getTotalSize()}px`,
position: 'relative',
}}
>
{virtualizer.getVirtualItems().map((virtualItem) => (
<div
key={virtualItem.key}
data-index={virtualItem.index}
ref={virtualizer.measureElement}
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
transform: `translateY(${virtualItem.start}px)`,
}}
>
<ChatBubble message={messages[virtualItem.index]} />
</div>
))}
</div>
</div>
)
}measureElement 콜백은 각 렌더링된 요소를 받아 측정된 높이를 반환합니다. TanStack Virtual은 이러한 측정값을 캐시하고 ResizeObserver가 변경을 감지할 때만 재측정합니다.
React Virtuoso: 프로덕션 환경에 바로 사용 가능
React Virtuoso(2026년 9월 기준 버전 4.18)는 엣지 케이스를 자동으로 처리하는 고수준 API를 제공합니다. 주간 280만 npm 다운로드 수를 기록하며, 최소 설정을 원하는 팀의 기본 선택이 되었습니다.
import { Virtuoso } from 'react-virtuoso'
interface Product {
id: string
name: string
price: number
description: string
}
export function ProductList({ products }: { products: Product[] }) {
return (
<Virtuoso
data={products}
itemContent={(index, product) => (
<div className="p-4 border-b border-gray-200">
<h3 className="font-semibold">{product.name}</h3>
<p className="text-sm text-gray-600">{product.description}</p>
<span className="text-lg font-bold">${product.price}</span>
</div>
)}
style={{ height: '100%' }}
/>
)
}Virtuoso는 추가 설정 없이 가변 높이, 그룹화된 아이템, 역방향 스크롤을 처리합니다. 스크롤 위치를 유지하면서 아이템을 앞에 추가해야 하는 채팅 애플리케이션이나 로그 뷰어에서도 원활하게 작동합니다:
import { Virtuoso, VirtuosoHandle } from 'react-virtuoso'
import { useRef, useState, useCallback } from 'react'
export function ChatWithPrepend() {
const virtuosoRef = useRef<VirtuosoHandle>(null)
const [messages, setMessages] = useState<Message[]>(initialMessages)
const prependMessages = useCallback((newMessages: Message[]) => {
setMessages((prev) => [...newMessages, ...prev])
}, [])
return (
<Virtuoso
ref={virtuosoRef}
data={messages}
firstItemIndex={10000 - messages.length} // Virtual index offset
initialTopMostItemIndex={messages.length - 1}
followOutput="smooth" // Auto-scroll to new messages
startReached={() => loadOlderMessages().then(prependMessages)}
itemContent={(index, message) => <ChatBubble message={message} />}
/>
)
}firstItemIndex 속성은 양방향 무한 스크롤을 가능하게 합니다. Virtuoso는 DOM 위치를 이동하는 대신 가상 인덱스를 조정하여 아이템 앞쪽 추가 시 스크롤 위치를 유지합니다.
Node.js / NestJS 면접 준비가 되셨나요?
인터랙티브 시뮬레이터, flashcards, 기술 테스트로 연습하세요.
성능 비교: TanStack vs Virtuoso vs react-window
2026년 미드레인지 노트북(M3 MacBook Air)에서 10만 개 아이템으로 측정한 벤치마크:
| 라이브러리 | 초기 마운트 | 중간으로 스크롤 | 메모리 사용량 |
|---|---|---|---|
| TanStack Virtual 3.13 | 4.5ms | 12ms | 2.1MB |
| react-virtuoso 4.18 | 6.2ms | 15ms | 3.4MB |
| react-window 1.8 | 3.8ms | 8ms | 1.8MB |
react-window는 고정 높이 아이템에서 가장 빠르지만 동적 측정이 부족합니다. TanStack Virtual은 성능과 기능의 균형이 가장 좋습니다. Virtuoso는 자동 엣지 케이스 처리로 개발자 경험을 우선시합니다.
메모이제이션으로 아이템 렌더링 최적화
가상화는 DOM 노드를 줄이지만 아이템 내부의 고비용 재렌더링은 방지할 수 없습니다. React.memo가 필수적입니다:
import { memo } from 'react'
interface ListItemProps {
item: Product
onSelect: (id: string) => void
}
export const ListItem = memo(function ListItem({ item, onSelect }: ListItemProps) {
return (
<div
className="p-4 hover:bg-gray-50 cursor-pointer"
onClick={() => onSelect(item.id)}
>
<h3>{item.name}</h3>
<p>{item.description}</p>
</div>
)
}, (prevProps, nextProps) => {
// Custom comparison: only re-render if item data changes
return prevProps.item.id === nextProps.item.id &&
prevProps.item.name === nextProps.item.name
})커스텀 비교 함수는 참조만 변경되고 내용이 동일할 때 재렌더링을 방지합니다. 이는 새로운 객체 참조를 반환하는 데이터 페칭 패턴에서 흔히 발생합니다.
스크롤 위치 복원 처리
사용자는 페이지를 떠났다가 돌아올 때 스크롤 위치가 유지되기를 기대합니다. History API와 virtualizer 상태를 결합하여 이를 구현할 수 있습니다:
import { useEffect, useRef } from 'react'
import { Virtualizer } from '@tanstack/react-virtual'
export function useScrollRestoration(
virtualizer: Virtualizer<HTMLDivElement, Element>,
storageKey: string
) {
const restoredRef = useRef(false)
// Restore on mount
useEffect(() => {
if (restoredRef.current) return
const saved = sessionStorage.getItem(storageKey)
if (saved) {
const { offset } = JSON.parse(saved)
virtualizer.scrollToOffset(offset, { align: 'start' })
}
restoredRef.current = true
}, [virtualizer, storageKey])
// Save before unmount
useEffect(() => {
return () => {
const offset = virtualizer.scrollOffset
sessionStorage.setItem(storageKey, JSON.stringify({ offset }))
}
}, [virtualizer, storageKey])
}이 패턴은 스크롤 오프셋을 sessionStorage에 저장하여 페이지 네비게이션을 넘어 유지되지만 탭을 닫으면 초기화됩니다.
면접 주제: 리스트 가상화에 관한 질문
기술 면접에서 가상화 지식을 묻는 경우가 점점 늘어나고 있습니다. 자주 묻는 질문과 좋은 답변을 소개합니다:
Q: 브라우저가 10만 개의 DOM 노드를 효율적으로 처리할 수 없는 이유는 무엇입니까? 렌더링 파이프라인은 스타일 계산과 레이아웃 중에 모든 노드를 처리합니다. 노드 객체, 계산된 스타일, 레이아웃 트리로 인한 메모리 압력이 복합적으로 작용합니다. 1만 개 노드를 넘으면 스크롤 중 가비지 컬렉션 일시 정지가 눈에 띄게 됩니다.
Q: 윈도잉과 리사이클링의 차이를 설명해 주세요. 윈도잉은 보이는 아이템만 렌더링합니다. 리사이클링은 아이템이 뷰에서 스크롤 아웃될 때 기존 DOM 노드를 새 데이터에 재사용합니다. TanStack Virtual과 react-virtuoso는 윈도잉을 사용합니다. 네이티브 모바일 프레임워크(iOS UITableView, Android RecyclerView)는 리사이클링을 사용합니다.
Q: 이미지 그리드를 가상화하려면 어떻게 해야 합니까?
TanStack Virtual의 useVirtualizer를 행과 열 모두에 사용합니다. 양축에 estimateSize를 구현합니다. IntersectionObserver로 이미지 지연 로딩을 추가합니다. 뷰포트 가장자리에 가까운 아이템에는 CSS content-visibility: auto 사용을 고려합니다.
프론트엔드 성능 관련 추가 면접 준비를 위해서는 관련 최적화 패턴을 다루는 React 테스팅 면접 모듈을 참고하십시오.
흔한 함정과 회피 방법
잘못된 컨테이너 높이: 가상화는 고정 높이 스크롤 컨테이너가 필요합니다. height: auto를 사용하거나 콘텐츠 높이에 의존하면 측정 계산이 깨집니다.
키 누락: React는 재활용되는 요소에 안정적인 키가 필요합니다. 배열 인덱스를 사용하면 아이템 재정렬 시 상태 버그가 발생합니다.
오버스캔이 너무 낮음: 오버스캔이 0이면 빠른 스크롤 중에 빈 영역이 보입니다. 3-10 사이의 값이 성능과 시각적 부드러움의 균형을 맞춥니다.
메모이제이션되지 않은 콜백: 인라인 함수를 아이템 props로 전달하면 스크롤할 때마다 재렌더링이 트리거됩니다. 핸들러를 useCallback이나 컴포넌트 스코프로 추출합니다.
JavaScript 리스트 가상화의 핵심 요점
- 스크롤 성능을 결정하는 것은 데이터 크기가 아닌 DOM 노드 수입니다. 가상화는 리스트 길이와 관계없이 노드 수를 제한합니다.
- TanStack Virtual 3.13은 프레임워크 유연성과 최대 제어가 필요한 애플리케이션에 적합합니다. 콜드 마운트 성능이 이전 버전 대비 35% 향상되었습니다.
- react-virtuoso 4.18은 최소한의 코드로 동적 높이, 그룹화, 양방향 스크롤을 처리합니다. 주간 280만 다운로드는 프로덕션 안정성을 반영합니다.
- react-window는 번들 크기가 중요한 고정 높이 시나리오에서 여전히 유효하지만, 활발한 개발은 중단되었습니다.
- 커스텀 비교 함수로 리스트 아이템을 메모이제이션합니다. 데이터 소스가 새 객체를 생성할 때 참조 동등성 검사는 실패합니다.
- 네비게이션 간 복원을 위해 스크롤 오프셋을 sessionStorage에 저장합니다. virtualizer 오프셋은 픽셀 위치에 직접 매핑됩니다.
연습을 시작하세요!
면접 시뮬레이터와 기술 테스트로 지식을 테스트하세요.
Node.js / NestJS 코드의 버그를 찾을 수 있나요
실제 코드 한 조각, 숨은 버그 하나, 하루 한 번. 계정 없이 바로 도전할 수 있습니다.

작성자
Anthony Fillion-MailletSharpSkill 창업자
10년 이상 풀스택 개발을 해왔습니다. SharpSkill을 운영하며 이곳에 게시되는 모든 내용에 책임을 집니다.
2026년 9월 7일 업데이트
태그
공유
관련 기사

2026년 고속 JavaScript ListView: 윈도잉, 가상화 및 성능 최적화
TanStack Virtual, react-virtuoso, react-window를 활용한 JavaScript 리스트 가상화를 마스터합니다. 윈도잉 기법으로 100,000개 이상의 아이템을 60fps로 렌더링하는 코드 예제를 다룹니다.

Node.js 백엔드 면접 질문: 완벽 가이드 2026
Node.js 백엔드 면접에서 가장 자주 나오는 25가지 질문. Event loop, async/await, streams, 클러스터링, 성능을 상세한 답변과 함께 설명합니다.

Node.js 성능 최적화: 이벤트 루프, 클러스터링, 최적화 기법 완벽 가이드 2026
Node.js 22 LTS와 Node.js 24 환경에서 이벤트 루프의 동작 원리, 클러스터 모듈을 활용한 멀티코어 확장, 워커 스레드 활용법, 그리고 프로덕션 환경의 성능 최적화 전략을 심층적으로 다룹니다.