# 2026년 고속 JavaScript ListView: 윈도잉, 가상화 및 성능 최적화 > TanStack Virtual, react-virtuoso, react-window를 활용한 JavaScript 리스트 가상화를 마스터합니다. 윈도잉 기법으로 100,000개 이상의 아이템을 60fps로 렌더링하는 코드 예제를 다룹니다. - Published: 2026-09-15 - Updated: 2026-09-15 - Author: Anthony Fillion-Maillet - Tags: javascript, performance, virtualization, react, typescript - Reading time: 9 min --- 가상화 없이 JavaScript ListView에서 수천 개의 아이템을 렌더링하면 브라우저가 수천 개의 DOM 노드를 생성합니다. 그 결과 초기 렌더링 지연, 스크롤 끊김, 메모리 소비 증가가 발생하며, 모바일 기기에서는 크래시가 발생할 수 있습니다. 2026년의 고속 JavaScript ListView 구현은 윈도잉을 통해 이 문제를 해결합니다. 뷰포트에 표시되는 10~20개의 아이템만 렌더링하면서 모든 아이템을 포함하는 스크롤 가능한 영역을 시뮬레이션하는 기법입니다. > **윈도잉의 핵심** > > 윈도잉은 보이는 아이템과 작은 버퍼(오버스캔)만 렌더링하여 리스트 크기에 관계없이 DOM 노드를 10,000개에서 약 20개로 줄입니다. ## 대규모 리스트에서 브라우저 성능이 저하되는 이유 모든 DOM 노드는 메모리를 소비하고 레이아웃 계산 시 브라우저가 위치를 추적해야 합니다. 적당히 복잡한 마크업(아바타, 두 개의 텍스트 span, 버튼)을 가진 10,000개의 리스트는 40,000개 이상의 DOM 노드를 생성합니다. Chrome의 렌더링 엔진은 스크롤 이벤트마다 모든 노드의 스타일과 위치를 재계산합니다. 그 결과 초기 렌더링에 2~4초가 걸리고, 스크롤은 15fps까지 떨어지며, 메모리 사용량은 500MB 이상으로 증가합니다. 모바일 Safari에서는 탭이 완전히 크래시되는 경우도 있습니다. 가상화는 일정한 DOM 노드 수를 유지함으로써 이 문제를 해결합니다. 데이터 배열에 100개가 있든 100,000개가 있든 렌더링되는 DOM은 약 20~30개 노드에 머무르며, 메모리는 일정하고 스크롤 성능은 60fps를 유지합니다. ```typescript // VirtualizedList.tsx import { useVirtualizer } from '@tanstack/react-virtual' import { useRef } from 'react' interface Item { id: string name: string email: string } export function VirtualizedList({ items }: { items: Item[] }) { const parentRef = useRef(null) // 예상 행 높이로 가상화 인스턴스 생성 const virtualizer = useVirtualizer({ count: items.length, getScrollElement: () => parentRef.current, estimateSize: () => 60, // 픽셀 단위 예상 높이 overscan: 5, // 뷰포트 위아래로 5개의 추가 아이템 렌더링 }) return (
{virtualizer.getVirtualItems().map((virtualRow) => { const item = items[virtualRow.index] return (

{item.name}

{item.email}

) })}
) } ``` 이 구현은 보이는 행과 오버스캔 버퍼만 렌더링합니다. 외부 컨테이너가 스크롤 가능한 총 높이를 설정하여 모든 아이템이 DOM에 존재하는 것처럼 스크롤바가 동작합니다. ## TanStack Virtual: 2026년의 헤드리스 접근 방식 [TanStack Virtual](https://tanstack.com/virtual/latest)은 프레임워크에 구애받지 않는 가상화 프리미티브를 제공합니다. react-window나 react-virtuoso와 달리 미리 빌드된 컴포넌트를 제공하지 않고, 어떤 아이템을 렌더링하고 어디에 배치할지 계산하는 훅만 제공합니다. 이 헤드리스 아키텍처는 스타일링과 DOM 구조에 대한 완전한 제어를 가능하게 합니다. 현재 버전(3.13)은 React, Vue, Solid, Svelte 및 바닐라 JavaScript를 지원합니다. 주요 구성 옵션은 다음과 같습니다. | 옵션 | 목적 | 기본값 | |--------|---------|--------| | `count` | 총 아이템 수 | 필수 | | `estimateSize` | 예상 아이템 높이/너비를 반환하는 함수 | 필수 | | `overscan` | 뷰포트 외부에 렌더링할 추가 아이템 수 | 1 | | `getScrollElement` | 스크롤 가능한 컨테이너 반환 | 필수 | | `measureElement` | 동적 크기를 위한 커스텀 측정 함수 | 자동 | `overscan` 값은 스크롤 부드러움에 직접적인 영향을 미칩니다. 너무 낮은 값(0~2)으로 설정하면 빠른 스크롤 시 흰색 깜박임이 발생합니다. 너무 높은 값(20 이상)으로 설정하면 가상화의 이점이 사라집니다. 대부분의 애플리케이션에서 5~10이 최적의 균형을 제공합니다. ```typescript // DynamicHeightList.tsx import { useVirtualizer } from '@tanstack/react-virtual' import { useRef, useCallback } from 'react' interface Message { id: string content: string timestamp: number } export function ChatList({ messages }: { messages: Message[] }) { const parentRef = useRef(null) // measureElement로 동적 높이 측정 활성화 const virtualizer = useVirtualizer({ count: messages.length, getScrollElement: () => parentRef.current, estimateSize: () => 80, // 대략적인 추정, 실제는 나중에 측정 overscan: 8, }) return (
{virtualizer.getVirtualItems().map((virtualRow) => { const message = messages[virtualRow.index] return (

{new Date(message.timestamp).toLocaleTimeString()}

{message.content}

) })}
) } ``` `measureElement` ref는 렌더링 후 자동 높이 측정을 활성화합니다. 이를 통해 수동으로 높이를 계산할 필요 없이 가변 콘텐츠 길이를 처리할 수 있습니다. ## react-virtuoso: 기능이 풍부한 대안 [react-virtuoso](https://virtuoso.dev/)는 정반대 접근 방식을 취합니다. 배터리 포함형 컴포넌트 라이브러리입니다. TanStack Virtual이 스크롤 컨테이너와 배치 로직 구축을 요구하는 반면, react-virtuoso는 이를 내부에서 처리합니다. 트레이드오프는 번들 크기(TanStack Virtual의 12KB에 비해 37KB)이지만, 그룹화된 리스트, 스티키 헤더, 역방향 스크롤이 있는 채팅 인터페이스 등 일반적인 패턴에 대한 보일러플레이트가 크게 줄어듭니다. ```typescript // VirtuosoList.tsx import { Virtuoso } from 'react-virtuoso' interface User { id: string name: string department: string } export function UserDirectory({ users }: { users: User[] }) { return ( (
{user.name.charAt(0)}

