무한 스크롤러의 복잡성

로버트 플랙
로버트 플랙

요약: DOM 요소를 재사용하고 표시 영역에서 멀리 떨어져 있는 요소는 삭제합니다. 자리표시자를 사용하여 지연된 데이터를 고려합니다. 다음은 무한 스크롤러의 데모코드입니다.

인터넷 전체에 무한 스크롤러가 표시됩니다. Google 뮤직의 아티스트 목록이 1개, Facebook의 타임라인, Twitter의 실시간 피드도 포함됩니다. 아래로 스크롤하다가 내려가면 새 콘텐츠가 어딘지 모르게 마법처럼 나타납니다. 사용자에게 원활한 환경이며 관심을 끌기도 쉽습니다.

하지만 무한 스크롤러의 기술적 도전과제는 보기보다 어렵습니다. The Right ThingTM을 하고 싶을 때 접하게 되는 문제의 범위는 방대합니다. 콘텐츠가 바닥글을 계속 밀어내므로 바닥글의 링크와 실제로 연결할 수 없게 되는 등의 간단한 상황으로 시작됩니다. 그러나 문제는 더욱 어려워집니다. 사용자가 휴대전화를 세로 모드에서 가로 모드로 바꿀 때 크기 조절 이벤트를 어떻게 처리하나요? 또는 목록이 너무 길어져서 휴대전화가 멈춰서 고통스러울 정도로 멈추지 않도록 하려면 어떻게 해야 할까요?

올바른 물건TM

성능 표준을 유지하면서 재사용 가능한 방식으로 모든 문제를 처리하는 방법을 보여주는 참조 구현을 고안하기에 충분한 이유가 있다고 생각했습니다.

이 목표를 달성하기 위해 DOM 재활용, Tombstone, 스크롤 앵커링이라는 3가지 기법을 사용할 예정입니다.

데모 사례는 메시지를 스크롤할 수 있는 행아웃과 유사한 채팅 창입니다. 가장 먼저 필요한 것은 채팅 메시지의 무한한 소스입니다. 기술적으로 무한 스크롤러는 실제로 무한합니다. 하지만 이러한 스크롤러로 끌어올릴 수 있는 데이터의 양을 고려할 때 진짜로 무한한 스크롤러도 있습니다. 편의상 채팅 메시지 세트를 하드 코딩하고 실제 네트워크와 조금 더 비슷하게 작동하도록 인위적인 지연을 가미하여 메시지, 작성자, 가끔 이미지 첨부파일을 무작위로 선택합니다.

채팅 앱 스크린샷

DOM 재활용

DOM 재활용은 DOM 노드 수를 낮게 유지하기 위해 잘 사용되지 않는 기술입니다. 일반적으로 화면 밖에 있는 기존 DOM 요소를 새로 만드는 대신 사용하여 이미 만든 DOM 요소를 사용합니다. DOM 노드 자체는 당연히 비용이 저렴하지만 각각 메모리, 레이아웃, 스타일, 페인트에 추가 비용이 발생하므로 무료는 아닙니다. 웹사이트의 DOM이 너무 커서 관리할 수 없는 경우에는 저사양 기기의 경우 완전히 사용할 수 없는 경우가 아니면 속도가 현저히 느려질 수 있습니다. 또한 스타일의 모든 재배치 및 재적용(클래스가 노드에서 추가되거나 삭제될 때마다 트리거되는 프로세스)이 커질수록 비용이 더 많이 듭니다. DOM 노드를 재활용하면 총 DOM 노드 수를 상당히 줄여 이러한 모든 프로세스가 빨라집니다.

