intersection을 이용한 무한스크롤 기능 구현

Jeonghun·2023년 9월 17일
0

React

목록 보기
13/21


최근 진행한 깃허브 이슈 프로젝트에서 데이터 리스팅 페이지에 '무한스크롤' 기능을 사용하여 구현하였다. 무한스크롤 기능의 구현 과정을 알아보자.

Infinite Scroll

무한스크롤이란 말 그대로 연속적인 스크롤을 제공하는 UI/UX 요소를 말한다. 이는 웹이나 앱에서 스크롤이 페이지의 끝에 도달했을 때, 자동으로 다음 데이터를 요청하여 받아오는 방식으로 별도의 페이지 이동 없이 데이터를 계속해서 불러오기 때문에 보통의 '페이지네이션' 기능보다 편리하다. 또한, 한 번에 모든 데이터를 불러오는 것이 아니기 때문에 보다 효율적으로 데이터를 관리할 수 있다.

무한스크롤의 구현 방법

1. 스크롤 이벤트 이용하기

무한스크롤 구현 방법 중 대표적으로 스크롤 이벤트를 이용할 수 있다. 이는 스크롤 이벤트를 감지하여 페이지의 끝에 도달했을 때 새로운 데이터를 불러오는 방식이다.

2. intersection observer api

무한스크롤 기능을 구현하는 또 다른 방법은 intersection observer 를 이용하는 방법이다. 이는 요소와 뷰포트 간의 교차를 감지하는 API를 사용하여, 특정 요소가 화면에 나타나면 새로운 데이터를 불러오는 방식이다.

3. 라이브러리 사용

리액트 환경에서는 react-infinite-scroll 라이브러리를 사용하여 간단하게 구현할 수 있다.

Intersection Observer

위에서 언급한 세 가지 방법중 나는 Intersection Observer API 를 이용해 기능을 구현하였다. 그 이유는 우선 라이브러리를 사용한 구현 방식은 이전 프로젝트에서 경험해보았으며, 모든 기능을 구현하는데 있어 무작정 라이브러리를 사용하는 것은 오히려 해당 기능의 작동 원리를 파악하기 어렵기 때문에 학습에 독이 된다고 생각했다. 물론 원티드에서 라이브러리 사용을 제한 하기도 했다. 스크롤 이벤트 방식을 사용하지 않은 이유는 다음과 같다.

Scroll Event vs Intersection

위에서 언급했듯 스크롤 이벤트는 '스크롤에 움직임에 따라 이벤트를 감지한다.' 이는 즉, 스크롤의 위치를 계산해야 하는 단점이 존재한다는 것이다. 또한, 스크롤이 조금만 움직여도 이벤트가 빈번하게 발생하여 성능상으로 좋지 않은 영향을 끼칠 위험이 크다.

반면에 Intersection 은 요소와 뷰포트의 교차 지점만을 계산하기 때문에 계산이 한 번만 발생하게되고, 따로 스크롤의 위치를 파악하지 않아도 된다. 또한, 외부 API 를 이용하는 방식이기 때문에 비교적 로직이 간단하다.

위와 같은 이유로 나는 프로젝트에서 Intersection Observer 를 채택했다.

Intersection API 사용방법

그럼 Intersection API의 사용방법을 알아보자. MDN을 보면, Intersection API에 대해 아래와 같이 설명하고 있다.

Intersection Observer API는 타겟 엘리먼트가 조상 엘리먼트, 또는 최상위 문서의 뷰포트(브라우저에서는 보통 브라우저의 viewport)의 교차영역에서 발생하는 변화를 비동기로 관찰하는 방법을 제공합니다.

사용 구문 작성

intersection API의 기본 사용 구문은 아래 코드와 같다. 기본적으로 new 키워드를 통해 intersection을 생성하고, 이는 매개변수로 callback 함수와 options를 받는다.

// intersection의 기본 구문

new IntersectionObserver(callback[, options]);

매개변수

  1. callback : 타겟이 되는 대상 요소의 영역 비율이 교차되었을 때 실행되는 함수이며 인자로 entries, observer 를 받는다.

    • 1.1) entries : intersectionOberserverEntry 객체의 배열

    • 1.2) observer : callback 함수를 호출하는 intersectionObserver

  2. options (선택) : intersection을 조절할 수 있는 객체로, 지정하지 않을시 문서의 뷰포트를 root로, margin 값 0으로 설정된다.

    • 2.1) root : 교차 영역의 기준이 되는 root 요소로 observer의 대상으로 등록할 요소는 반드시 root의 하위 요소여야 한다. 기본값은 null 이며, 이는 브라우저의 viewport가 된다.

    • 2.2) rootMargin : root 요소의 margin 값이다. css에서 margin 값을 부여하는 것과 동일한 방식으로 선언할 수 있으며, rootMargin의 값에 따라 교차 영역이 확대 되거나 축소된다. rootMargin의 '기본값은 상하좌우 모두 0px' 이다.

    • 2.3) threshold : 0.0 ~ 1.0 사이의 숫자, 혹은 이 숫자들로 이루어진 배열로, 대상 요소에 대한 교차 영역의 비율을 의미한다. threshold가 0.0일 경우 대상 요소가 교차 영역에 진입하는 시점에 observer를 실행하게 되며, 1.0일 경우에는 요소의 전체가 교차 영역에 들어왔을 때, observer를 실행한다. 기본값은 0.0 이다.

  3. Instance Methods

    • 3.1) disconnect() : 관찰 대상 요소들에 대한 모든 관찰을 멈출 때 사용한다.

    • 3.2) observe() : 대상 요소에 대한 observer를 등록할 때 즉, 관찰을 시작할 때 사용한다.

    • 3.3) unobserve() : 하나의 대상 요소에 대한 관찰만을 멈출 때 사용한다.

    • 3.4) takeRecords() : intersectionObserverEntry 객체의 배열을 리턴한다.

