Uber는 대규모, 대용량 데이터 시각화를 주로 다루고 있습니다. Uber에서 메인 기술로 삼는 지도 기반 시각화는 아니지만, Uber에서 대용량의 리스트 데이터 렌더링에 대해 알기 쉽게 설명한 글이 있어서 가져와봤습니다. 아래는 Uber의 FE아티클을 우리말로 번역, 의역한 것 입니다.
React에서 대용량의 리스트를 렌더링하는 것은 개발자들에게 까다로운 일이 될 수 있습니다. 리스트의 크기가 커짐에 따라 DOM (Document Object Model) 트리도 같이 커지고, 느린 렌더링, 버벅거리는 스크롤, 높은 메모리 사용량 등과 같은 성능 이슈를 동반하게 됩니다. 아 아티클에서는, 개발자들이 대용량 리스트를 렌더링하는 과정에서 마주하는 일반적인 문제를 소개하고, 이를 해결하기 위해 사용될 수 있는 다양한 해결책을 소개하려 합니다.
웹 브라우저에서 대용량 리스트를 렌더링하는 것이 왜 어려운 작업일까요? 대용량 리스트를 렌더링 하는 과정에서 고려해야 할 몇 가지 요소가 있습니다. 첫번째는 성능으로, 화면에 렌더링할 요소의 수가 증가하면 브라우저의 렌더링 엔진에 성능 문제가 발생하기 시작합니다. 이로 인해 렌더링 속도가 느려져 사용자 인터페이스가 느려지고 사용자 경험이 저하될 수 있습니다.
대용량 리스트 요소들을 조작하는 것은 높은 계산 비용을 동반합니다. 최종 사용자에게 큰 목록을 스크롤하는 것은 버벅일 수 있고, 최악의 경우 완전히 응답하지 않는 페이지가 표시될 수도 있습니다. 휴대폰과 같은 저사양 디바이스에서는 처리 능력과 메모리가 제한되어 있기 때문에 부정적인 영향이 더욱 커질 수 있습니다.
이러한 모든 문제를 고려할 때 우리는 소프트웨어 개발자로서 최적화 기술을 사용하고 적절한 전략을 선택해야 합니다.
이 글은 React에서 대규모 리스트를 렌더링하는 것을 중심으로 다룹니다. 먼저 대규모 리스트가 무엇인지에 대해 정의하겠습니다. 그리고 문제 정의에서 지적한 문제를 해결하기 위해 통합할 수 있는 몇가지 가능한 해결책들을 살펴보겠습니다. 마지막으로 브라우저에서 대규모 리스트를 렌더링할 때 신경써야 할 몇 가지 핵심 사항들을 살펴보겠습니다.
대규모 리스트란 무엇일까요?
그림 1 : 대규모 리스트를 렌더링하기 위한 코드 스니펫
대규모 리스트에 대한 정의는 동적입니다. 이는 전적으로 최종 사용자의 기기에 따라 다릅니다. 사용자가 모두 고사양 컴퓨터나 모바일 기기를 사용하는 경우에는 성능 저하가 나타나기 전까지 페이지에서 안전하게 렌더링할 수 있는 요소의 수가 상당히 많을 것 입니다.
반면에, 최종 사용자의 기기가 한정된 메모리와 처리 능력을 가지는 평범한 기기라면 이 임계값은 위의 경우보다 작을 것 입니다. 최신 브라우저의 발달으로 방대한 양의 DOM 요소를 큰 걱정 없이 불러올 수 있습니다. 그럼에도 불구하고 순서가 있는 1,000개 이상의 리스트를 렌더링하기 시작하면 방대한 양이라고 생각할 수 있을 것 입니다.
수만개의 복잡한 리스트 항목을 렌더링해야 하는 상황이라면 최종 사용자의 하드웨어 성능에 상관없이 다음 해결책 중 하나를 사용해야 할 것 입니다. 먼저, 누구나 코드에 통합할 수 있는 일반적인 해결책부터 시작하겠습니다:
그림 2: 렌더링 트리 예시
일반적으로 간결한 렌더링 트리를 유지하는 것은 전반적인 성능 향상을 동반합니다. DOM 요소가 많을 수록 브라우저에서 더 많은 공간을 할당해야 함은 물론이고, 브라우저는 레아웃 단계에서 더 많은 시간 소모를 할 것 입니다. 그리고 이는 막대한 양의 리스트를 렌더링할 때 매우 중요합니다. 단 하나의 div를 줄이는 것도 성능 측면에서 눈에 띄는 차이를 가져올 수도 있습니다.
가장 좋은 방법은 디자인에 영향을 주지 않는 선에서 최소한의 DOM 요소를 사용하여 리스트 항목을 만드는 것 입니다.
먼저 약 10,000개의 요소로 구성된 간단한 리스트를 만들어보겠습니다. 이는 다음의 code sandbox에서 확인할 수 있습니다.
페이지의 Lighthouse 성능을 살펴봅시다:
그림3 : 대규모 리스트에 대한 Lighthouse 점수
측정항목 | 결과 |
---|---|
First contentful paint | 0.9 seconds |
Largest contentful paint | 3.0 seconds |
Total blocking time | 2.1 seconds |
Total performance score | 43 |
이제 리스트 항목에 div를 하나 더 추가하고 다시 성능 수치를 살펴보겠습니다.
다음은 Lighthouse 성능 수치입니다:
그림 4: 추가 div 한 개에 대한 Lighthouse 점수
측정항목 | 결과 |
---|---|
First contentful paint | 1 second |
Largest contentful paint | 3.3 seconds |
Total blocking time | 3.3 seconds |
Total performance score | 40 |
이제 실질적인 해결책을 살펴보겠습니다. 첫 번째는 무한 스크롤 기법을 사용하는 것 입니다. 간단히 말해, 이 기술은 전체 페이지 길이를 채우는 데 필요한 리스트 항목만 렌더링하고, 사용자가 아래로 스크롤할 때 항목을 더 추가하는 것을 의미합니다.
이는 react-infinite-scroller 라이브러리를 사용하여 구현할 수 있습니다.
다음은 react-infinite-scroller 라이브러리를 사용하여 10,000 아이템의 리스트를 구현한 예시 입니다.
성능에 어떠한 영향을 미치는지 살펴봅시다:
그림 5: 무한스크롤을 적용한 Lighthouse 점수
측정항목 | 결과 |
---|---|
First contentful paint | 1.0 seconds |
Largest contentful paint | 2.5 seconds |
Total blocking time | 160ms |
Total performance score | 78 |
총 성능 점수는 43에서 78으로 크게 증가했습니다.
이는 기본적으로 리스트를 작은 부분으로 작게 잘랐기 때문입니다. 10,000개의 항목을 한 번에 모두 렌더링하는 대신 사용자의 시야에서 보일 페이지의 일부에 필요한 처음 몇 개의 항목만 렌더링했습니다. 사용자가 아래로 스크롤하면 react-infinite-scroller 라이브러리가 DOM에 더 많은 항목을 추가합니다.
이 문제에 대한 또 다른 해결책은 윈도우 기법을 사용하는 것입니다.
윈도우 기법(또는 가상화)은 언제든지 전체 목록중 사용자에게 보이는 부분만 렌더링하는 기술입니다. 무한 스크롤과 달리 윈도우 기법에서는 DOM이 가지는 요소 수가 항상 일정합니다. 즉, 사용자의 시야를 채우는 데 필요한 요소만 렌더링하고 리스트의 위쪽과 아래쪽에서 아직 시야에 들어오지 않은 부분은 제거합니다.
그림 6: 윈도우 기법 예시
윈도우 기법은 특히 리스트의 각 요소들이 고정된 높이값을 가질 때 빛을 발합니다.
이렇게 하면 스크롤에 따라 DOM에서 어떤 항목을 추가하거나 제거해야 하는지 쉽게 계산할 수 있습니다. 또한 사용자가 스크롤한 위치에 상관없이 DOM 요소의 수가 일정하게 유지되므로 페이지의 메모리 사용량이 일정하게 유지됩니다.
다음은 react-window를 사용하여 동일한 10,000개 항목 목록을 구현한 예시입니다.
성능에 어떤 영향을 미치는지 살펴봅시다:
그림 7: 윈도우기법을 사용한 대용량 리스트의 Lighthouse 점수
측정항목 | 결과 |
---|---|
First contentful paint | 1.0 seconds |
Largest contentful paint | 2.5 seconds |
Total blocking time | 190ms |
Total performance score | 76 |
이 두 가지 기술을 사용하게 되면 브라우저 검색 기능을 사용할 수 없게 됩니다. 즉, 사용자가 아직 렌더링 되지 않은 항목에 대해 cmd + f(ctrl + f) 검색을 수행하면 일치하는 항목이 0/0으로 표시될 것 입니다. 이를 방지하기 위해서는 리스트에 필터링 옵션이나 사용자 지정 검색을 추가해야 합니다.
또한 네이티브 스크롤에 비하면 DOM에서 요소를 추가/제거할 때 약간의 지연이 발생합니다. 하지만, 이는 엄청난 양의 DOM 요소로 인해 궁극적으로 어플리케이션이 중단되는것 보다는 낫습니다.
가볍게 유지하고 라이브러리를 사용하고 싶지 않다면 Intersection Observer API를 사용하여 자체적으로 DOM 요소의 지연 로딩을 구현할 수도 있습니다.
먼저 뷰포트를 채울 만큼 충분한 컴포넌트를 추가합니다. getBoundingClientRect
를 사용하여 뷰포트 높이를 구하고 이를 가장 작은 요소의 높이로 나눕니다. 이 수에 1을 더하면 좋습니다. 이제 그 아래에 더미 컴포넌트를 추가합니다(이 컴포넌트는 아무것도 표시할 필요 없이 리스트의 맨 아래에 존재하기만 하면 됩니다). 이 컴포넌트에 intersection observable
을 연결하고 이 컴포넌트가 표시될 때마다 리스트에 항목을 더 추가하여 DOM에 렌더링합니다. 이렇게 하면 간단한 무한 스크롤 해결책을 개발할 수 있습니다.
이제 React 어플리케이션에서 큰 리스트를 다룰 수 있을 것입니다. 간단히 말해서, 한 번에 리스트의 작은 부분만 렌더링하고 사용자가 리스트를 잘게 쪼개서 볼 수 있도록 현재 시점(view)에 충분한 필터가 있는지 확인해야 합니다.
보통의 프로젝트를 할 때 구현단계에서 끝나는 경우가 많아 리스트에 추가 최적화 기술을 적용하지 않거나, 적용하더라도 무한스크롤 정도에서 종료하는 경우가 있었는데, 새로 windowing
기술에 대해 알 수 있었습니다.
추가로 생각할 수 있는 방법에는 무한 스크롤을 위, 아래로 적용하거나 pagination
을 적용하는 방법또한 프로젝트의 상태에 따라 적용가능한 방법이지 않을까 생각됩니다.