50,000개의 리스트를 부드럽게 렌더링하는 간단한 트릭

발행: (2026년 1월 18일 오전 09:22 GMT+9)
12 min read
원문: Dev.to

Source: Dev.to

위에 제공된 Source 링크 외에 번역할 텍스트가 포함되어 있지 않습니다. 번역을 원하는 본문을 제공해 주시면 한국어로 번역해 드리겠습니다.

왜 50,000개의 아이템을 렌더링하는 것이 나쁠까?

이 정도 과장된 숫자라면 명백해 보일 수 있지만, 1,000개와 같이 훨씬 작은 숫자는 어떨까요? 숫자는 임의적이며, 디바이스와 그 자원에 따라 무게가 달라지기 때문에 별다른 의미가 없습니다.

DOM(Document Object Model)은 웹 페이지의 HTML 구조를 노드와 객체의 트리 형태로 나타냅니다. 이 트리는 브라우저의 렌더링 엔진에 의해 생성되며, 우리가 직접 관리할 필요는 없습니다. DOM 노드가 변경되면 브라우저는 다음 작업을 수행해야 할 수 있습니다:

  1. 스타일 재계산
  2. 페이지 레이아웃 재배치
  3. 페이지 일부 재페인팅

관여되는 노드가 많을수록 이 과정은 더 비용이 많이 들게 됩니다.

Source:

가상 DOM

가상 DOM은 실제 DOM의 메모리 내 표현으로, 브라우저의 렌더링 엔진과 상호 작용하지 않습니다. 상태 변화가 발생하면 React는 실제 DOM 대신 먼저 가상 DOM을 업데이트합니다. 그런 다음 diffing 알고리즘을 사용해 필요한 변경 사항을 식별합니다. 이 선택적 메커니즘은 다음과 같은 이유로 성능과 예측 가능성을 크게 향상시킵니다:

  • 불필요한 재렌더링 감소
  • 실제 DOM에 대한 직접 조작 최소화

예시: 선적 항구를 선택하는 <select>

<select>
  <option value="NLRTM">Rotterdam</option>
  <option value="DEHAM">Hamburg</option>
  <option value="FRLAV">Le Havre</option>
</select>

가상 DOM에서 “Rotterdam” 옵션은 (명확성을 위해 단순화된) 다음과 같이 보입니다:

{
  type: 'option',
  props: {
    value: 'NLRTM',
    children: 'Rotterdam'
  },
  key: null,
  stateNode: HTMLOptionElement, // 실제 DOM 노드에 대한 참조
  return: FiberNode,            // 부모를 가리키는 포인터
  sibling: FiberNode,           // “Hamburg”을 가리키는 포인터
  // ... 수십 개의 다른 내부 조정 속성
}

보시다시피 React가 각 노드를 관리하기 위해 필요로 하는 메타데이터는 상당합니다. <select>1,000개의 항구가 있다면, React의 메모리는 이러한 객체 1,000개로 채워집니다. 이 컴포넌트가 렌더링될 때마다 React는 브라우저 엔진이 만든 JavaScript 및 C++ 객체 외에도 큰 연결 리스트를 탐색해야 할 수도 있습니다.

1,000개의 항목이 현대 머신을 충돌시키지는 않지만 16 ms 프레임 예산을 초과할 수 있어 프레임 손실이 발생합니다—특히 스크롤이나 타이핑 같은 사용자 인터랙션과 결합될 때 그렇습니다. 모바일 기기는 자원이 훨씬 제한적이기 때문에 문제가 더욱 악화됩니다. 모바일에서 길고 가상화되지 않은 리스트를 스크롤하면 무거운 느낌이 들고 눈에 띄는 끊김(jank)이 발생합니다.

Note: 복잡한 웹 페이지는 각각 자체 오버헤드를 가진 많은 컴포넌트들로 구성됩니다. 컴포넌트를 격리된 상태에서 설계·개발·테스트하면 누적 비용이 숨겨질 수 있습니다.

메모이제이션만으로는 충분하지 않은 이유

대부분의 React 개발자는 메모이제이션을 사용하려고 하는데, 이는 이해할 수 있습니다: 렌더링이 적어지면 작업량이 줄어드는 것처럼 들리기 때문이죠. 하지만 메모이제이션은 React가 컴포넌트 코드를 다시 실행하는 것을 막을 뿐, 브라우저가 그려야 하는 양을 줄여주지는 않습니다. 1,000개의 요소가 존재한다면, 브라우저는 여전히 그 1,000개의 요소를 레이아웃하고 페인팅해야 합니다.

스크롤과 같은 상호작용 중에는 React가 다시 렌더링하지 않더라도, 브라우저는 매 프레임마다 모든 보이는 요소의 레이아웃을 계산하고 페인팅해야 합니다. 요소가 충분히 많다면, 이것만으로도 끊김(jank)이 발생할 수 있습니다.