사용 예시 코드

위의 설명을 바탕으로 예시 코들를 작성해보면 다음과 같다.

function createObserver() {
  let observer;

  const options = {
    root: document.querySelector('.rootElement'), // .rootElement className을 가진 요소를 root로 지정
    rootMargin: "5px", // rootMargin 값을 상하좌우 5px로 지정
    threshold: [1], // 대상 요소의 전체가 교차 영역에 진입했을 때 observer 실행
  };

  
// IntersectionObserver 생성
  observer = new IntersectionObserver(handleIntersect, options);
  observer.observe(boxElement);
}

프로젝트에 적용하기

나의 react 프로젝트에 이를 적용하기 위해 아래와 같이 useInfiniteScroll 이라는 이름의 커스텀 훅 파일을 작성했다.

// useInfiniteScroll.ts

import { useEffect, useRef, useState } from 'react';

const useInfiniteScroll = (callback: () => void): [boolean, IntersectionObserver | null] => {
  // isFetching 상태로 스크롤이 페이지 하단에 도달했는지 여부 체크
  const [isFetching, setIsFetching] = useState<boolean>(false);
  
  // IntersectionObserver 인스턴스를 참조하기 위한 observer ref 생성
  const observer = useRef<IntersectionObserver | null>(null);

  useEffect(() => {
    // 현재 브라우저가 IntersectionObserver를 지원하지 않으면 return
    if (!('IntersectionObserver' in window)) return;

    // 대상 요소가 뷰포트와 교차할 때 호출되는 콜백 함수
    const handleIntersect = ([entry]: IntersectionObserverEntry[]) => {
      // 대상 요소가 현재 뷰포트와 교차하면 isFetching을 true로 변경
      if (entry.isIntersecting) setIsFetching(true);
    };

    // IntersectionObserver를 초기화하고 handleIntersect 콜백 연결
    observer.current = new IntersectionObserver(handleIntersect);
    return () => observer.current?.disconnect(); // 컴포넌트가 언마운트될 때 Observer 연결 해제
  }, []);

  useEffect(() => {
    // isFetching이 false면 추가 작업 없이 return
    if (!isFetching) return;

    // isFetching이 true면 callback 함수를 호출한 후 isFetching을 다시 false로 변경
    callback();
    setIsFetching(false);
  }, [isFetching]);

  return [isFetching, observer.current];
};

export default useInfiniteScroll;

그리고 아래와 같이 IssueList 컴포넌트에서 해당 훅을 import해 사용했다.

// IssueList.tsx

import React, { useCallback } from 'react';
import { useIssueData } from '../../hooks/useIssueData';
import useInfiniteScroll from '../../hooks/useInfiniteScroll';

// 이외 import 구문 생략

const IssueList: React.FC = () => {
  const { issues, moreDataLoading, loadMoreIssues, hasMore } = useIssueData();

  // 추가 데이터가 있을 경우 loadMoreIssue 함수 호출
  const fetchMoreData = useCallback(() => {
    if (hasMore) {
      loadMoreIssues();
    }
  }, [hasMore, loadMoreIssues]);

  // 스크롤이 페이지 하단에 도달할 때 fetchMoreData 함수가 호출됨
  const [isFetching, observer] = useInfiniteScroll(fetchMoreData);

  // 마지막 이슈 항목에 참조를 설정하여 해당 항목이 뷰포트와 교차할 때 observer가 동작하도록 함
  const lastIssueRef = (node: HTMLDivElement) => {
    if (moreDataLoading) return;
    if (observer && node) observer.observe(node);
  };


// return 구문 생략
};

export default IssueList;

결과물

위 코드를 적용해보니 IssueList Page에서 스크롤을 마지막까지 내렸을 때 마다, Network 탭에서 API 호출을 하는것을 확인할 수 있었다.


포스팅을 마치며

이상으로 Intersection API를 통해 infinite Scroll 기능을 구현하는 방법에 대해 알아보았다. 해당 API를 이용함으로써 기존의 scroll 이벤트를 사용하는 방법보다 성능을 향상시킬 수 있으며, 효율적인 방법으로 데이터를 fetch 할 수 있게 되었다.

참고

MDN Intersection API

profile
안녕하세요, 프론트엔드 개발자 임정훈입니다.

0개의 댓글