첫 번째 장애물은 스크롤 자체입니다. DOM에는 사용 가능한 모든 항목의 매우 작은 하위 집합만 있을 것이므로 브라우저의 스크롤바가 이론적으로 존재하는 콘텐츠의 양을 적절하게 반영하도록 하는 다른 방법을 찾아야 합니다. 1px x 1px 센티널 요소를 변환과 함께 사용하여 항목이 포함된 요소(활주로)에 원하는 높이를 지정하도록 합니다. 활주로의 모든 요소를 자체 레이어로 승격하여 활주로의 레이어가 완전히 비어 있도록 합니다. 배경 색상도 없고요. 런웨이의 레이어가 비어 있지 않으면 브라우저를 최적화할 수 없으며 높이가 수십만 픽셀인 텍스처를 그래픽 카드에 저장해야 합니다. 확실히 휴대기기에서는 실행할 수 없습니다.

스크롤할 때마다 표시 영역이 활주로 끝에 가까운지 확인합니다. 이 경우 센티널 요소를 이동하고 표시 영역을 벗어난 항목을 활주로 하단으로 이동하여 새 콘텐츠를 채워 활주로를 확장합니다.

런웨이 센티널 센티널 2 센티널 2

다른 방향으로 스크롤할 때도 마찬가지입니다. 하지만 스크롤바 위치가 일관되게 유지되도록 구현에서는 활주로를 축소하지 않습니다.

Tombstone

앞서 언급했듯이 데이터 소스가 실제로 작동하는 것처럼 작동하려고 합니다. 네트워크 지연 시간 등의 문제가 있습니다. 즉, 사용자가 플리키 스크롤을 사용하면 데이터가 있는 마지막 요소를 쉽게 지나칠 수 있습니다. 이 경우, 데이터가 도착하면 실제 콘텐츠로 대체될 Tombstone 항목(자리표시자)을 배치합니다. Tombstone도 재활용되며 재사용 가능한 DOM 요소를 위한 별도의 풀이 있습니다. Tombstone에서 콘텐츠가 채워진 항목으로 원활하게 전환할 수 있도록 이 작업이 필요합니다. 이렇게 하지 않으면 사용자에게 큰 혼란을 줄 수 있으며 실제로 사용자가 집중하고 있는 항목을 추적하지 못하게 될 수 있습니다.

그런
무덤이다. 정말 바위같아. 세상에.

여기서 흥미로운 도전과제는 항목 또는 첨부된 이미지당 텍스트 양이 다르기 때문에 실제 항목이 Tombstone 항목보다 높이가 더 클 수 있다는 점입니다. 이 문제를 해결하기 위해 데이터가 들어올 때마다 표시 영역 위에 Tombstone이 교체될 때마다 현재 스크롤 위치를 조정하여 스크롤 위치를 픽셀 값이 아닌 요소에 고정합니다. 이 개념을 스크롤 고정이라고 합니다.

스크롤 고정

스크롤 앵커링은 Tombstone이 교체될 때와 창의 크기가 조절될 때 (기기가 뒤집힐 때 발생함) 모두 호출됩니다. 표시 영역에서 맨 위에 보이는 요소가 무엇인지 파악해야 합니다. 이 요소는 부분적으로만 표시될 수 있으므로 표시 영역이 시작되는 요소의 상단에서 오프셋도 저장합니다.

스크롤 고정 다이어그램

표시 영역의 크기가 조절되고 활주로가 변경되면 사용자와 시각적으로 동일하게 보이는 상황을 복원할 수 있습니다. 승리! 크기가 조절된 창을 제외하면 각 항목의 높이가 변경되었을 가능성이 있습니다. 이 경우 고정된 콘텐츠를 얼마나 아래쪽에 배치해야 하는지 어떻게 알 수 있을까요? 안 됩니다! 이를 확인하기 위해 고정된 항목 위에 모든 요소의 레이아웃을 지정하고 모든 높이를 더해야 합니다. 이로 인해 크기 조절 후 상당한 일시중지가 발생할 수 있으므로 이를 원치 않습니다. 대신 위의 모든 항목이 Tombstone과 크기가 같다고 가정하고 그에 따라 스크롤 위치를 조정합니다. 요소가 활주로로 스크롤될 때 스크롤 위치를 조정하여 레이아웃 작업을 실제로 필요할 때로 연기합니다.