Render less, not faster

이상적인 상황이라면, 여러분이 사용하는 API가 paginate, filter, 혹은 search 기능을 제공하여 반환되는 결과 수를 줄일 수 있습니다. 포트 선택기 예시에서, 전 세계의 모든 포트(또는 유럽 전체의 포트)를 반환하는 대신, 선택된 국가의 포트만—예를 들어 50개 정도만—돌려줄 수 있습니다.

하지만 API를 항상 제어할 수 있는 것은 아닙니다:

  • 내부 API가 다른 팀에 의해 관리될 수 있습니다.
  • 서드파티 API는 전적으로 여러분의 손이 닿지 않는 영역입니다.

이러한 경우, 책임은 UI 쪽으로 넘어갑니다. 큰 데이터셋을 받더라도 한 번에 모두 렌더링할 필요는 없습니다. 렌더링을 줄이는 방법은 다음과 같습니다:

  • 화면에 보이는 부분만 표시하기.
  • 사용자가 상호작용할 때까지 작업을 지연시키기.
  • 큰 드롭다운을 검색 기반 인터페이스로 교체하기.

목표는 렌더링 속도를 높이는 것이 아니라, 처음부터 불필요한 작업을 하지 않는 것입니다. 브라우저가 처리해야 할 요소 수를 줄이면, 다른 모든 것이 기본적으로 더 저렴해집니다.

우리는 이를 windowing(또는 virtualization)이라고 하는 기법으로 구현합니다.

가상화란 무엇인가?

그 용어는 실제보다 더 위협적으로 들릴 수 있습니다. 가상화는 뷰포트에 보이는 부분만 렌더링한다는 의미입니다. 뷰포트는 리스트를 표시하는 어떤 요소든 될 수 있습니다—예를 들어 <Select> 컴포넌트의 경우 옵션을 보여주는 스크롤 가능한 컨테이너가 뷰포트가 됩니다.

뷰포트에 여섯 개의 요소가 표시된다면, 브라우저는 데이터셋이 얼마나 크든 정확히 여섯 개의 요소만 렌더링합니다. 우리는 단순히 요소를 숨기는 것이 아니라, 화면 밖으로 나가면 DOM에서 제거합니다.

Virtualization illustration

Caveat: 화면 밖에 있는 항목은 물리적으로 DOM에서 제거되기 때문에, “페이지 내 찾기”와 같은 기본 브라우저 기능으로는 해당 항목을 찾을 수 없습니다. 또한, 모든 옵션이 존재한다고 가정하는 특정 상호작용(예: 키보드 네비게이션)에는 추가 처리가 필요할 수 있습니다.

Bottom line

  • Big lists can cripple performance, especially on constrained devices.
    큰 리스트는 성능을 크게 저하시킬 수 있으며, 특히 제약이 있는 디바이스에서 그렇습니다.

  • Memoization helps with React’s work but does nothing for the browser’s layout/paint cost.
    메모이제이션은 React 작업에 도움이 되지만 브라우저의 레이아웃/페인트 비용에는 영향을 주지 못합니다.

  • Virtualization/windowing removes off‑screen elements from the DOM, dramatically reducing the work the browser must do.
    가상화/윈도윙은 화면에 보이지 않는 요소들을 DOM에서 제거하여 브라우저가 수행해야 할 작업을 크게 줄입니다.

Use virtualization whenever you have large collections, and fall back to pagination or server‑side filtering when possible. This keeps your UI snappy and your users happy.
큰 컬렉션을 다룰 때는 언제든 가상화를 사용하고, 가능하면 페이지네이션이나 서버‑사이드 필터링으로 대체하십시오. 이렇게 하면 UI가 반응성이 뛰어나고 사용자가 만족합니다.

Source:

Summary

채팅 기능이든, 소셜 피드이든, 혹은 단순히 Select 컴포넌트를 만들든, 가상화(virtualization)를 적용하면 앱이 훨씬 부드럽게 느껴집니다. 직접 사용해 보고 싶다면, 검증된 라이브러리를 사용하는 것을 권장합니다. React의 경우 다음과 같은 선택지가 있습니다:

이 글이 도움이 되었고, 앞으로도 이런 콘텐츠를 더 만들고 싶다면 다음과 같이 참여해 주세요:

Cover image by Edson Junior on Unsplash.

Back to Blog

관련 글

더 보기 »

React는 어떻게 작동하나요?

Component는 기본 React 앱이 컴포넌트들로 구성된다는 것을 의미합니다. 컴포넌트는 UI를 반환하는 JavaScript 함수일 뿐입니다. javascript function App { return Hello ; } JS...

State를 이동하여 리렌더링 최적화

소개 성능을 향상시키고 불필요한 재‑렌더링을 줄이는 한 가지 방법은 상태를 낮추는 것입니다. 특히 그 상태가 특정 부분에만 영향을 미치는 경우에 효과적입니다.