{user.name}

{user.department}

)} /> ) } ``` 더 적은 코드로 동일한 가상화 결과를 얻을 수 있습니다. react-virtuoso는 자동으로 아이템을 측정하고, 동적 높이를 처리하며, 스크롤 위치 유지를 제공합니다. [React 성능에 관한 면접 준비](/technologies/react-next/interview-questions/styling-css-in-js)에서 각 라이브러리를 언제 선택해야 하는지 이해하는 것은 아키텍처에 대한 인식을 보여줄 수 있는 기회입니다. TanStack Virtual은 이미 TanStack Table을 사용하거나 프레임워크에 구애받지 않는 코드가 필요한 애플리케이션에 적합합니다. react-virtuoso는 번들 크기보다 개발 속도를 우선시하는 팀에 적합합니다. ## 일반적인 함정과 해결책 ### 이미지로 인한 스크롤 위치 점프 이미지는 비동기로 로드되어 초기 측정 후 행 높이가 변경됩니다. 가상화기는 이 변화를 예측할 수 없어 아래 콘텐츠가 밀려납니다. 해결책은 aspect-ratio 또는 고정 치수를 사용하여 이미지 공간을 예약하는 것입니다. ```typescript // ImageListItem.tsx interface ImageItem { id: string src: string title: string } export function ImageListItem({ item }: { item: ImageItem }) { return (
{/* 로드 상태와 관계없이 80x80px 공간 예약 */}
{item.title}

