0. 들어가며…
- 대부분의 서비스는 수 많은 데이터를 스크롤 기반으로 보여준다.
- 거래 내역, 로그, 쇼핑 내역처럼 연속된 데이터를 무한 스크롤로 제공하는 방식은 이미 모바일·웹 모두에서 기본 UI 패턴이 되었다.
- 그런데 특정 상황에서 이 스크롤 방식이 문제가 될 수 있다.
"만약 한 번에 보여줘야 하는 아이템이 100개 이상이라면 어떨까?"
- 단순히 100개의 DOM을 렌더링해 스크롤하는 정도는 괜찮아 보이지만, 실제 서비스 환경에서는 상황이 다르다.
- 예를 들어, 100개짜리 리스트를 무한 스크롤로 노출한 상태에서 소켓 데이터가 실시간으로 들어오고, 동시에 canvas 기반 그래프까지 그려야 한다면?
- 스크롤이 순간적으로 뚝뚝 끊기는 ‘렉’이 발생한다.
- 이 문제의 핵심 원인은 화면에 쌓여 있는 DOM 수와 그로 인해 반복되는 레이아웃(Layout) 계산이다.
- 브라우저의 Reflow/Repaint 비용은 결코 가볍지 않으며, DOM이 많아질수록 이 비용은 기하급수적으로 증가한다.
"그렇다면 어떻게해야 이 문제를 예방할 수 있을까?"
- 확실한 방법은 “화면에 존재하는 DOM 개수를 제한하는 것”이다.
- 예를 들어, 리스트가 1,000개여도 실제 DOM을 약 30개만 유지하고,
- 스크롤 시 DOM은 그대로 둔 채 “데이터만 교체해 재활용한다면” 과도한 레이아웃 비용을 크게 줄일 수 있다.
- 이는 Android의
RecyclerView(구 ListView)가 사용하는 방식과 동일하다. - JavaScript에서도 이 개념을 Virtual Scroll(가상 스크롤)이라고 부른다.
이번 글에서는 Virtual Scroll이 어떤 원리로 동작하는지, 그리고 실제로 성능적 이점이 얼마나 있는지를 실측 데이터를 통해 분석한다.
해당 글은 시리즈로 구성되어 있으며, 원리와 성능 비교는 본 글에서, 구현 코드는 다음 글에서 다룬다.
브라우저 렌더링 최적화를 위한 Virtual Scroll - 원리/성능 비교 ← 이번 글
브라우저 렌더링 최적화를 위한 Virtual Scroll - 구현 코드 살펴보기
1. Virtual Scroll의 원리를 살펴보자
1) Virtual Scroll의 개념
(1) 정의
- Virtual Scroll(가상 스크롤)은 대량의 리스트를 “실제 DOM 전체를 만들지 않고” 스크롤 가능한 UI로 보여주는 기술이다.
- 데이터가 1,000개, 10,000개로 많아질수록 DOM 개수도 증가하기에 브라우저는 금방 느려진다.
- 하지만 Virtual Scroll을 사용하면 이 문제를 해결할 수 있다!
- Virtual Scroll은 전체 데이터 개수만큼 DOM을 생성하지 않는다.
- 대신 화면에 보이는 영역에 필요한 DOM만(보통 20~40개 정도) 유지하고, 스크롤에 따라 DOM의 위치와 내용만 교체하는 방식이다.

- 즉 사용자에게는 “수천 개가 한 번에 렌더링된 긴 리스트”처럼 보이지만, 실제로는 극히 소수의 DOM만 계속 재활용된다!
2) Virtual Scroll의 핵심 아이디어
(1) DOM을 최소한으로 유지하고(visible only) 재사용한다(pooling)
- Virtual Scroll의 핵심은 “데이터 개수만큼 DOM을 만들지 않는다”는 점이다.
- 데이터가 10,000개라도 실제 화면에 존재하는 DOM은 약 20~40개에 불과하다.
- 아래 그림처럼, Full Render 방식에서는 데이터 개수(10,000개)만큼 DOM이 그대로 생성된다.

- 반면, Virtual Scroll은 화면에 실제로 보이는 영역을 기준으로 일정 개수만 생성하고, 스크롤 시 이 DOM을 그대로 재활용한다.

