Front-end/깊게 파고들기

javascript . 어떻게 무한 스크롤을 구현하시나요?

아지송아지 2022. 4. 10. 12:01

안녕하세요.

 

여러분은 무한 스크롤을 지금까지 어떤 식으로 구현하셨나요?

자바스크립트로는 다양한 방법으로 구현할 수 있습니다.

직접 구현하는 사람도 있고 라이브러리를 사용하는 사람도 있습니다.

 

실제로 npm에 infinite-scroll을 검색하면 굉장히 많은 라이브러리들이 있습니다.

44페이지까지 있네요.

 

 

 

처음 제작하신다면 라이브러리보다는 직접 구현하는 방법을 추천드립니다.

 

이번 글에는 세 가지 무한 스크롤을 구현하는 방법이 나와있습니다.

1. 익숙한 방법

2. debouncing, throttling

3. Intersection Observer API

 

 

 

1. 익숙한 방법

지금까지 저는 아래 코드와 같이 구현했었습니다.

window.addEventListener("scroll", infiniteScroll);

let isUpdateList = true;    

function infiniteScroll(){
    const currentScroll = window.scrollY;
    const windowHeight = window.innerHeight;
    const bodyHeight = document.body.clientHeight;
    const paddingBottom = 200;
    if(currentScroll + windowHeight + paddingBottom >= bodyHeight){
        if(isUpdateList){
            isUpdateList = false;
            
            -- after fetch API --
            isUpdateList = true;
        }
    }
}

window에 스크롤 이벤트를 넣고 원하는 값들을 계속 가져왔습니다.

스크롤이 하단에 다다르면 코드를 실행시켰고 이벤트 중복을 방지하기 위하여 isUpdateList로 통제하였습니다.

 

이 방법에는 문제점이 있습니다.

1. 스크롤을 할 때마다 이벤트가 발생됩니다.

2. 1번에 따른 reflow 단계가 계속 일어납니다.

3. 스크롤 이벤트가 무거워질수록 위험합니다.

최적화가 필요합니다.

 

 

 

2. debouncing? throttling?

먼저 디바운싱과 스로틀링에 대해 간단히 설명드리겠습니다.

debouncing - 연속해서 호출되는 함수들 중 제일 마지막 혹은 처음 함수만 실행되는 것

throttling - 마지막 함수가 호출되면 일정 시간 동안은 다시 호출되지 않는 것

 

프로그래밍 기법 중 하나입니다.

debouncing은 주로 input에서 사용되고, throttling은 스크롤 이벤트에 자주 사용됩니다.

throttling을 사용하여 무한 스크롤을 구현해보겠습니다.

window.addEventListener("scroll",infiniteScroll);

let timer = null;

function infiniteScroll(){
    const currentScroll = window.scrollY;
    const windowHeight = window.innerHeight;
    const bodyHeight = document.body.clientHeight;
    const paddingBottom = 200;
    
    if(currentScroll + windowHeight + paddingBottom >= bodyHeight){
        if (!timer) {
            timer = setTimeout(() => {
                timer = null;
                -- fetch API -- 
            }, 200);
        }
    }
}

하단 영역에 다다르면 timer에 setTimeout을 설정합니다.

timer는 200ms 뒤에 null값이 되지만 그동안 실행이 되지 않습니다.

즉, 이벤트가 실행되는 정도를 정합니다.

 

이 방법에도 문제점이 있습니다.

1. 스크롤을 할 때마다 이벤트가 발생됩니다.

2. setTimeout은 기대한 대로 작동하지 않을 수 있습니다.

  - setTimeout은 Task Queue에 들어가는데, Queue에  저장된 비동기 task를 처리하는 시점에서 Call stack이 비어져있지 않을 경우 기대했던 동작이 일어나지 않을 수 있습니다.

 

 

* window.addEventListener("scroll", callback) - passive 최적화

passive 속성으로 스크롤 이벤트를 성능 향상을 해보겠습니다.

window.addEventListener("scroll", callback, {passive:true})

이벤트를 조작하다 보면 event.preventDefault()를 사용할 때가 있습니다.

브라우저는 기본적으로 preventDefault()를 호출하는지 호출하지 않는지 감시합니다.

 

passive 속성이 true일 경우 preventDefault()를 호출하지 않을 것을 나타냅니다.

만약 호출하는 경우에도 콘솔에 경고를 출력하는 것 외에 아무런 동작도 하지 않습니다.

 

기본값은 false이며 touchstart, touchmove 이벤트에 대해서는 기본값이 true입니다.

자세한 내용은 MDN문서를 참고해주세요.

 

 

3. Intersection Observer API

마지막 방법입니다.

addEventListener가 중복해서 쌓이고 복잡한 로직이 섞여 있으면 성능 이슈가 일어납니다.

Intersection Observer API는 타겟의 변화를 관찰하며 들어가거나 나갈 때 또는 두 요소의 교차 부분만큼 변경될 때 콜백이 실행됩니다.

