요약: DOM 요소를 재사용하고 뷰포트에서 멀리 떨어진 요소를 삭제하세요. 자리표시자를 사용하여 지연된 데이터를 고려합니다. 무한 스크롤러의 데모와 코드는 다음과 같습니다.
무한 스크롤러는 인터넷 전반에 걸쳐 표시됩니다. Google Music의 아티스트 목록, Facebook의 타임라인, Twitter의 실시간 피드도 마찬가지입니다. 아래로 스크롤하면 하단에 도달하기 전에 새로운 콘텐츠가 갑자기 표시됩니다. 사용자에게 원활한 환경을 제공하며 그 매력을 쉽게 알 수 있습니다.
하지만 무한 스크롤러의 기술적 과제는 보이는 것보다 어렵습니다. 올바른 일™을 하려고 할 때 발생하는 문제의 범위는 광범위합니다. 콘텐츠로 인해 바닥글이 계속 밀려나 바닥글의 링크에 사실상 도달할 수 없게 되는 것과 같은 간단한 문제부터 시작됩니다. 하지만 문제는 더 어려워집니다. 사용자가 휴대전화를 세로에서 가로로 전환할 때 크기 조절 이벤트를 어떻게 처리하나요? 목록이 너무 길어질 때 휴대전화가 갑자기 멈추는 것을 어떻게 방지하나요?
The right thing™
성능 표준을 유지하면서 재사용 가능한 방식으로 이러한 모든 문제를 해결하는 방법을 보여주는 참조 구현을 마련할 충분한 이유가 있다고 생각했습니다.
목표를 달성하기 위해 DOM 재활용, 툼스톤, 스크롤 고정의 세 가지 기법을 사용할 것입니다.
데모 케이스는 메시지를 스크롤할 수 있는 행아웃과 유사한 채팅 창입니다. 가장 먼저 필요한 것은 채팅 메시지의 무한 소스입니다. 기술적으로는 무한 스크롤러가 진정한 무한은 아니지만 이러한 스크롤러에 입력할 수 있는 데이터의 양을 고려하면 무한이라고 해도 무방합니다. 간단하게 하기 위해 채팅 메시지 세트를 하드 코딩하고 메시지, 작성자, 가끔 이미지 첨부파일을 무작위로 선택하여 실제 네트워크와 약간 더 유사하게 동작하도록 인위적인 지연을 추가합니다.

DOM 재활용
DOM 재활용은 DOM 노드 수를 낮게 유지하는 데 활용도가 낮은 기술입니다. 일반적인 아이디어는 새 DOM 요소를 만드는 대신 화면에 표시되지 않는 이미 생성된 DOM 요소를 사용하는 것입니다. DOM 노드 자체는 저렴하지만 메모리, 레이아웃, 스타일, 페인트에 추가 비용이 발생하므로 무료는 아닙니다. 웹사이트에 관리하기에 너무 큰 DOM이 있으면 로우엔드 기기가 완전히 사용할 수 없지는 않더라도 눈에 띄게 느려집니다. 또한 클래스가 노드에서 추가되거나 삭제될 때마다 트리거되는 프로세스인 스타일의 재배치 및 재적용은 DOM이 커질수록 비용이 더 많이 듭니다. DOM 노드를 재활용하면 DOM 노드의 총수가 훨씬 낮게 유지되어 이러한 모든 프로세스가 더 빨라집니다.
첫 번째 장애물은 스크롤 자체입니다. DOM에는 언제든지 사용 가능한 모든 항목의 작은 하위 집합만 있으므로 브라우저의 스크롤바가 이론적으로 존재하는 콘텐츠의 양을 적절하게 반영할 수 있는 다른 방법을 찾아야 합니다. 변환을 사용하여 항목이 포함된 요소(런웨이)가 원하는 높이를 갖도록 하는 1px x 1px 센티널 요소를 사용합니다. 런웨이 자체의 레이어가 완전히 비어 있도록 런웨이의 모든 요소를 자체 레이어로 승격합니다. 배경색이 없습니다. 런웨이의 레이어가 비어 있지 않으면 브라우저의 최적화가 적용되지 않으며 높이가 수십만 픽셀인 텍스처를 그래픽 카드에 저장해야 합니다. 휴대기기에서는 확실히 불가능합니다.
스크롤할 때마다 뷰포트가 활주로 끝에 충분히 가까워졌는지 확인합니다. 이 경우 센티널 요소를 이동하고 뷰포트를 벗어난 항목을 러너웨이 하단으로 이동하여 러너웨이를 확장하고 새 콘텐츠로 채웁니다.
다른 방향으로 스크롤하는 경우에도 마찬가지입니다. 하지만 스크롤바 위치가 일관되게 유지되도록 구현에서 런웨이를 축소하지는 않습니다.
Tombstone
앞서 언급했듯이 데이터 소스가 실제와 유사하게 작동하도록 노력합니다. 네트워크 지연 시간 등 모든 것을 고려해야 합니다. 즉, 사용자가 플리키 스크롤을 사용하는 경우 데이터가 있는 마지막 요소를 쉽게 지나쳐 스크롤할 수 있습니다. 이 경우 데이터가 도착하면 실제 콘텐츠가 있는 항목으로 대체되는 묘비 항목(자리표시자)이 배치됩니다. 묘비도 재활용되며 재사용 가능한 DOM 요소를 위한 별도의 풀이 있습니다. 이렇게 해야 묘비에서 콘텐츠가 채워진 항목으로 부드럽게 전환할 수 있습니다. 그렇지 않으면 사용자에게 매우 어색하게 느껴지고 실제로 집중하고 있던 항목을 놓칠 수 있습니다.

