무한 스크롤러의 복잡성

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

무한 스크롤러가 인터넷 곳곳에 표시됩니다. Google Music의 아티스트 목록은 하나이고, Facebook의 타임라인은 하나이며, 트위터의 실시간 피드도 하나입니다. 아래로 스크롤하다가 하단에 도달하기 전에 마치 갑자기 새 콘텐츠가 표시되는 것처럼 보입니다. 사용자에게 원활한 환경을 제공하며 매력을 쉽게 확인할 수 있습니다.

하지만 무한 스크롤러의 기술적 문제는 생각보다 어렵습니다. 올바른 방식™으로 일을 처리하려고 할 때 발생하는 문제의 범위는 매우 넓습니다. 콘텐츠가 계속해서 바닥글을 밀어내기 때문에 바닥글의 링크에 거의 액세스할 수 없게 되는 것과 같은 간단한 것부터 시작됩니다. 하지만 문제는 점점 더 어려워집니다. 사용자가 휴대전화를 세로 모드에서 가로 모드로 전환할 때 크기 조절 이벤트를 처리하려면 어떻게 해야 하나요? 또는 목록이 너무 길어질 때 휴대전화가 멈추지 않도록 하려면 어떻게 해야 하나요?

The right thing™

이러한 이유로 성능 표준을 유지하면서 이러한 모든 문제를 재사용 가능한 방식으로 해결하는 방법을 보여주는 참조 구현을 제공하기로 했습니다.

목표를 달성하기 위해 DOM 재활용, 삭제된 노드, 스크롤 고정이라는 세 가지 기법을 사용합니다.

데모 케이스는 메시지를 스크롤할 수 있는 행아웃과 유사한 채팅 창입니다. 먼저 무한한 채팅 메시지 소스가 필요합니다. 기술적으로는 현재 존재하는 무한 스크롤러 중 실제로 무한인 것은 없지만 이러한 스크롤러에 제공할 수 있는 데이터의 양을 고려하면 무한이라고 해도 무방합니다. 간단히 하기 위해 일련의 채팅 메시지를 하드코딩하고 메시지, 작성자, 가끔 이미지 첨부파일을 무작위로 선택한 후 인위적인 지연을 약간 추가하여 실제 네트워크와 조금 더 유사하게 작동하도록 합니다.

Chat 앱 스크린샷

DOM 재활용

DOM 재활용은 DOM 노드 수를 적게 유지하기 위해 활용도가 낮은 기법입니다. 일반적인 생각은 새 DOM 요소를 만드는 대신 화면 밖에 있는 이미 만들어진 DOM 요소를 사용하는 것입니다. DOM 노드 자체는 저렴하지만 각 노드가 메모리, 레이아웃, 스타일, 페인트에 추가 비용을 추가하므로 무료는 아닙니다. 웹사이트의 DOM이 관리하기에 너무 크면 저가형 기기는 완전히 사용할 수 없지는 않더라도 속도가 눈에 띄게 느려집니다. 또한 스타일의 모든 재레이아웃 및 재적용(클래스가 노드에 추가되거나 삭제될 때마다 트리거되는 프로세스)은 DOM이 커질수록 비용이 증가합니다. DOM 노드를 재활용하면 총 DOM 노드 수를 상당히 줄여서 이러한 모든 프로세스를 더 빠르게 처리할 수 있습니다.

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

스크롤할 때마다 뷰포트가 활주로 끝에 충분히 가까워졌는지 확인합니다. 그렇다면 센티널 요소를 이동하고 뷰포트를 벗어난 항목을 활주로 하단으로 이동하여 활주로를 확장하고 새 콘텐츠로 채웁니다.

Runway Sentinel Viewport

다른 방향으로 스크롤하는 경우에도 마찬가지입니다. 그러나 스크롤바 위치가 일관되게 유지되도록 구현 시에는 활주로를 축소하지 않습니다.

Tombstone

앞서 언급했듯이 Google에서는 데이터 소스가 실제 세계의 것처럼 작동하도록 노력하고 있습니다. 네트워크 지연 시간과 모든 것을 고려합니다. 즉, 사용자가 플릭 스크롤을 사용하면 데이터가 있는 마지막 요소를 쉽게 지나칠 수 있습니다. 이 경우 데이터가 도착하면 실제 콘텐츠가 포함된 항목으로 대체되는 자리표시자 항목인 삭제된 항목이 배치됩니다. 또한 비석은 재활용되며 재사용 가능한 DOM 요소를 위한 별도의 풀이 있습니다. 그래야 비어 있는 항목에서 콘텐츠가 채워진 항목으로 원활하게 전환할 수 있습니다. 그렇지 않으면 사용자에게 매우 불편하게 느껴질 수 있으며 사용자가 집중하고 있는 항목을 놓칠 수도 있습니다.