쉽게 말해 두 요소의 상태를 감지하여 콜백 함수를 실행합니다.

 

요소의 교차를 계속 지켜보기 위해 메인 스레드를 사용할 필요가 없어지고 교차 영역 관리를 최적화합니다.

 

이는 여러 기능에서 사용됩니다.

1. 이미지나 컨텐츠의 지연 로딩 (lazy loading)

2. 무한 스크롤

3. 광고 수익 계산을 위한 광고의 가시성 보고

4. 사용자에게 결과가 표시되는 여부에 따라 작업이나 애니메이션을 수행할지 여부를 경정

 

사용법

const target = document.querySelector("#target");
const option = {
    root: null,
    rootMargin: "0px 0px 0px 0px",
    thredhold: 0,
}

const observer = new IntersectionObserver(callback, option);
observer.observe(target);

new IntersectionObserver()는 두 개의 인자를 받습니다.

 

callback

콜백 함수는 요소가 한 방향 혹은 다른 방향으로 교차할 때 실행됩니다.

 

option

 * root

타겟과의 어떤 요소를 교차할지 정합니다.

기본값은 null이며 브라우저 뷰포트입니다. 엘리먼트로 설정하면 해당 엘리먼트와 타켓을 감시합니다.

 

* rootMargin

root의 범위를 정합니다.

css margin과 속성이 유사하며 "10px 20px 10px 30px"과 같이 정합니다.

기본값은 0입니다.

 

* thredhold

타겟이 얼마만큼 보였을 때 콜백 함수를 실행할지 정합니다.

0~1의 숫자로 정합니다. ex) 0.5 -> 50%, 0.25 -> 25%

25%마다 실행하고 싶으면 [0, 0.25, 0.5, 0.75, 1]과 같은 배열로 설정합니다.

 

 

예시

// html
<body>
    <ul>
        <li></li>
        <li></li>
        <li></li>
        <li></li>
        <li></li>
    </ul>
    <div id="endList"></div>
</body>

리스트 끝에 <div id="endList">를 만들어 줍니다.

스크립트로 endList를 감지할 예정입니다.

 

// javascript
const listEnd = document.querySelector("#endList");
const option = {
    root: null,
    rootMargin: "0px 0px 0px 0px",
    thredhold: 0,
}

const onIntersect = (entries, observer) => { 
    // entries는 IntersectionObserverEntry 객체의 리스트로 배열 형식을 반환합니다.
    entries.forEach(entry => {
        if(entry.isIntersecting){
            const listWrap = document.querySelector("ul");
            listWrap.insertAdjacentHTML("beforeend", `
                <li></li>
                <li></li>
                <li></li>
                <li></li>
            `)
        }
    });
};

const observer = new IntersectionObserver(onIntersect, option);
observer.observe(listEnd);

endList를 타켓으로 설정한 후 화면에 들어오면 콜백 함수를 실행합니다.

entry.isIntersecting으로 교차가 되었는지를 판단하여 교차가 된 상태면 <li>들을 추가합니다.

entrie는 IntersectionObserverEntry객체를 배열로 반환합니다.

console.log(entries);

 

IntersectionObserver Method에 대해 좀 더 자세히 설명드리겠습니다.

세 가지 메서드가 존재합니다.

IntersectionObserver.observe(target)

타겟을 관찰하기 시작할 때 사용합니다.

 

IntersectionObserver.unobserve(target)

관찰을 멈추고 싶을 때 사용합니다. 하나의 타겟만 관찰을 멈출 수 있습니다.

 

IntersectionObserver.disconnect(target)

관찰하고 있는 다수의 타겟을 모두 멈추고 싶을 때 사용합니다. 

 

위 코드에서는 endList라는 div를 만들어 마지막 스크롤을 감지했습니다.

리스트의 마지막 아이템을 감지할 수 있었지만, 그렇게 되면 unobserve와 observe 등 추가적인 작업이 필요하다고 생각하여 하지 않았습니다.

 

 

 

마치며


지금까지 여러 방법들을 활용하여 무한 스크롤을 구현해 보았습니다.

더 좋은 방법이 있거나 수정/추가해야 할 부분이 있다면 알려주시면 감사하겠습니다.

 

사실 이 글은 지금까지 깨끗하지 못한 방법으로 구현하였던 저를 반성하며 작성하였습니다.

 

자신의 코드에 안주하지 않고 발전하자는 마음은 항상 있었지만, 그렇게 행동하지 못한 것에 부끄러웠습니다.

앞으로 제 코드를 돌아보며 최적화와 더 나은 방법은 무엇이 있는지 의심하며 고민해 봐야겠습니다.

 

감사합니다.

 

 

 

 

참고 : 

https://developer.mozilla.org/ko/docs/Web/API/Intersection_Observer_API

https://jbee.io/web/optimize-scroll-event/

https://developer.mozilla.org/ko/docs/Web/API/EventTarget/addEventListener