본문으로 바로가기

IntersectionObserve API, 무한 스크롤

category JavaScript 2021. 5. 15. 13:32

원래 이전에 레이지 로딩에 이어서 IntersectionObserve 두 번째 시간

별 다른 검색 없이 쉽게 구현할 수 있어서 쪼금 김이 빠졌다 아마 그런만큼 헛점이 많을 줄 알았으나

구현 퀄리티를 제외하고 코드의 흐름에는 크게 차이가 없는 것 같아서 뿌듯했다

상황은 데이터를 검색 후 200개의 아이템을 응답으로 받은 상황을 가정

구현하려고 하는 바는 다음과 같다

아이템 7개를 먼저 화면에 보여주고

스크롤이 마지막에 닿으면 다음 7개를 밑에 붙여서 추가로 그린다

200번째 아이템까지 반복

매우 간단.

초기 코드

<li class="container">

</li>
.container{
  list-style:none;
}
.item{
    width: 100px;
    height: 100px;
    background: black;
    margin: 10px;
    list-style: none;
    color: whitesmoke;
    font-size: 2rem;
    text-align: center;
}
const $list = document.querySelector('.container');
const itemArr = new Array(200).fill().map((_, index) => index+1);

render();

function render(){
  $list.innerHTML = itemArr.map(item=>`<div class="item">${item}</div>`).join('');
}

위에서 말했듯이 이 js는 데이터를 받아서 ul안에 뿌려주는 작업을 하는 상황으로 가정하자

데이터를 받는 부분은 생략.. 이미 itemArr을 받아왔다 MVC모델에 의해 이 js는 알 필요가 없다ㅋㅋ

데이터 자르기

우선 7개씩 잘라서 보여주는 것이 요구사항이다

slice 메서드를 이용해서 7개를 잘라보자

$list.innerHTML = itemArr.slice(0,7).map(item=>${item}).join('');

이렇게 하면 화면에 7개의 아이템만 나올 것인데 문제는 다음이다

IntersectionObserve객체로 마지막 아이템을 만난 후에 다시 7개의 아이템을 렌더링 함수로 호출해야하니까

lastIndex라는 변수를 만들어서 동적으로 범위를 증가시키자

그리고 innerHTML에 그냥 넣게된다면 이전의 7개는 초기화된다 그것은 우리가 바라는 사항이 아니기 때문에 +로 누적해서 문자열이 더해지게 수정하자

let lastIndex = 0;
...

function render(){
  $list.innerHTML =+ itemArr.slice(lastIndex,lastIndex+7).map(item=>`<div class="item">${item}</div>`).join('');
  lastIndex += 7;
}

이제 render함수를 호출하기만 하면 7개씩 아이템들이 화면에 추가되게된다

IntersectionObserve

다음으로 할 것은 IntersectionObserve으로 아이템들을 구독하는 것 우선 객체를 생성해주자

    const io = new IntersectionObserver(((entries, observer) => {
        entries.forEach(entry=>{
            if(entry.isIntersecting){
              //something
            }
        });
    }));

something에 구현할 코드는 '현재 화면에 들어온 entry가 마지막이라면 render함수를 호출한다'인데

마지막 아이템인지 식별할 수 있는 방법은 전역 변수로 지정한 lastIndex를 이용하면 될 것 같고.. item들에게도 인덱스를 주어서 그 값을 비교해하면 되겠다

$list.innerHTML += itemArr.slice(lastIndex,lastIndex+7).map((item,index)=>${item}).join('');

data-set을 이용해서 인덱스값을 줬고 렌더링마다 itemArr.map()메서드의 인덱스는 0부터 시작하기 때문에 전역변수 lastIndex의 도움을 받아서 인덱싱해줬다

    const io = new IntersectionObserver(((entries, observer) => {
        entries.forEach(entry=>{
            if(entry.isIntersecting){
                if(Number(entry.target.dataset.index)===lastIndex){
                    render();
                }
                observer.unobserve(entry.target);
                console.log(entry.target,'is unobserve');
            }
        })
    }));

여기서 잠시 헤맸는데 data-set에 들어있는 값은 문자열이라 lastIndex와 그냥 비교했더니 제대로된 결과를 얻을 수 없었다.. Number을 이용해서 비교했다

이제 만든 객체로 아이템들을 관찰해주면 끝이다

function render(){
          $list.innerHTML += itemArr.slice(lastIndex,lastIndex+7).map((item,index)=>`<div class="item" data-index="${index+1+lastIndex}">${item}</div>`).join('');
        lastIndex+=7;

          [...document.querySelectorAll(`.item`)].forEach(elem=> {
            io.observe(elem);
        });

    console.log(lastIndex,'~',lastIndex+7,'items render');
}

렌더링 함수가 호출되고 item들이 만들어진 후에 아이템들을 긁어와서 io가 관찰하게끔 했다

이제 무한 스크롤 구현이 끝났는데 마음에 안드는 부분이 남아있어서 몇가지 더 해봤다

리팩토링?

새로운 아이템들을 렌더링할 때마다 이전에 모든 아이템들을 다시 가져와서 io로 관찰시켜주고있다는 점이 굉장히 마음에 안들었다

고민을 좀 하다가 한 번 렌더링할 때마다 페이지 넘버 class를 동적으로 부여한 뒤에 새롭게 추가된 페이지들만 불러오는 것

    let [lastIndex, page] = [0,1];
    ...
    function render(){
        $list.innerHTML += itemArr.slice(lastIndex,lastIndex+7).map((item,index)=>`<div class="item page${page}" data-index="${index+1+lastIndex}">${item}</div>`).join('');
        lastIndex+=7;

        [...document.querySelectorAll(`.page${page}`)].forEach(elem=> {
            if(Number(elem.dataset.index)===lastIndex) io.observe(elem);
        });
        page+=1;
    }

하다보니 모든 아이템을 관찰해 줄 필요도 없는 것 같아서 마지막 아이템에만 관찰해주는 것으로 수정

See the Pen dyvpMbQ by unganam (@unganam) on CodePen.