방망이 깎는 코모
무한스크롤 (Infinite Scroll) 본문

기존 뉴스 피드, 이미지 갤러리, 소셜 미디어등의 플랫폼에서는 대량의 데이터를 제공할 때, 사용자가 화면 하단의 페이지 번호를 클릭하여 새로운 데이터를 불러오는, 전통적인 페이지네이션 방식을 사용했었다. 이러한 페이지네이션 방식은 ①버튼 클릭이라는 추가적인 상호작용이 필요하고, ②새로운 페이지 로딩으로 인해 사용자 경험이 끊기거나 ③모바일 환경의 경우 페이지 번호가 작아서 클릭이 어렵다는 점등의 단점이 존재한다.
무한스크롤 개념
최근에는 스마트폰을 포함한 모든 터치 스크린에서 더 편리하게 동작하는 무한스크롤(Infinite Scroll) 방식으로 구현하는 추세이다. 무한스크롤 기능은 웹 페이지나 앱에서 사용자가 스크롤할 때마다 새로운 데이터를 계속해서 불러오는 기능을 말한다.
무한스크롤의 장점
- 사용자는 로딩 시간이 빠르게 느껴진다.
- 높은 중독성으로 사용자의 페이지 참여 시간이 길어진다.
- 터치 스크린에서 보다 편리하게 다음 페이지 이동이 가능하다등
무한스크롤의 단점
- 페이지를 새로고침 할 경우, 사용자는 자신이 머물렀던 위치를 알 수 없다.
- 특정 데이터나 이전에 검색한 데이터에 재접근이 어렵다.
- 페이지가 동적으로 로드되면서 검색엔진이 모든 컨텐츠를 인덱싱하지 못할 수 있다등
최근의 디지털 플랫폼, 특히 숏폼 컨텐츠가 주를 이루는 소셜 미디어와 뉴스 피드에서는 무한스크롤 방식이 널리 채택되고 있다. 사용자가 별도의 페이지 이동 없이 연속적으로 데이터를 탐색 가능하므로 소비자에게 많은 양의 정보를 빠르게 전달하기에 가장 효과적인 방식이기 때문이다. 예를 들면, 이미지 위주의 '인스타그램'이나 'Pinterest'와 같은 플랫폼에서는 스크롤을 할 수록 사용자의 관심사와 관련된 컨텐츠가 끝없이 로드된다.
'더보기(Load More)' 버튼 기능도 크게는 무한스크롤 방식에 포함된다.
무한스크롤 구현 I : 스크롤 위치 기준 (document.documentEement 활용)
document.documentElement 란 HTML 문서의 루트 요소, 즉 <html> 태그를 참조하는 DOM객체로써, 이 객체를 이용하여 스크립트를 통해 전체 HTML 문서의 속성에 접근하고 수정할 수 있다.