레이아웃

레이아웃이라는 중요한 세부정보는 건너뛰었습니다. DOM 요소를 재활용할 때마다 일반적으로 전체 활주로가 재배치되므로 초당 60프레임이라는 목표보다 훨씬 낮아집니다. 이를 피하기 위해 직접 레이아웃의 부담을 덜고 변환과 함께 절대 위치로 배치된 요소를 사용합니다. 이렇게 하면 실제로는 빈 공간만 있을 때 활주로 위쪽의 모든 요소가 여전히 공간을 차지하고 있다고 가정할 수 있습니다. 직접 레이아웃을 작업하므로 각 항목이 끝나는 위치를 캐시하고 사용자가 뒤로 스크롤할 때 캐시에서 올바른 요소를 즉시 로드할 수 있습니다.

항목은 DOM에 연결될 때 한 번만 다시 페인트되고 런웨이에서 다른 항목을 추가하거나 삭제해도 안되는 것이 이상적입니다. 이는 최신 브라우저에서만 가능합니다.

최첨단 조정

최근 Chrome에서 CSS Containment 지원을 추가했습니다. 이 기능을 사용하면 개발자가 요소에 레이아웃 및 페인트 작업의 경계임을 브라우저에 알릴 수 있습니다. 여기에서 직접 레이아웃을 작업하므로 이를 포함하는 주요 애플리케이션입니다. 런웨이에 요소를 추가할 때마다 다른 항목은 재레이아웃의 영향을 받을 필요가 없다는 것을 알게 됩니다. 따라서 각 항목은 contain: layout여야 합니다. 또한 웹사이트의 나머지 부분에 영향을 미치지 않으므로 런웨이 자체에도 이 스타일 지시어가 있어야 합니다.

또 다른 고려 사항은 사용자가 요소 재활용을 시작하고 새 데이터를 로드할 수 있을 만큼 충분히 스크롤했는지 감지하는 메커니즘으로 IntersectionObservers를 사용하는 것입니다. 그러나 IntersectionObservers는 requestIdleCallback을 사용하는 경우처럼 긴 지연 시간으로 지정되므로 IntersectionObservers가 없을 때보다 IntersectionObservers의 반응이 느릴 수 있습니다. scroll 이벤트를 사용하는 현재 구현에서도 스크롤 이벤트가 '최선의 노력'에 따라 전달되기 때문에 이 문제가 발생합니다. 결국 Houdini의 Compositor Worklet이 이 문제에 대한 Hi-Fi 솔루션이 될 것입니다.

아직 완벽하지 않음

현재 DOM 재활용의 구현은 실제로 화면에 표시되는 요소만 신경 쓰는 대신 표시 영역을 통과하는 모든 요소를 추가하기 때문에 바람직하지 않습니다. 즉, 실제로 빠르게 스크롤하면 Chrome에서 레이아웃과 페인트에 너무 많은 작업을 맡기게 되어 따라갈 수 없습니다. 결국 배경만 표시됩니다. 세상의 끝은 아니지만 확실히 개선해야 할 과제입니다.

우수한 사용자 환경과 높은 성능 표준을 결합하려고 할 때 단순한 문제가 얼마나 어려워질 수 있는지 확인하시길 바랍니다. 프로그레시브 웹 앱이 휴대전화에서 핵심 환경이 되면서 이는 점점 더 중요해지고 웹 개발자는 성능 제약 조건을 준수하는 패턴을 사용하는 데 계속 투자해야 할 것입니다.

모든 코드는 Google 저장소에서 확인할 수 있습니다. 재사용 가능한 상태로 유지하기 위해 최선을 다했지만 npm에서 실제 라이브러리나 별도의 저장소로 게시하지는 않을 것입니다. 교육적 목적으로 주로 사용합니다.