그런 무덤. 매우 석회질입니다. 와우.

여기서 흥미로운 문제는 항목당 텍스트 또는 첨부된 이미지의 양이 다르기 때문에 실제 항목의 높이가 삭제된 항목보다 클 수 있다는 점입니다. 이 문제를 해결하기 위해 데이터가 들어오고 뷰포트 위에서 삭제된 항목이 교체될 때마다 현재 스크롤 위치를 조정하여 스크롤 위치를 픽셀 값이 아닌 요소에 앵커링합니다. 이 개념을 스크롤 고정이라고 합니다.

스크롤 고정

스크롤 고정은 툼스톤이 교체될 때와 창 크기가 조절될 때 (기기가 뒤집힐 때도 발생) 모두 호출됩니다. 표시 영역에서 가장 상단에 표시되는 요소를 파악해야 합니다. 이 요소는 부분적으로만 표시될 수 있으므로 표시 영역이 시작되는 요소 상단에서의 오프셋도 저장합니다.

스크롤 고정 다이어그램

뷰포트 크기가 조정되고 활주로가 변경되면 사용자에게 시각적으로 동일하게 느껴지는 상황을 복원할 수 있습니다. 승리! 크기가 조절된 창은 각 항목의 높이가 변경되었을 수 있다는 의미이므로 고정된 콘텐츠를 얼마나 아래에 배치해야 하는지 어떻게 알 수 있나요? YouTube는 그렇지 않습니다. 이를 확인하려면 고정된 항목 위에 있는 모든 요소를 배치하고 모든 높이를 더해야 합니다. 그러면 크기 조정 후 상당한 일시중지가 발생할 수 있으며 이는 바람직하지 않습니다. 대신 위의 모든 항목이 삭제된 항목과 크기가 같다고 가정하고 그에 따라 스크롤 위치를 조정합니다. 요소가 런웨이로 스크롤되면 스크롤 위치를 조정하여 레이아웃 작업이 실제로 필요할 때까지 효과적으로 지연합니다.

레이아웃

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

이상적으로는 항목이 DOM에 연결될 때만 한 번만 다시 칠해지고, 런웨이에서 다른 항목이 추가되거나 삭제되어도 영향을 받지 않아야 합니다. 이는 가능하지만 최신 브라우저에서만 가능합니다.

최신 트윅

최근 Chrome에서는 개발자가 브라우저에 요소가 레이아웃 및 페인트 작업의 경계라고 알릴 수 있는 기능인 CSS 컨테이닝 지원을 추가했습니다. 여기서는 직접 레이아웃을 실행하므로 컨테이너에 적합한 애플리케이션입니다. 런웨이에 요소를 추가할 때마다 다른 항목은 재레이아웃의 영향을 받지 않아도 된다는 것을 알 수 있습니다. 따라서 각 항목은 contain: layout를 가져와야 합니다. 또한 웹사이트의 나머지 부분에 영향을 미치지 않으려면 런웨이 자체에도 이 스타일 디렉티브를 적용해야 합니다.

고려한 또 다른 사항은 사용자가 요소 재활용을 시작하고 새 데이터를 로드하기에 충분히 스크롤했을 때 이를 감지하는 메커니즘으로 IntersectionObservers를 사용하는 것입니다. 그러나 IntersectionObservers는 requestIdleCallback를 사용하는 것처럼 지연 시간이 길도록 지정되므로 IntersectionObservers를 사용하지 않는 것보다 반응이 느린 것처럼 느껴질 수 있습니다. scroll 이벤트를 사용하는 현재 구현에서도 스크롤 이벤트가 '최선의 방식'으로 전달되므로 이 문제가 발생합니다. 결국 Houdini의 컴포저이터 워크렛이 이 문제에 대한 고화질 솔루션이 될 것입니다.

아직 완벽하지는 않음

현재 DOM 재활용 구현은 실제로 화면에 표시되는 요소만 고려하는 대신 뷰포트를 통과하는 모든 요소를 추가하므로 이상적이지 않습니다. 즉, 정말 빠르게 스크롤하면 Chrome에 레이아웃과 페인트 작업이 너무 많이 실행되어 Chrome이 이를 따라잡을 수 없습니다. 배경만 표시됩니다. 세상이 끝나는 것도 아니지만 개선할 필요가 있습니다.

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

모든 코드는 저장소에서 확인할 수 있습니다. 재사용 가능하도록 하기 위해 최선을 다했지만 npm 또는 별도의 저장소에 실제 라이브러리로 게시하지는 않을 예정입니다. 주된 용도는 교육입니다.