- 보통 다음과 같은 구조로 동작한다.
- 화면에 보이는 아이템 수 + 버퍼(예: 위아래 10개)만 DOM 생성
- 스크롤이 내려가도 DOM은 삭제·추가되지 않음
- 변경되는 것은 “각 div가 어떤 데이터를 표시할지”뿐
- 즉, div의 역할만 바뀌고, DOM 자체는 계속 재활용됨
- 이러한 구조 덕분에 불필요한 레이아웃(Layout)·리플로우(Reflow)가 크게 줄어든다.
(2) translateY로 실제 리스트 위치를 ‘흉내내기’
- DOM을 재사용하면 자연스럽게 한 가지 의문이 생긴다.
같은 div인데 어떻게 데이터 0번, 100번, 10,000번 위치에 있는 것처럼 보일 수 있는지
- 그 이유는 Virtual Scroll은 position:absolute + transform: translateY() 조합을 사용하기 때문이다.
- 각 DOM을 (레이아웃 변경 없이)
transform으로 이동시켜, 해당 위치에 있는 것처럼 보이게 한다. - 아래 예시는 첫 번째 div가 translateY를 변경하며 ‘역할’이 바뀌는 과정을 단계별로 보여준다.
A. 초기 상태 — 첫 번째 div는 데이터[0]을 그린다
- 첫 번째 div는
Item #1을 표시하며translateY(0px)위치에 배치된다. - 이 시점에서는 화면에 보이는 약 20~40개의 DOM이 각자 초기 데이터 역할을 맡고 있다.

B. 스크롤이 조금 내려간 상태 (예: 20px 이동)
- 스크롤 값은 변했지만 DOM 구조는 그대로다.
- 첫 번째 div가 아래로 이동한 것처럼 보이지만, 이는 transform 때문이 아니라 viewport 자체가 이동한 것이다.
- DOM은 아직도 기존 데이터 역할을 유지한다.

C. 스크롤이 itemHeight만큼 이동됐을 때 (예: 40px 도달)
itemHeight경계에 도달하면 Virtual Scroll은 다음 작업을 수행한다:- 첫 번째 div의 데이터 역할을
Item #1→Item #2로 교체 - transform을
translateY(0px → 40px)로 업데이트 - 내부적으로 데이터 index도 함께 갱신
- 첫 번째 div의 데이터 역할을

- 즉, 같은 div지만 translateY 위치와 데이터 index를 바꿔줌으로써 전혀 다른 “리스트 요소”처럼 보이게 된다.
- 이 과정에서 레이아웃(Layout)이나 Reflow는 발생하지 않으며, 모든 이동은 GPU가 처리하는
transform기반 애니메이션이다.
(3) spacer로 전체 높이를 유지해 “진짜 스크롤”처럼 보이기
- Virtual Scroll에서는 실제로 존재하는 DOM이 20~40개뿐이지만,
- 사용자는 “10,000개의 아이템이 전부 렌더링된 긴 리스트”를 스크롤하고 있다고 느껴야 한다.
- 이 착시를 만들어주는 핵심 요소가 바로 spacer(전체 높이를 가진 투명한 div)이다.
- Virtual Scroll의 HTML 구조는 크게 아래 세 가지로 구성된다.
- Container: 실제 스크롤이 일어나는 영역
- Spacer:
dataLength × itemHeight높이를 가진 거대한 빈 div - Item DOM들: Spacer 위에 absolute로 떠 있는, 재사용되는 약 20~40개의 DOM 노드

