먼저 무한스크롤은 JQuery에서도 다양한 플러그인이 있고, 그 외에도 검색하면 많은 라이브러리들이 있기 때문에 무한스크롤을 직접 구현하기보다는 이러한 외부 라이브러리를 사용하는 것을 우선 추천드립니다.

 

IntersectionObserver에 대한 내용은 이전 글에서 설명되어 있습니다.

 

IntersectionObserver을 이용한 무한 스크롤은 MDN 문서에서도 공식적으로 권장하고 있는 사항입니다.

Implementing “infinite scrolling” web sites, where more and more content is loaded and rendered as you scroll, so that the user doesn’t have to flip through pages.

 

예전에 무한 스크롤을 구현한다고 하면 뷰포트의 높이와 document, window의 높이, 스크롤의 현재 위치의 차이 등을 계산하여 무한 스크롤 여부를 정하는 스크롤 이벤트를 이용한 방법이 많이 사용되었습니다. 이 방법은 개인적으로 매우 이해하기 어렵고 비직관적이라고 생각합니다.

이러한 기존의 스크롤 이벤트를 이용한 무한 스크롤 대비 IntersectionObserver를 사용한 무한 스크롤의 장점은 다음과 같습니다.

  1. 비교적 이해가 쉽습니다. – ‘리스트의 마지막 요소가 화면에 보이면 다음 데이터를 준비한다‘ 라는 비교적 간단한 로직으로 구현 가능합니다.
  2. throttle, debounce 관련 문제를 따로 처리하지 않아도 됩니다. – 스크롤 이벤트는 뷰포트, 스크롤의 위치 좌표를 계산하는 과정에서 trottle이나 debounce와 관련된 문제가 있으며, 이에 대한 별도의 처리 과정이 필요하나 IntersectionObserver는 계산 과정이 매우 단축되기 때문에 이러한 처리 과정이 필요 없습니다.
  3. reflow가 많이 줄어듭니다. – 스크롤 이벤트는 좌표 계산 과정에서 DOM의 재 렌더링(reflow)가 유발되지만 IntersectionObserver는 이러한 문제로부터 자유롭습니다.

 

IntersectionObserver를 이용한 무한 스크롤은 대략 다음과 같이 진행됩니다.

  1. 첫 데이터 리스트는 일반적으로 표시
  2. 데이터 리스트의 마지막 요소를 관측(observe)
    – 마지막 요소는 <li> 요소의 배열을 순회해서 el.nextSiblingnull 이면 마지막 요소로 판단합니다.
  3. 2번의 마지막 요소가 화면에 10 ~ 100% 들어왔으면 (threshold 0.1 ~ 1) 다음 데이터를 로딩
  4. 기존 observe 되고 있던 요소의 관측을 종료하고 (unobserve)  마지막 요소를 다시 찾아 관측
  5. 더 이상 가져올 데이터가 없다면 모든 관측대상을 disconnect 하고 종료하기.

 


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>IntersecionObserver: infinite scroll example</title>
<style>
body {
padding: 30px;
background-color: gray;
}
#container {
margin: 0px auto;
width: 500px;
}
#list {
padding: 0px;
}
#list li {
height: 250px;
line-height: 250px;
display: block;
background-color: aliceblue;
margin-bottom: 15px;
text-align: center;
vertical-align: middle;
font-size: 3em;
}
#msg-loading {
background-color: honeydew;
color: red;
text-align: center;
border-radius: 5px;
}
.fade-in {
opacity: 1;
animation-name: fadeInOpacity;
animation-iteration-count: 1;
animation-timing-function: ease-in;
animation-duration: 0.5s;
}
.fade-out {
opacity: 0;
animation-name: fadeOutOpacity;
animation-iteration-count: 1;
animation-timing-function: ease-in;
animation-duration: 0.5s;
}
@keyframes fadeInOpacity {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
@keyframes fadeOutOpacity {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
</style>
</head>
<body>
<div id="container">
<ul id="list">
</ul>
<p id="msg-loading">……. 로딩중 …….</p>
</div>
<script>
let currentPage = 1
const DATA_PER_PAGE = 10,
lastPage = 5
const msgLoading = document.getElementById("msg-loading")
// 데이터 추가 함수
function addData(currentPage) {
const $list = document.getElementById("list")
for (let i = (currentPage – 1) * DATA_PER_PAGE + 1; i <= currentPage * DATA_PER_PAGE; i++) {
const $li = document.createElement("li")
$li.textContent = `${currentPage}페이지 : ${i}번째 데이터`
$li.classList.add("fade-in")
$list.appendChild($li)
}
}
// IntersectionObserver 갱신 함수
function observeLastChild(intersectionObserver) {
const listChildren = document.querySelectorAll("#list li")
listChildren.forEach(el => {
if (!el.nextSibling && currentPage < lastPage) {
intersectionObserver.observe(el) // el에 대하여 관측 시작
} else if (currentPage >= lastPage) {
intersectionObserver.disconnect()
msgLoading.textContent = "페이지의 끝입니다."
}
})
}
// IntersectionObeserver 부분
const observerOption = {
root: null,
rootMargin: "0px 0px 0px 0px",
threshold: 0.5
}
// IntersectionObserver 인스턴스 생성
const io = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
// entry.isIntersecting: 특정 요소가 뷰포트와 50%(threshold 0.5) 교차되었으면
if (entry.isIntersecting) {
msgLoading.classList.add("fade-in")
// 다음 데이터 가져오기: 자연스러운 연출을 위해 setTimeout 사용
setTimeout(() => {
addData(++currentPage)
observer.unobserve(entry.target)
observeLastChild(observer)
msgLoading.classList.remove("fade-in")
}, 1000)
}
})
}, observerOption)
// 초기 데이터 생성
addData(currentPage)
observeLastChild(io)
</script>
</body></html>

 

 

문의 | 코멘트 또는 yoonbumtae@gmail.com


카테고리: WEB: Frontend


0개의 댓글

답글 남기기

Avatar placeholder

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다