내가 담당한 기능 중 상품 목록(전체, 카테고리 별)를 무한 스크롤로 구현해봤다.
무한 스크롤을 선택한 이유는 두가지이다.
- 이번 프로젝트는 바닐라 스크립트로 SPA처럼 동작하는 것처럼 만들어보고 싶었고 새로고침이 발생하는 pagination보다 자연스럽게 렌더링되는 무한 스크롤이 사용자 경험에서 더 나을 것이라 생각했다.
- 무한 스크롤은 정말 많은 서비스에서 사용되고 있기 때문에 한번 꼭 해보고 싶었다. 실제 서비스에서 어떤 식으로 활용될 지 맛보기로나마 경험해보고 싶었다.
MDN에서는 Intersection Observer을 아래와 같이 정의한다.
타겟 요소와 상위 요소 또는 최상위 document 의 viewport 사이의 intersection 내의 변화를 비동기적으로 관찰하는 방법
또한 MDN은 필요한 상황의 예시를 명시해놨다.
- intersection 감지를 구현하면 영향을 받는 모든 요소를 알기 위해서 Element.getBoundingClientRect()와 같은 메서드를 호출하는 여러 이벤트 핸들러와 루프가 얽혀있었습니다. 모든 코드가 메인 스레드에서 실행되기 때문에, 이 중 하나라도 성능 문제를 일으킬 수 있습니다. 사이트가 이러한 테스트들과 함께 로드되면 상황이 더욱 나빠질 수 있습니다.
내가 MDN 문서로 이해한 바로는 사용자가 웹상에서 이동하는 위치를 계속해서 반환하여 사용자의 intersection을 감지하는 방식(기존의 위치 정보를 활용한 스크롤 이벤트 등)은 문제가 발생하기 쉽고 복잡도가 높기 때문에, 더 나은 방식으로 사용자의 intersection을 감지할 수 있는 api를 제공한다는 의미이다.
일단 공식문서에서 적어놓은 '비동기적으로 관찰한다'라는 것을 기억해둘 필요가 있다.
인터섹션의 작동원리를 간단하게 표현하면 아래와 같다.
- 관찰할 Target을 정한다
- observe를 통해 관찰을 시작한다.
- target이 일정 threshold(가시성, 보여지는 정도)를 넘으면
- IntersectionObserver의 콜백함수가 실행된다.
- 이 과정이 백그라운드에서 비동기적으로 진행된다.
- 관찰 대상은 언제든지 바꿀 수 있으며 취소 또한 가능하다.
더 이상 사용자의 위치 정보를 계속해서 반환할 필요가 없다.
더 이상 불필요한 함수의 호출을 방지하기 위해 디바운싱, 스로틀링을 활용하지 않아도 된다.
이러한 이유로 Intersection Observer가 무한 스크롤 구현에 더 적절한 API라고 나는 생각했다.
- Intersection Observer(콜백, threshold(가시성))를 호출한다.
- 보여지는 마지막 요소를 target으로 정하는 함수를 호출한다.
- 스크롤이 내려가면 Intersection Observer의 콜백이 실행된다.
- 콜백은 insertAdjacentHTML을 통해 html을 브라우저에 추가하고, 기존 관찰 대상을 취소한다. 그리고 다시 2번이 실행되는 함수이다.
// intersectionObserver 콜백함수
const ioCallback = (entries, io) => {
$article = document.querySelectorAll('#itemlist');
// IntersectionObserver의 entries 객체를 구조분해할당하기
const {isIntersecting, target} = entries[0];
if (isIntersecting) { // 감지 상태를 확인함.
// 기존 관찰 대상을 취소를 안한다면 계속해서 콜백함수가 실행됨.(관찰 대상을 최하단의 요소로 바꿔줘야 함)
io.unobserve(target);
setTimeout(() => { // setTimeout은 백그라운드에 순차적으로 비동기 함수들이 실행되게 하기 위해 사용
showContent(4, flag); // insertAdjacentHTML로 html을 띄우는 함수
observeLastItem(io, $article);
}, 700);
}
};
// 관찰 대상을 상품 목록의 마지막 요소로 변경하고 관찰 시작하는 함수
const observeLastItem = (io, items) => {
let lastItem = items[items.length - 1];
io.observe(lastItem); //
};
showContent(8); // 랜딩했을 때 최초 상품 8개 띄우기
const io = new IntersectionObserver(ioCallback, {threshold: 0.9});
// IntersectionObserver 호출
observeLastItem(io, document.querySelectorAll('#itemlist'));