본문 바로가기

IT & AI

100만 행 데이터 그리드가 버티는 법, 양축 가상화부터 DOM 풀링까지

728x90

100만 행 데이터 그리드가 버티는 법, 양축 가상화부터 DOM 풀링까지

IT & AI 뉴스 썸네일
IT & AI 뉴스 썸네일

대용량 데이터 그리드는 행 몇 개를 덜 그리는 것만으로 빨라지지 않아요. 열 너비 계산, 검색 상태, 편집기, 아이콘, 스크롤 이벤트가 한 프레임 안에서 함께 움직여야 해요. VisuaLeaf 개발자가 MongoDB와 PostgreSQL용 그리드를 약 1년 동안 다듬으며 적용한 최적화 과정을 공개했어요. 1

핵심 요약

구분핵심왜 볼 만한가요
렌더링 범위화면에 보이는 행과 열만 그리는 양축 가상화데이터가 늘어도 DOM 크기를 일정하게 유지할 수 있어요
상태 구조원본 데이터와 별도로 표시용 섀도 테이블 구성스크롤 중 타입 판별과 중첩 경로 탐색을 반복하지 않아요
스크롤 처리수동적 리스너, 프레임 병합, 버퍼와 히스테리시스 적용이벤트가 몰려도 렌더링 범위를 매번 다시 만들지 않아요
DOM 비용아이콘 공유, 편집기 지연 마운트, 위치 기반 노드 재사용셀 생성과 파괴에 쓰는 시간을 줄일 수 있어요
구현 선택Canvas 대신 DOM 기반 구조 유지텍스트 선택과 접근성, 기능 확장을 함께 챙길 수 있어요

1. 행만 가상화하면 넓은 데이터에서 다시 막혀요

처음에는 모든 셀을 렌더링하는 단순한 표로 시작했어요. 10,000행에 30열만 곱해도 셀은 300,000개가 돼요. 브라우저는 이 노드들의 스타일과 레이아웃, 페인트를 처리해야 하고 프레임워크도 변경 여부를 계속 확인해요. 60fps 화면에서 한 프레임에 쓸 수 있는 시간은 16.7ms라서 금방 한계가 와요. 2

수직 가상화는 뷰포트 주변의 약 40개 행만 남겨 이 문제를 줄여요. 전체 높이만 가진 빈 영역을 만들면 브라우저의 기본 스크롤바도 유지할 수 있어요. 하지만 열이 300개라면 40개 행만 그려도 셀이 12,000개예요. 그래서 이 구현은 행과 열을 모두 가상화했어요.

728x90

열마다 너비가 달라서 가로축은 단순 나눗셈으로 계산하기 어려워요. 각 열의 시작 위치를 누적 합 배열에 저장하고, 현재 스크롤 위치에 놓인 열은 이진 탐색으로 찾았어요. 열 너비나 순서가 바뀔 때만 누적 합을 다시 만들어요. 그 결과 500개 열이 있어도 화면에는 약 12개 열만 남아요.

원본 데이터와 화면 상태를 분리했어요

MongoDB의 BSON과 PostgreSQL의 JSONB는 값의 타입과 중첩 구조가 다양해요. 같은 `123`처럼 보여도 문자열과 정수는 검색과 편집 방식이 달라요. 렌더링할 때마다 타입을 판별하고 중첩 경로를 순회하면 스크롤 경로에 불필요한 계산이 들어가요.

이를 피하려고 표시 문자열, 확정된 타입, 평탄화한 경로, 열 순서, 검색 결과를 미리 계산한 섀도 테이블을 뒀어요. 원본 문서는 데이터의 기준으로 남기고, 섀도 테이블은 화면 상태의 기준으로 썼어요. 셀을 그리는 순간에는 이미 정리된 값을 읽기만 하니 스크롤 중 작업이 단순해져요.

스크롤 이벤트보다 프레임 예산을 먼저 봤어요

스크롤 이벤트는 짧은 시간에 여러 번 발생해요. 구현자는 수동적 이벤트 리스너로 브라우저가 JavaScript를 기다리지 않게 했고, 여러 이벤트를 `requestAnimationFrame` 한 번으로 합쳤어요. 새 뷰포트가 기존 버퍼 안에 있으면 렌더링을 건너뛰고, 가장자리에 가까워졌을 때만 버퍼를 새 위치로 옮겼어요.

빠르게 튕기는 스크롤에서는 속도도 추적했어요. 일정 속도를 넘으면 셀을 급히 채우지 않고 기본 스크롤 이동을 먼저 허용해요. 속도가 안정된 뒤 화면을 다시 채우는 방식이에요. 모든 이벤트에 즉시 반응하는 대신 사용자가 실제로 볼 프레임에 작업을 배치한 셈이에요.

새 노드를 만들기보다 기존 노드를 다시 썼어요

셀마다 별도 아이콘 요소를 두면 같은 장식 때문에 DOM이 늘어요. 타입 아이콘을 공유 SVG 데이터 URI 배경으로 바꾸자 브라우저가 같은 이미지를 재사용할 수 있었어요. 무거운 편집기도 모든 셀에 붙이지 않고 더블클릭한 셀에만 마운트했어요.

행과 열은 데이터 값이 아니라 화면상 위치로 추적했어요. 스크롤 창이 이동해도 같은 DOM 노드를 유지하고 내용만 교체해요. Angular의 `trackBy`, React와 Vue의 `key`를 데이터 정체성이 아닌 렌더링 슬롯에 맞춘 방식이에요. 새 노드와 이벤트 리스너를 계속 만들지 않으니 윈도 전환 비용이 줄어요.

왜 중요한가요

이 사례는 대용량 UI 최적화를 한 가지 기술로 설명하지 않아요. 양축 가상화가 렌더링 개수를 줄이고, 섀도 테이블이 반복 계산을 막아요. 이벤트 처리는 프레임 단위로 합치고, DOM 생성 비용은 풀링과 지연 마운트로 낮춰요. 병목을 측정한 뒤 서로 다른 층에서 비용을 덜어낸 과정이 구체적이에요. 2

Canvas는 더 높은 성능 상한을 제공하지만 텍스트 선택, 접근성, 셀별 상호작용을 직접 구현해야 해요. 이 프로젝트는 DOM을 유지하면서 필요한 성능에 도달하는 쪽을 골랐어요. 데이터 크기뿐 아니라 편집, 검색, 드래그 같은 제품 요구까지 놓고 렌더링 방식을 선택했다는 점이 실무에서 참고할 만해요.

직접 그리드를 만들 계획이 없더라도 적용 범위는 넓어요. 긴 목록, 로그 뷰어, 타임라인, 파일 탐색기처럼 반복 요소가 많은 화면에서도 표시 상태 사전 계산, 이벤트 위임, 공유 배경 이미지, 위치 기반 DOM 재사용을 검토할 수 있어요. 다만 처음부터 모든 기법을 넣기보다 성능 프로파일에서 실제 병목을 확인한 뒤 하나씩 적용하는 편이 안전해요.

참고 자료

  1. 데이터 그리드 구축의 10단계 — GeekNews
  2. The 10 Levels of Building a Data Grid — VisuaLeaf Blog
728x90