엘리먼트의 크기와 뷰포트의 상대적인 위치 정보를 제공하는 메서드입니다.
반환 값은 padding과 border-width를 포함해 전체 Element가 들어 있는 가장 작은 사각형인 DOMRect 객체입니다.
이미지 출처: MDN
예시 화면에는 출력되어 지는 <li> 요소들이 있고 스크롤을 마지막 요소까지 내리게 되면 다음 <li> 요소들을 보여지게 됩니다.
구현 아이디어는 getBoundingClientRect() 메서드를 통해 마지막 <li> 요소가 뷰포트에 들어가 있는지 없는지 판단합니다.
마지막 <li> 요소가 뷰포트에 들어가 있다면 다음에 출력할 <li> 요소들을 더해 렌더링합니다.
isElementInViewport(element) {
// el의 출력값은 각각의 <li class="item"> 입니다.
const rect = element.getBoundingClientRect();
//reat의 출력값 예시는 DOMRect {x: 35.5, y: 280.5, width: 292.5, height: 442.6875, top: 280.5, …}
// bottom: 723.1875
// height: 442.6875
// left: 35.5
// right: 328
// top: 280.5
// width: 292.5
// x: 35.5
// y: 280.5
// [[Prototype]]: DOMRect 입니다.
return (
rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <=
(window.innerHeight || document.documentElement.clientHeight) &&
rect.right <= (window.innerWidth || document.documentElement.clientWidth)
);
// element가 뷰포트 범위에 있다면 true를 반환합니다.
}
applyEventToElement = (items) => {
document.addEventListener("scroll", () => {
// 스크롤을 하면 li 요소들에 이벤트를 실행합니다.
items.forEach((element, index) => {
if (this.isElementInViewport(element) && items.length - 1 === index) {
// 만약 마지막 li 요소가 뷰포트 범위에 있다면 조건문이 실행됩니다.
this.onNextPage();
// onNextPage()는 다음 페이지를 불러오도록 구현한 함수입니다.
}
});
});
};
render() {
// this.data에는 li 요소로 뿌릴 데이터들(url, name)이 들어있습니다.
this.$searchResult.innerHTML = this.data
.map(
(cat) => `
<li class="item">
<img src=${cat.url} alt=${cat.name} />
</li>`
).join("");
// 화면에 출력될 li 요소들입니다.
let listItems = this.$searchResult.querySelectorAll(".item");
this.applyEventToElement(listItems);
// li 요소들을 listItems변수에 담아 applyEventToElement로 보냅니다.
}
위의 방법으로 스크롤을 구현하게 된다면 스크롤을 할때마다 모든 <li> 요소에 이벤트가 발생합니다.
또한 스크롤 이벤트는 화면을 새로 그리는 리플로우를 발생시킵니다.
이러한 요소들은 퍼포먼스를 저해할 수 있고 이는 사용자의 경험에 좋지 않아질 우려가 생깁니다.
주시하는 요소가 뷰포트에 포함되는지 포함되지 않는지, 더 쉽게는 사용자 화면에 지금 보이는 요소인지 아닌지를 구별하는 기능을 제공합니다.
// IntersectionObserver 객체를 생성합니다.
let intersectionObserver = new IntersectionObserver(callback, options);
// 주어진 대상 요소를 주시합니다.
intersectionObserver.observe(document.querySelector('.대상 요소'));
각각의 <li> 요소들을 observe() 를 사용하여 주시합니다.
주시된 <li> 요소들이 화면에 보인다면 IntersectionObserverEntry.isIntersecting 을 사용하여 조건문을 실행합니다.
마지막 <li> 요소에 부여된 인덱스와 서버에서 받아온 데이터의 길이를 비교하여 다음 페이지를 로드합니다.
// IntersectionObserver 객체를 생성합니다.
listObserver = new IntersectionObserver((items, observer) => {
// items를 출력해보면 감시하고 있는 IntersectionObserver 객체들이 반환됩니다.
// [IntersectionObserverEntry, IntersectionObserverEntry, IntersectionObserverEntry, ...]
// 모두 똑같은 이름의 객체이지만 객체가 주시하고 있는 요소는 다릅니다.
items.forEach((item) => {
// 아이템이 화면에 보일 때 조건문 실행합니다.
// (IntersectionObserver 객체 안에 isIntersecting은 화면에 보인다면 true
// , 보이지 않는다면 false를 반환합니다.)
if (item.isIntersecting) {
item.target.querySelector("img").src =
item.target.querySelector("img").dataset.src;
// 마지막 요소를 찾아냅니다.
// (target은 해당 li요소, 'data-어쩌구'로 지정된 요소는 'dataset.어쩌구' 불러올 수 있음)
let dataIndex = Number(item.target.dataset.index);
// 서버에서 받아온 data의 길이와 현재 주시중인 요소에 부여된 인덱스 + 1(마지막 인덱스)이 같다면 조건문을 실행합니다.
if (dataIndex + 1 === this.data.length) {
// onNextPage()는 다음 페이지를 불러오도록 구현한 함수입니다.
this.onNextPage();
}
}
});
});
render() {
// 각각의 요소들에 IntersectionObserver에 사용할 인덱스를 부여해줍니다.
this.$searchResult.innerHTML = this.data
.map(
(cat, index) => `
<li class="item" data-index=${index} >
<img src="https://via.placeholder.com/200x300"
data-src=${cat.url}
alt=${cat.name}
/>
</li>
`
)
.join("");
this.$searchResult.querySelectorAll(".item").forEach(($item, index) => {
$item.addEventListener("click", () => {
this.onClick(this.data[index]);
});
// forEach로 각각의 li 요소들을 주시 시작합니다.
this.listObserver.observe($item);
});
}
매번 전체 요소에 이벤트를 발생시키지 않기 때문에 첫 번째 방법보다는 나은 방법이라고 알게 되었습니다.
두 번째 방법보다 좋은 방법을 공부하게 된다면 게시글에 추가하겠습니다.
추가로 Intersection Observer에 관한 더 자세한 내용은 HEROPY TECH님의 블로그에 가독성 좋게 설명되어 있으니 참고해주세요.