{item.title}

) } ``` ### 스크롤 중 상태 손실 가상화된 아이템은 뷰에서 스크롤 아웃되면 언마운트됩니다. 로컬 상태(폼 입력, 펼쳐진 아코디언)가 사라집니다. 해결책은 아이템 ID로 키가 지정된 상태를 부모 컴포넌트나 상태 관리 라이브러리로 끌어올리는 것입니다. ```typescript // ExpandableList.tsx import { useState, useCallback } from 'react' import { useVirtualizer } from '@tanstack/react-virtual' interface FAQ { id: string question: string answer: string } export function FAQList({ faqs }: { faqs: FAQ[] }) { // 상태가 부모에 존재하여 행 언마운트 후에도 유지됨 const [expandedIds, setExpandedIds] = useState>(new Set()) const parentRef = useRef(null) const toggleExpanded = useCallback((id: string) => { setExpandedIds(prev => { const next = new Set(prev) if (next.has(id)) { next.delete(id) } else { next.add(id) } return next }) }, []) const virtualizer = useVirtualizer({ count: faqs.length, getScrollElement: () => parentRef.current, estimateSize: () => 60, overscan: 5, }) return (
{virtualizer.getVirtualItems().map((virtualRow) => { const faq = faqs[virtualRow.index] const isExpanded = expandedIds.has(faq.id) return (
) })}
) } ``` ### 불안정한 키로 인한 재렌더링 배열 인덱스를 키로 사용하면 리스트가 재정렬될 때까지는 작동합니다. 아이템이 잘못된 상태를 받아 시각적 결함과 불필요한 렌더링이 발생합니다. 항상 데이터 자체에서 안정적이고 고유한 식별자를 사용해야 합니다. ```typescript // 올바름: 데이터에서 안정적인 키 {virtualizer.getVirtualItems().map((virtualRow) => { const item = items[virtualRow.index] return
...
// 데이터베이스/API의 id })} // 잘못됨: 인덱스는 리스트 재정렬 시 변경됨 {virtualizer.getVirtualItems().map((virtualRow, index) => { return
...
// 재정렬 시 버그 발생 })} ``` ## 2026년 프로젝트를 위한 라이브러리 비교 적절한 가상화 라이브러리 선택은 프로젝트 요구사항에 따라 달라집니다. 이 비교는 2026년 생태계를 반영합니다. | 기준 | TanStack Virtual | react-virtuoso | react-window | |----------|-----------------|----------------|---------------| | 번들 크기 | 12KB | 37KB | 6KB | | 동적 높이 | 수동 설정 | 자동 | 미지원 | | TypeScript | 네이티브 | 네이티브 | @types 패키지 | | 프레임워크 지원 | React, Vue, Solid, Svelte | React만 | React만 | | 유지보수 | 활발함 (2026) | 활발함 (2026) | 유지보수 모드 | | 학습 난이도 | 높음 | 낮음 | 가장 낮음 | react-window는 번들 크기가 중요한 단순 고정 높이 리스트에는 여전히 유효합니다. 그러나 유지보수 모드 상태와 동적 높이 지원 부재로 인해 복잡한 요구사항을 가진 새 프로젝트에는 적합하지 않습니다. [고성능 React 애플리케이션 구축](/blog/react-next/react-testing-2026-vitest-rtl-best-practices)에서 가상화 트레이드오프를 이해하는 것은 면접관이 찾는 성능 인식을 보여줍니다. ## 성능 개선 측정 가상화의 이점을 정량화하려면 일관된 측정 방법론이 필요합니다. Chrome DevTools의 Performance 패널이 관련 메트릭을 제공합니다. 가상화 전후 주요 측정 항목은 다음과 같습니다. - **First Contentful Paint (FCP)**: 첫 번째 콘텐츠가 렌더링될 때까지의 시간. 가상화는 초기 DOM 생성을 제한하여 이를 단축합니다. - **Total Blocking Time (TBT)**: 로딩 중 메인 스레드 블로킹. DOM 노드가 적을수록 스타일/레이아웃 계산이 줄어듭니다. - **메모리 사용량**: Memory 패널에서 힙 크기 스냅샷. 리스트 길이와 관계없이 일정해야 합니다. - **스크롤 fps**: 스크롤하면서 Performance 트레이스 기록. 프레임 레이트는 60fps를 유지해야 합니다. ```typescript // PerformanceMonitor.tsx import { useEffect, useRef } from 'react' export function useScrollFPS(containerRef: React.RefObject) { const frameCountRef = useRef(0) const lastTimeRef = useRef(performance.now()) useEffect(() => { const container = containerRef.current if (!container) return let rafId: number const measureFrame = () => { frameCountRef.current++ const now = performance.now() if (now - lastTimeRef.current >= 1000) { console.log(`Scroll FPS: ${frameCountRef.current}`) frameCountRef.current = 0 lastTimeRef.current = now } rafId = requestAnimationFrame(measureFrame) } const handleScroll = () => { if (!rafId) { rafId = requestAnimationFrame(measureFrame) } } container.addEventListener('scroll', handleScroll, { passive: true }) return () => { container.removeEventListener('scroll', handleScroll) cancelAnimationFrame(rafId) } }, [containerRef]) } ``` 이 훅은 개발 중 스크롤 FPS를 로깅합니다. 프로덕션 애플리케이션에서는 Sentry Performance나 커스텀 텔레메트리 같은 모니터링 도구를 사용해야 합니다. ## JavaScript 리스트 성능의 핵심 포인트 - 가상화는 DOM 노드를 수천 개에서 약 20개로 줄여 데이터 크기와 관계없이 메모리와 스크롤 성능을 일정하게 유지합니다 - TanStack Virtual(12KB)은 React, Vue, Solid, Svelte에서 최대 제어를 위한 헤드리스 프리미티브를 제공합니다 - react-virtuoso(37KB)는 자동 동적 높이 처리가 포함된 배터리 포함형 컴포넌트를 제공합니다 - react-window는 2022년 이후 유지보수 모드이며 동적 높이 지원이 없습니다 - 오버스캔을 5~10으로 설정하면 가상화 이점을 해치지 않으면서 최적의 스크롤 부드러움을 얻습니다 - 이미지 크기를 예약하여 비동기 로딩 중 스크롤 위치 점프를 방지합니다 - 가상화된 행은 스크롤 중 언마운트되므로 확장 가능/편집 가능한 상태는 부모 컴포넌트로 끌어올립니다 - 재정렬 시 상태 불일치를 방지하기 위해 배열 인덱스가 아닌 데이터 ID의 안정적인 키를 사용합니다 - 구현 전후 FCP, TBT, 메모리, 스크롤 FPS를 측정하여 개선 사항을 정량화합니다 --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/ko/blog/node-nestjs/fast-javascript-listview-2026-windowing-virtualization-performance