여기서 흥미로운 점은 실제 항목이 항목당 텍스트 양이 다르거나 첨부된 이미지로 인해 비석 항목보다 높이가 더 클 수 있다는 것입니다. 이 문제를 해결하기 위해 데이터가 들어오고 뷰포트 위에 툼스톤이 대체될 때마다 현재 스크롤 위치를 조정하여 스크롤 위치를 픽셀 값이 아닌 요소에 고정합니다. 이 개념을 스크롤 고정이라고 합니다.
스크롤 고정
스크롤 고정은 툼스톤이 대체될 때와 창 크기가 조절될 때 모두 호출됩니다 (기기가 뒤집힐 때도 발생함). 표시 영역에서 가장 위에 있는 표시 요소를 파악해야 합니다. 이 요소는 부분적으로만 표시될 수 있으므로 표시 영역이 시작되는 요소의 상단으로부터의 오프셋도 저장합니다.

뷰포트의 크기가 조정되고 트랙에 변경사항이 있는 경우 사용자에게 시각적으로 동일하게 느껴지는 상황을 복원할 수 있습니다. 승리! 하지만 창의 크기가 조절되면 각 항목의 높이가 변경될 수 있으므로 고정된 콘텐츠를 얼마나 아래에 배치해야 하는지 어떻게 알 수 있을까요? 아니요. 이를 확인하려면 고정된 항목 위에 있는 모든 요소를 배치하고 모든 높이를 더해야 합니다. 이렇게 하면 크기 조절 후 상당한 일시중지가 발생할 수 있으며 이는 바람직하지 않습니다. 대신 위의 모든 항목이 묘비와 크기가 동일하다고 가정하고 스크롤 위치를 적절하게 조정합니다. 요소가 활주로로 스크롤되면 스크롤 위치가 조정되어 레이아웃 작업이 실제로 필요할 때까지 효과적으로 지연됩니다.
레이아웃
중요한 세부정보인 레이아웃을 건너뛰었습니다. DOM 요소를 재활용하면 일반적으로 전체 활주로가 다시 배치되므로 초당 60프레임이라는 목표에 훨씬 미치지 못하게 됩니다. 이를 방지하기 위해 Google에서는 레이아웃의 부담을 직접 감당하고 변환을 사용하여 절대 위치로 배치된 요소를 사용합니다. 이렇게 하면 실제로는 빈 공간만 있는데도 활주로의 모든 요소가 여전히 공간을 차지하는 것처럼 할 수 있습니다. 레이아웃을 직접 실행하므로 각 항목이 끝나는 위치를 캐시할 수 있으며 사용자가 뒤로 스크롤할 때 캐시에서 올바른 요소를 즉시 로드할 수 있습니다.
이상적으로는 항목이 DOM에 연결될 때 한 번만 다시 칠해지고 트랙의 다른 항목이 추가되거나 삭제되어도 영향을 받지 않아야 합니다. 최신 브라우저에서만 가능합니다.
최첨단 조정
최근 Chrome에 CSS Containment 지원이 추가되었습니다. 이 기능을 사용하면 개발자가 브라우저에 요소가 레이아웃 및 페인트 작업의 경계임을 알릴 수 있습니다. 여기서는 레이아웃을 직접 수행하므로 포함에 적합한 애플리케이션입니다. 요소를 트랙에 추가할 때마다 다른 항목이 레이아웃 변경의 영향을 받지 않아도 된다는 것을 알 수 있습니다. 따라서 각 항목은 contain: layout
를 가져와야 합니다. 또한 웹사이트의 나머지 부분에 영향을 미치지 않도록 활주로 자체에도 이 스타일 지시어가 적용되어야 합니다.
또 다른 고려사항은 사용자가 요소를 재활용하고 새 데이터를 로드할 수 있을 만큼 충분히 스크롤한 시점을 감지하는 메커니즘으로 IntersectionObservers
을 사용하는 것입니다. 하지만 IntersectionObserver는 지연 시간이 긴 것으로 지정되어 있으므로 (requestIdleCallback
사용과 같음) IntersectionObserver를 사용하면 사용하지 않을 때보다 응답성이 떨어지는 것으로 느껴질 수 있습니다. 스크롤 이벤트가 '최대한 노력'을 기반으로 디스패치되므로 scroll
이벤트를 사용하는 현재 구현에서도 이 문제가 발생합니다. 결국 Houdini의 컴포지터 워크릿이 이 문제의 충실도 높은 솔루션이 될 것입니다.
아직 완벽하지는 않습니다.
현재 DOM 재활용 구현은 실제로 화면에 표시되는 요소만 고려하는 대신 뷰포트를 통과하는 모든 요소를 추가하므로 이상적이지 않습니다. 즉, 매우 빠르게 스크롤하면 Chrome에서 레이아웃과 페인트에 너무 많은 작업을 수행하여 따라갈 수 없습니다. 배경만 표시됩니다. 심각한 문제는 아니지만 개선해야 할 부분입니다.
우수한 사용자 환경과 높은 성능 표준을 결합하려고 할 때 간단한 문제가 얼마나 어려워질 수 있는지 알 수 있기를 바랍니다. 프로그레시브 웹 앱이 휴대폰의 핵심 환경이 되면서 이 점이 더욱 중요해지고 웹 개발자는 성능 제약 조건을 준수하는 패턴을 사용하는 데 계속 투자해야 합니다.
모든 코드는 저장소에서 확인할 수 있습니다. 재사용 가능하도록 최선을 다했지만 npm에 실제 라이브러리로 게시하거나 별도의 저장소로 게시하지는 않습니다. 주요 용도는 교육입니다.