무한 스크롤

컨텐츠를 페이징하는 기법 중 하나로, 스크롤하다 컨텐츠의 마지막 요소를 볼 즈음 다음 컨텐츠가 있으면 불러오는 방식이다. 

 

구현 방식 두 가지

  1. window의 scroll 이벤트를 통해 스크롤이 일어날 때마다 화면 전체의 height와 스크롤 위치를 통해 스크롤이 컨텐츠의 끝에 도달했는지 체크해서 처리하는 방식
  2. intersection observer 방식 (지정한 DOM 객체가 보고 있는 시야에 닿았는지 체크)

 

실습

컴포넌트 구조

  • App - 데이터 불러오고 PhotoList 컴포넌트 생성, 불러온 데이터를 pothoList라는 이름으로 내려줌
  • PhotoList - 스크롤이 끝에 도달하면 onScrollEnded 콜백을 호출하는 식으로 App 컴포넌트 쪽에 제어권을 넘김

 

Scroll Event

- 무한 스크롤 구현 전 더보기 버튼 UI를 구현해보는 상황

더보기 버튼을 여러번 클릭하면 다음과 같이 api가 여러번 호출이 되는 문제 상황이 발생한다. 

 

isLoading state를 추가하여 로딩 중에는 api 호출이 한 번만 되도록 설정하였다. 

  window.addEventListener("scroll", () => {
    const { isLoading, totalCount, photos } = this.state;
    const isScrollEnded =
      window.innerHeight + window.scrollY >= document.body.offsetHeight;

    if (isScrollEnded && !isLoading && photos.length < totalCount) {
      onScrollEnded();
    }
  });

 

위 코드로 스크롤이 화면의 끝에 닿았는지를 체크할 수 있는데 콘솔에 찍어보면 다음과 같다. 

  window.addEventListener("scroll", () => {
    console.log(
      window.innerHeight + window.scrollY >= document.body.offsetHeight
    );
  });
}

화면 바닥에 닿았을 경우에만 값이 true가 된다.

  • document.body.offsetHeight: 전체 렌더링 된 높이 (body의 높이)
  • window.innerHeight: 네비게이션 창(탭, url, 북마크)을 제외한 높이
  • window.scrollY: 현재 스크롤바 위치

따라서 window.innerHeightwindow.scrollY을 더하면 전체 컨텐츠에서 스크롤이 얼마나 되었는지 볼 수 있다.

이 경우에는 무조건 스크롤의 끝에 닿은 경우에만 true가 되기 때문에 100px정도 보정을 하고 싶다면 다음과 같이 작성하면 된다.

  window.addEventListener("scroll", () => {
    console.log(
      window.innerHeight + window.scrollY + 100 >= document.body.offsetHeight
    );
  });
}

 

무한 스크롤에도 isLoading을 체크해야 api 호출이 여러번 되지 않는다.

  window.addEventListener("scroll", () => {
    const isScrollEnded =
      window.innerHeight + window.scrollY + 100 >= document.body.offsetHeight;

    if ((isScrollEnded && !this.state.isLoading)) {
      onScrollEnded();
    }
  });

아니면 isLoading 대신에 쓰로틀링(Throttling)을 사용할 수도 있다.

쓰로틀링은 처음 들어온 이벤트만 실행하고 일정 시간동안 들어온 동일한 이벤트는 전부 무시하는 것이다.

 

현재 페이지가 끝났는데도 스크롤의 끝에 도달할 때마다 계속 호출하고 있다는 문제점이 있다.

마지막 데이터까지 불러왔다는 것을 체크하기 위해서는 컨텐츠의 전체 개수를 불러오는 api 필요하다.

위에서 불러온 값과 현재 내가 불러온 데이터의 개수를 비교해서 체크할 수 있다. 

totalCount state 추가

  window.addEventListener("scroll", () => {
    const { isLoading, totalCount, photos } = this.state;
    const isScrollEnded =
      window.innerHeight + window.scrollY >= document.body.offsetHeight;

    if (isScrollEnded && !isLoading && photos.length < totalCount) {
      onScrollEnded();
    }
  });
}

 

지금 스크롤 이벤트가 계속 반복되고 있는데 이런 현상은 디바운싱으로 방지할 수 있다.

 

Intersection Observer

 

IntersectionObserver - Web API | MDN

Intersection Observer API의 IntersectionObserver 인터페이스는 대상 요소와 상위 요소, 또는 대상 요소와 최상위 문서의 뷰포트가 서로 교차하는 영역이 달라지는 경우 이를 비동기적으로 감지할 수 있는

developer.mozilla.org

Intersection Observer을 이용해서 가장 마지막에 있는 li가 있을 때를 감지하고 그 후 다음 페이지를 불러올 수 있다.

참고로 Intersection Observer는 무한 스크롤 외에도 이미지 지연에 좋다.

이미지 지연이란 실제로 화면에 보이는 이미지만 그때 바로 불러오는 기법이다.

 

  const observer = new IntersectionObserver(
    (entries) => {
      entries.forEach((entry) => {
        if (entry.isIntersecting && !this.state.isLoading) {
          if (this.state.photos.length < this.state.totalCount) {
            onScrollEnded();
          }
        }
      });
    },
    {
      threshold: 0.5,
    }
  );

entries는 observer할 대상을 여러개 정할 수 있다.

속성 중 threshold는 요소가 얼마만큼 보였는지를 의미한다.

만일 threshold를 1이라고 한다면 요소가 뷰포트에 완전히 들어올 경우에만 감지가 된다.

 

스크롤 이벤트처럼 매번 이벤트가 발생하지 않기 때문에 선능면에서 더 좋다.

 

정리

무한 스크롤 UI를 구현하는 방법 두 가지

  • scroll 이벤트를 이용해 계산
  • Intersection Observer
    • observe, unobserve를 잘 해야함
    • threshold값으로 observe 대상이 얼마나 노출되어있는지에 따라 동작하게 할 수 있음

무한 스크롤의 단점은 푸터를 보기가 어렵다는 점이다.

따라서 상황에 따라 무한 스크롤 UI보다는 직접 불러오는 인터렉션을 통해 로딩하는 게 더 나을 수도 있다.

 


출처: 프로그래머스 프론트엔드 데브코스 

[Day 25] VanillaJS를 통한 자바스크립트 기본 역량 강화 II (2)

복사했습니다!