- 이 중에서 Spacer는 아래 두 가지 역할을 담당한다.
A. 전체 데이터 높이를 ‘가짜로’ 만든다
- 예를 들어, 데이터가 10,000개이고 각 아이템의 높이가 40px이라면:
전체 높이 = itemHeight(40px) × dataLength(10,000) = 400,000px
- Spacer는 이 400,000px 높이를 그대로 가진 투명한 div일 뿐이지만, 브라우저는 이 값을 기준으로 스크롤바를 생성한다.
- 그 결과, 실제 DOM은 30개만 존재해도 사용자는 “엄청 긴 리스트를 스크롤하는 중”이라고 자연스럽게 느끼게 된다.
B. 재사용되는 item DOM의 ‘위치 기준점’ 역할을 한다
- Virtual Scroll의 item DOM들은 모두
position:absolute로 배치된다. - 즉, 문서 흐름에 속하지 않고 원하는 위치에 자유롭게 올릴 수 있다.
- Spacer는 이 item DOM들이 떠 있을 공간을 제공한다.
- 재사용되는 각 DOM은 Spacer 위에서
translateY(0px → 40px → 80px → …)방식으로 이동하며, 마치 해당 index의 위치에 존재하는 것처럼 보이게 된다.
- 지금까지 Virtual Scroll이 어떤 구조로 동작하는지를 살펴보았다.
- 핵심은 “DOM을 최소한으로 유지하고, 기존 DOM을 재활용하며, transform으로 위치만 바꾼다”는 점이다.
- 이론적으로만 보면 매우 효율적인 구조지만, 실제 브라우저 렌더링 파이프라인에서도 동일한 성능 개선이 보장될까?
- 특히 Layout → Paint → Composite 단계에서 Virtual Scroll이 Full Render 대비 얼마나 비용을 줄여주는지가 중요한 포인트다.
- 그래서 이어지는 [2. Virtual Scroll의 성능 측정해보기] 에서 동일한 조건에서 Full Render와 Virtual Scroll을 비교하며, 성능 차이가 발생하는지 확인해보겠다.
2. Virtual Scroll의 성능 측정해보기
1) 테스트 환경은?
- Virtual Scroll의 강점은 DOM 수를 최소화해 Layout 비용과 GPU 메모리를 줄이는 구조적 최적화에 있다.
- 즉, “DOM을 적게 그리니까 성능이 빨라진다”는 것이 이론적 설명이다.
하지만 실제 브라우저 환경에서도 정말 그렇게 동작할까?
- Virtual Scroll이 Layout → Paint → Composite 파이프라인에서 실제 성능상 효과가 있는지 확인하기 위해 Chrome Performance 패널로 렌더링 지표를 직접 수집해 비교했다
- 하드웨어 성능이나 백그라운드 앱의 영향을 최소화하기 위해 아래 조건을 고정하여 실험을 진행했다.
- 성능 테스트에 사용한 코드는 이 링크에서 볼 수 있다.
렌더링 아이템 수: 10,000개
CPU Throttling: 4x (저사양 기기 상황 재현)
네트워크 제한: 없음 (순수 렌더링 비용만 측정)
브라우저: Chrome 시크릿 모드 / 142.0.7444.162 (arm64)
테스트 기기: iOS 32G 환경
2) 측정값 살펴보기
- 동일한 테스트 시나리오(페이지 로드 → 스크롤)에서 관찰된 주요 성능 지표는 아래와 같다.
| 지표 | Full Render | Virtual Scroll | 개선율 |
|---|---|---|---|
| UpdateLayoutTree | 52.3 ms | 6.3 ms | ▲ 88% 빠름 |
| 평균 Paint | 3.8 ms | 1.9 ms | ▲ 50% 빠름 |
| GPU Memory | 40.9 MB | 6.5 MB | ▲ 84% 절감 |
| Frame Count | 28 | 33 | +18% 증가 |
| FPS | ~50 fps | ~60 fps | +20% 향상 |
| LayoutCount | 3 | 16 | (수는↑, 비용은↓) |
- 요약하면, Virtual Scroll은 초기 렌더링에서 약 8배 빠르고, 스크롤 중에도 안정적으로 60fps를 유지한다.
(1) Layout / Paint Time
- 아래 그래프는 UpdateLayoutTree(레이아웃)와 Paint 비용이 얼마나 줄어드는지 보여준다.

