📢 어렵고 정석적인 개념 설명보다는 저같은 초보자도 이해하기 쉽게 정리하는 것을 원칙으로 포스팅하고 있습니다. 😄

[JavaScript] Intersection Observer API로 무한 스크롤 구현하기

Intersection Observer란?

Intersection Observer란 브라우저 뷰포트(브라우저 화면에 보이는 것)와 설정한 요소의 교차점을 관찰해서, 그 요소가 뷰포트에 있는지의 여부를 감시하는 비동기적 기능을 말한다.

쉽게 말하면 스타크래프트의 옵저버처럼 내가 감시하고 싶은 유닛을 지정하면 그 유닛이 내 시야에 들어오는 순간을 알려주는 기능이라고 생각하면 된다. (물론 스타크래프트의 옵저버는 따로 감시할 유닛을 지정하지는 않지만..)

 

⚠️ 이 글은 해당 API의 세세한 기능 설명을 담고 있지 않으며, 단순 구현 방법만을 설명하고 있습니다.
해당 포스팅에서는 Intersection Observer API이란 무엇이며, 간단히 구현하는 방법 정도만을 소개하고 있고
해당 API의 다른 메서드 설명은 포함하고 있지 않으니 자세한 정보는 공식 문서를 참고해 주세요!

 

특정 요소가 뷰포트에 들어오면 alert 알림 뜨게 하기

<div id="wrap">
    <section id="point"></section>
</div>

높이가 10,000px인 wrap이 있다고 하자. 그리고 이 wrap의 어딘가에는 point 섹션이 자리 잡고 있다.

내가 원하는 기능은 스크롤을 내리다가 point 섹션이 조금이라도 보이는 순간 alert('point!')을 띄우는 것이다.

스크롤 이벤트 등을 이용해서 구현할 수도 있지만, Intersection Observer API를 이용해서 간단하게 구현해 보자.

 

const point = document.querySelector('.point');
const observer = new IntersectionObserver((entries) => {
    entries.forEach(entry => {
      if (entry.isIntersecting) {
        alert('point!');
      }
    })
})

observer.observe(point); // 감시할 대상 지정

변수 observer에 new IntersectionObserver()를 통해 API를 생성해 준다. 그리고, 콜백함수를 이용해서 entries를 받아오는데 여기서 entries란 옵저버가 감시할 대상들의 배열을 말한다. 위 코드에서는 감시할 대상이 1개이지만, 일반적으로는 감시할 대상이 여러 개인 경우가 많으므로 forEach()를 이용해서 감시할 대상(entry)을 하나씩 가져와준다.

 

if (entry.isIntersecting)에서 isIntersecting이란 우리가 감시하는 요소(entry)가 뷰포트에 교차하는 순간을 의미한다.

즉, 감시 대상이 브라우저 화면에 들어왔냐 여부를 반환해 준다. 감시할 대상은 API를 생성한 변수에 observe 메서드를 이용해서 지정해 준다.

 

위 코드는 뷰포트에 point 섹션이 교차되는 순간 'point!'라는 alert을 뜨게 하는 코드이다.

뷰포트에 찾는 요소가 들어오는 지를 감시하는 Insersection Observer API의 가장 일반적이며 기초적인 기능이다.

 

무한 스크롤 구현 : 스크롤하면 전체 데이터 중에 n개씩 나오게 하기

<div id="list_wrap">
    <ul class="items"></ul>
</div>

약 100,000개의 데이터가 있다고 가정하자. 이 100,000개를 한 페이지에 전부 보여주면 렉이 걸릴 수밖에 없다.

따라서, 우리는 한 번에 보여주는 게 아닌 스크롤을 통해서 마지막 아이템이 뷰포트에 감지되면 20개씩 리스트를 생성하려고 한다. 구현하려고 하는 로직은 대충 이렇다. "처음에 20개를 생성 -> 마지막 아이템을 감지 타겟으로 등록 -> 스크롤을 하다가 마지막 아이템을 옵저버가 감지 -> 새로운 20개를 생성 -> 마지막 아이템을 감지 타겟으로 등록 -> 반복..."

 