document.documentElement
객체를 사용하는 무한스크롤 기능 구현은 주로 전체 문서의 스크롤 위치와 높이를 기반으로 한다. 이 방식에서는 사용자가 페이지 하단에 도달할 때 마다 새로운 컨텐츠를 로드하는 로직이 포함된다. 즉, 사용자의 스크롤 위치(scrollTop
)와 뷰포트의 높이(innerHeight
)가 문서의 전체 높이(scrollHeight
)에 근접하면 추가 데이터를 로드하는 이벤트를 트리거한다.
window.addEventListener('scroll', () => {
const documentHeight = document.documentElement.scrollHeight;
const scrollTop = document.documentElement.scrollTop;
const viewportHeight = window.innerHeight;
// 사용자가 페이지 하단에 도달했는지 확인
if (scrollTop + viewportHeight >= documentHeight) {
console.log('스크롤이 하단에 닿았으니 데이터를 불러옵니다!');
// 여기에 데이터를 로드하는 함수를 호출
}
});
⚠️ clientHeight와 innerHeight의 차이
document.documentElement.clientHeight | window.innerHeight | |
정의 | <html> 요소의 높이 | 브라우저 윈도우의 뷰포트 높이 |
브라우저 UI (수평 스크롤바 포함) |
브라우저 UI를 제외한 순수한 컨텐츠 영역의 높이 |
모든 브라우저 UI 포함하는 높이 |
clientHeight 값을 사용한다면, 브라우저 UI나 수평 스크롤바등이 고려되지 않아 정확한 계산이 어렵다. 따라서 사용자의 실제 뷰포트 크기에 기반하여 페이지 하단에 도달했는지 확인하려면 innerHeight를 사용하는 것이 보다 정확하고 신뢰할 수 있다. 이는 다양한 화면크기와 레이아웃에서 일관된 UX를 제공하기 위해서도 더 나은 선택이다.
위와 같이 '스크롤 위치'를 기준으로 작동하는 무한스크롤은 문법이 우리에게 익숙하고, 구현하기 쉽다는 장점이 있는 반면 여러가지 성능 문제를 일으킨다.
- 높은 리소스 사용 : 브라우저가 스크롤 이벤트를 매우 빈번하게 처리해야함으로써 성능 저하 발생
- 리플로우/리페인트 : 잦은 페이지 레이아웃 변경으로 성능 저하 발생
- 수동 계산 요구 : 정확한 스크롤바 위치와 문서 높이를 계산하기 위해 많은 수학적 계산과 상태관리가 요구되며, 다양한 화면 크기와 레이아웃에 대응하기 위해 추가적인 로직이 필요
- 이벤트 처리 최적화 : 빈번한 스크롤 이벤트 처리를 최적화하기 위해 디바운스/스로틀 기법이 필요
- 비일관적인 UX : 스크롤 기준 방식은 사용자의 환경에 따라 다르게 작동 가능 등
따라서 현재는 뷰포트 기준으로 작동하는 intersection Observer를 통한 무한스크롤 방식이 많이 쓰인다.
무한스크롤 구현 II : 뷰포트 기준 (Intersection Observer API 활용)
WEB API 중 Intersection Observer API는 대상 요소가 뷰포트에 들어오거나 나갈 때 콜백 함수를 실행하는 기능을 구현한다. 이를 통해 구현한 무한스크롤은 스크롤 위치 기준 방식에 비해 다양한 이점을 갖고 있다.
- 성능 최적화 : 스크롤 위치 기반 방식은 스크롤 이벤트에 의존하기 때문에 DOM접근 및 계산이 빈번하게 발생한다. 반면 Intersection Observer의 경우, 브라우저의 내장 기능을 활용하여 뷰포트와 감시 대상 요소의 교차를 비동기적으로 감지하기 때문에 메인 스레드의 작업을 방해하지 않음
- 복잡한 계산 불필요 : 스크롤 위치 기반 방식은 정확한 위치를 구하기 위해 복잡한 수학적 계산이 필요하지만, Intersection Observer는 교차 상태만을 감지하므로 불필요한 계산이나 리소스 사용을 최소화
- 일관된 경험 : 스크롤 위치 기반 방식은 다양한 기기와 브라우저 종류에 따라 일관성을 유지하기 어렵지만, Intersection Observer는 뷰포트 내에 특정 요소가 들어오는/나가는 순간을 정확하게 감지하기 때문에 일관된 방식으로 작동
IntersectionObserver 기본 구조
const observer = new IntersectionObserver(observerCallback, {
root: null,
rootMargin: "0px",
threshold: 1.0,
});
새로운 Intersection Observer 객체를 생성하면서, 추후 호출될 '콜백함수'와 '옵션' 객체를 전달한다. 옵션 객체는 앞으로 생성할 옵저버의 작동 방식을 세부적으로 조정할 수 있다.
root
: 루트 요소 지정null
또는 명시하지 않을 경우 : 브라우저의 뷰포트를 root로 설정 (기본값)- 명시할 경우 : 해당 HTML요소가 root로 설정 (예:
document.querySelector("#scrollArea")
)
rootMargin
: root 요소의 마진을 지정threshold
: 교차 상태가 되기 위해 감시 대상인 target이 root에 얼마나 교차되어야 하는지를 결정하며, 값이 배열일 경우 여러 교차 지점을 지정 가능
(예) 값이 0.5일 경우, target이 50%가 교차되면 교차 상태로 간주하는 것을 의미
(예) 값이 1.0일 경우, target의 100%가 root와 교차해야 교차상태로 간주하는 것을 의미
Intersection Observer 생성 및 감시 대상 연결
// 새로운 데이터를 로드하는 함수
function loadMoreData() {
console.log('새로운 데이터 로드');
// 여기에 데이터를 로드하는 로직을 구현
}
// Intersection Observer 설정
let observer = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
loadMoreData();
}
});
}, { threshold: 1.0 });
// 타겟 요소를 감시
let target = document.querySelector('#target');
observer.observe(target);
entries
:IntersectionObserverEntry
객체의 배열로, 감시되고 있는 각 요소에 대한 정보가 담긴다.entry
:IntersectionObserverEntry
객체로, 감시 대상 요소에 대한 정보를 가진다.isIntersecting
: 감시 대상 요소가 root와 교차하는지 여부를 나타내는Boolean
반환 (즉, 뷰포트안에 들어왔는지)target
: 교차 상태를 감시하고 있는 대상 요소intersectionRatio
: 대상 요소가 교차 영역 안에 얼마나 들어와 있는지를 나타내는 비율
마치며
'무한스크롤'을 구현하기 위해 공부한 내용을 글로 정리하면서 clientHeight, innerHeight, scrollHeight등 각각 다른 기준의 '높이'들의 차이점에 대해 알아볼 수 있었다. 이론적으로만 알고 있었던 '옵저버'를 코드로 직접 구현해보는 것은 매우 흥미로운 경험이었다. 특히 복잡한 수학적 계산이 요구되었던 '교차 순간'을 isIntersecting을 통해 '되냐 안되냐'의 boolean값으로 바로 알 수 있다는 것은 허탈할 만큼 간단했다. 이 옵저버 야무지게 써먹어야겠다.
참고 문헌
1. INTERACTION DESIGN FOUNDATION, PAGINATION : https://www.interaction-design.org/literature/topics/pagination
2. Blog Driven Development, 실무에서 느낀 점을 곁들인 Intersection Observer API 정리 : https://velog.io/@elrion018/%EC%8B%A4%EB%AC%B4%EC%97%90%EC%84%9C-%EB%8A%90%EB%82%80-%EC%A0%90%EC%9D%84-%EA%B3%81%EB%93%A4%EC%9D%B8-Intersection-Observer-API-%EC%A0%95%EB%A6%AC
3. MDN, Intersection Observer API : https://developer.mozilla.org/ko/docs/Web/API/Intersection_Observer_API
'개념정리' 카테고리의 다른 글
리액트로 드래그 앤 드롭 구현하는 방법 (1) | 2024.02.05 |
---|---|
RESTful API 정의 및 사용 방법 (0) | 2024.01.10 |
HTTP와 HTTPS 차이점 및 HTTPS 적용 방법 (1) | 2024.01.09 |
브라우저 저장소의 종류와 특징 : localStorage, sessionStorage, IndexedDB (0) | 2024.01.05 |
자바스크립트 Class를 활용한 모듈화 기법 (0) | 2024.01.04 |