① Layout(UpdateLayoutTree) – 88% 감소
- Full Render: 전체 10,000 DOM의 크기·위치 계산 → 52ms
- Virtual Scroll: 보이는 30~40개만 계산 → 6ms
16.7ms(1 frame 예산)을 초과하면 반드시 jank가 발생한다.
Full Render는 이를 넘기지만, Virtual Scroll은 여유로운 수치를 보인다.
② Paint – 50% 감소
- Virtual Scroll은 전체 DOM을 다시 그리지 않음
- 화면에 보이는 “20~40개의 DOM만” 페인트하면 되기 때문
특히 모바일(WebView 포함) 환경에서는 Paint 비용이 배터리·발열과 직결되기 때문에 이 차이가 더 크게 느껴진다.
(2) GPU Memory / FPS
- 다음 그래프는 GPU 메모리 사용량과 FPS를 비교한 것이다.

③ GPU 메모리 – 84% 감소
- Full Render: 10,000 DOM 전체가 레이어로 올라가며 약 40MB
- Virtual Scroll: 재사용되는 pool DOM만 레이어 → 6.5MB
④ FPS – 스크롤 체감 품질 차이를 만드는 지표
- Full Render: 약 50fps → 약간의 튐(jank) 존재
- Virtual Scroll: 항상 55~60fps 근처 유지
스크롤 중 Virtual Scroll의 LayoutCount가 더 많음에도 FPS가 높은 이유는 간단하다.
Virtual Scroll은 “횟수는 많아도 매우 작은 범위”만 Layout 하고,
Full Render는 “횟수는 적어도 매우 큰 범위”를 Layout 하기 때문이다.
(3) Frame & Layout Count

- Virtual Scroll은 더 많은 프레임을 생성해 스크롤 반응성이 높다.
- Layout 횟수는 Full Render보다 많지만, 각 Layout의 범위가 작아 오히려 전체 비용은 낮다.
(4) 결론
실험 결과를 요약하면 다음과 같다.
- LayoutTree 비용: 8배 감소
- Paint 비용: 50% 감소
- GPU Memory: 1/6
- FPS: 60 근처 유지
- 이는 Virtual Scroll이 단순히 “DOM을 덜 그린다”가 아니라,
- 브라우저 렌더링 파이프라인 전체를 최적화하는 구조라는 걸 의미한다.
3. 마치며…
Virtual Scroll은 레이아웃(Layout) → 페인트(Paint) → 컴포지트(Composite)로 이어지는 브라우저 렌더링 파이프라인을 최적화하는 기법이다.
이번 글에서는 Virtual Scroll이 어떤 원리로 동작하는지, 그리고 실제 브라우저 환경에서 얼마나 큰 성능 차이를 만드는지를 실측 데이터를 기반으로 살펴보았다.
정리하면 Virtual Scroll의 핵심은 다음과 같다.
- DOM 수 최소화: 보이는 영역 + 버퍼만 렌더링
- DOM 재활용(pooling): 새로 만들지 않고 역할만 교체
- transform 기반 위치 이동: Layout/Reflow 없이 고속 이동
- spacer로 전체 높이 시뮬레이션: “긴 리스트를 스크롤하는 듯한” 착시 유지
- 렌더링 파이프라인 최적화: Layout 8배 ↓, Paint 50% ↓, GPU Memory 1/6 ↓
이 구조 덕분에 Virtual Scroll은 실서비스에서 즉시 체감되는 성능 개선을 제공한다.
특히 작은 화면·제한된 리소스 환경(모바일 WebView, Hybrid App)에서는 그 효과가 더 크게 나타난다.
다음 시간에는 이번 원리를 기반으로 Virtual Scroll을 실제 코드로 구현하는 방법을 단계별로 살펴보겠다.
'개발 기술 > 사소하지만 놓치기 쉬운 개발 지식' 카테고리의 다른 글
| sourcemap? 운영에서는 그냥 끄는 옵션 아니에요? (0) | 2025.12.13 |
|---|---|
| 브라우저 렌더링 최적화를 위한 Virtual Scroll - 구현 코드 살펴보기 (1) | 2025.11.16 |
| [Vue] Fragment의 함정: 왜 $el은 Text Node가 될까? (0) | 2025.10.26 |
| [JS] 모바일 웹뷰에서 가상 키보드 감지하는 법: visualViewport·디바운스·rAF (8) | 2025.08.17 |
| [JS] Array.map() vs Iterator Helper API: 어떤 방식이 더 빠를까? (2) | 2025.07.21 |
댓글