개발 기술/사소하지만 놓치기 쉬운 개발 지식

브라우저 렌더링 최적화를 위한 Virtual Scroll - 원리/성능 비교

by GicoMomg 2025. 11. 16.

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의 위치와 내용만 교체하는 방식이다.

[스크롤시 각 요소의 Y 값이 변하면서 아이템이 교체되는 모습(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은 다음 작업을 수행한다:
    1. 첫 번째 div의 데이터 역할을 Item #1Item #2로 교체
    2. transform을 translateY(0px → 40px)로 업데이트
    3. 내부적으로 데이터 index도 함께 갱신

 

  • 즉, 같은 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을 실제 코드로 구현하는 방법을 단계별로 살펴보겠다.

반응형

댓글