먼저 무한스크롤은 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
를 사용한 무한 스크롤의 장점은 다음과 같습니다.
- 비교적 이해가 쉽습니다. – ‘리스트의 마지막 요소가 화면에 보이면 다음 데이터를 준비한다‘ 라는 비교적 간단한 로직으로 구현 가능합니다.
- throttle, debounce 관련 문제를 따로 처리하지 않아도 됩니다. – 스크롤 이벤트는 뷰포트, 스크롤의 위치 좌표를 계산하는 과정에서 trottle이나 debounce와 관련된 문제가 있으며, 이에 대한 별도의 처리 과정이 필요하나
IntersectionObserver
는 계산 과정이 매우 단축되기 때문에 이러한 처리 과정이 필요 없습니다. - reflow가 많이 줄어듭니다. – 스크롤 이벤트는 좌표 계산 과정에서 DOM의 재 렌더링(reflow)가 유발되지만
IntersectionObserver
는 이러한 문제로부터 자유롭습니다.
IntersectionObserver
를 이용한 무한 스크롤은 대략 다음과 같이 진행됩니다.
- 첫 데이터 리스트는 일반적으로 표시
- 데이터 리스트의 마지막 요소를 관측(
observe
)
– 마지막 요소는 <li> 요소의 배열을 순회해서el.nextSibling
이null
이면 마지막 요소로 판단합니다. - 2번의 마지막 요소가 화면에 10 ~ 100% 들어왔으면 (
threshold 0.1 ~ 1
) 다음 데이터를 로딩 - 기존
observe
되고 있던 요소의 관측을 종료하고 (unobserve
) 마지막 요소를 다시 찾아 관측 - 더 이상 가져올 데이터가 없다면 모든 관측대상을
disconnect
하고 종료하기.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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> |
0개의 댓글