const data = Array.from({ length: 100000 }, (_, i) => `Item ${i + 1}`);
const itemsContainer = document.querySelector('.items');
let observerTarget = itemsContainer.lastElementChild;

const observer = new IntersectionObserver((entries) => {
    entries.forEach(entry => {
        if (entry.isIntersecting) {
            loadMoreItems(); // 아이템을 생성하는 함수
        }
    })
})

let visibleItems = 0; // 초기 시작은 0개
function loadMoreItems() {
    observer.unobserve(observerTarget); // 기존에 등록된 옵저버 타겟 삭제
    const itemsToAdd = 20; // 생성할 아이템의 갯수

    for (let i = visibleItems; i < visibleItems + itemsToAdd; i++) {
        // 생성한 아이템의 갯수가 data보다 크거나 같아지면 옵저버를 멈춤
        if (i >= data.length) {
            observer.unobserve(observerTarget);
            break;
        }

        const newItem = document.createElement('li');
        newItem.classList.add('item');
        newItem.textContent = data[i];
        itemsContainer.appendChild(newItem);
        
        observerTarget = newItem; // 생성될 때마다 타겟을 지정
    }

    visibleItems += itemsToAdd; // 현재 아이템의 갯수에 생성된 아이템의 갯수를 더함
    observer.observe(itemsContainer.lastElementChild); // 옵저버에 등록
}

loadMoreItems(); // 초기 상태 (아이템 0개)일 때 실행하는 함수

초기 상태일 때(스크롤 없이 처음 페이지에 들어왔을 때) 아이템이 0개이니 일단 20개를 우선적으로 생성해줘야 한다.

loadMoreItems() 함수는 아이템을 20개 생성하고, 아이템을 생성할 때마다 그 아이템을 옵저버에 등록시킬 타겟 변수에 담아준다. 아이템을 생성할 때마다 타겟 변수에 담으므로 자연스럽게 마지막 아이템이 옵저버의 타겟이 된다.

 

초기에 20개를 생성해 주고, 그 20번째 아이템이 옵저버의 감지 타겟이 되었다. 이제 스크롤을 하다가 20번째 아이템이 브라우저의 뷰포트에 들어오는 순간 다시 loadMoreItems() 함수를 실행한다. 이때는 for문에서 가리키는 변수값을 잘 봐야 한다. visibleItems는 초기에 0이었다. (아이템이 0개였기 때문에) 하지만 지금은 20개가 있다. 따라서 visibleItems도 20이 되어야 한다. 이 변수도 loadMoreItems()가 실행될 때 itemsToAdd 만큼 더해주어야 한다.

 

초기에 데이터를 20번째까지 만들어놨으므로 다음 차례는 21번부터 40번까지의 데이터를 불러와야 한다. 따라서, 반복문을 visibleItems + itemsToAdd의 값까지 반복시켜 주었다.

 

for문이 종료된 후, 옵저버에게 새롭게 생성된 아이템들 중 마지막 아이템을 감지 타겟으로 등록하는 observe(itemContainer.lastElementChild)를 수행하면, 이제 스크롤을 하다가 20번째를 기준으로 계속해서 데이터가 무한히 생성되는 무한 스크롤을 구현할 수 있다.

 

무한 스크롤은 Intersection Observer API의 기능을 적절히 활용하여 뷰포트에 진입하는 특정 대상을 감지하여 구현되는 기능이다. 해당 API를 사용하면 무한 스크롤 외에도 다양한 기능을 구현할 수 있다. 따라서, 뷰포트를 활용한 기능 구현이 필요한 경우에는 무한 스크롤에 국한되지 않고 여러 가지 방법으로 Inersection Observer를 활용해 보자.

 

1. const observer = new IntersectionObserver((entries) => entry.forEach(..)) // entries는 감시 대상 배열
2. entry.isIntersecting // 감시 대상이 뷰포트에 들어오면 true를 반환
3. observer.observe(point) // 옵저버에 감시 대상 등록
4. observer.unobserve(point) // 옵저버에 감시 대상 해제
5. 이 밖에도 다양한 옵저버 메서드와 기능들이 많으니 공식 문서를 꼭 확인해 보기!