[ TIL 221216 ] 무한 스크롤 (Infinite Scroll) - 스크롤 방식

ponyo·2022년 12월 16일
0

Today I Learned

목록 보기
28/30

무한 스크롤

모바일에서 페이지네이션 방식을 사용하려면 원하는 페이지로 이동을 하기 위해 숫자 버튼을 일일이 손으로 눌러야 하기 때문에 매우 불편

이 때 사용할 수 있는게 무한 스크롤

사용자가 스크롤링 하다가 미리 로드된 콘텐츠를 다 확인하면 다음 목록을 또 로드해서 별도의 인털게션 없이 목록을 계속 불러오는 방식


스크롤이 최하단에 왔는지 판단하기

Element.scrollHeight

: 엘리먼트의 총 높이를 나타내며 바깥으로 넘쳐서 보이지 않는 콘텐츠도 포함

Element.clientHeight

: 엘리먼트의 내부 높이 (padding 포함, scroll bar 높이, margin, border 미포함)

Element.offsetHeight

: 엘리먼트의 내부 높이 (padding 포함, scroll bar 높이, margin, border 포함)

Element.scrollTop

: 스크롤 바의 Top 부분이 화면에 내려온 위치

즉, scrollHeight - clientHeight - scrollTop 이 미리 정해놓은 offset 미만 일 때 스크롤이 최하단에 왔다고 판단해서 다음 페이지를 가져오고 기존 항목들에 덧붙여(append) 주면 됩니다.


스크롤 방식의 한계

스크롤을 움직일 때마다 이벤트 발생 -> 성능 문제 야기
이를 해결하기 위해 보통 이벤트에 쓰로틀링(throttling)을 적용하여 이벤트 제한


Debounce vs Throttling

주로 DOM 이벤트를 기반으로 실행하는 자바스크립트를 성능상의 이유로 이벤트를 제한할 때 debounce와 throttling 을 적용

debounce

: 이벤트를 그룹핑해서 특정 시간이 지난 후 하나의 이벤트만 발생하도록 하는 기술. 연달아서 호출되는 함수들 중 마지막 함수만 호출하도록 하는 것

throttling

: 이벤트를 일정한 주기마다 발생하는 기술. 마지막 함수가 호출된 후 일정 시간이 지나기 전엔 다시 호출되지 않도록 하는 방식

Preview

리액트 프로젝트 생성

npx create-react-app modal-playground --template typescript

패키지 설치

npm i axios throttle-debounce
npm i @types/throttle-debounce -D

src/App.tsx

import React, { useState, useRef, useEffect } from "react";
import axios from "axios";
import { throttle } from "throttle-debounce";

interface Airline {
  id: number;
  name: string;
  country: string;
  logo: string;
  slogan: string;
  head_quaters: string;
  website: string;
  extablished: string;
}

interface Passenger {
  _id: string;
  name: string;
  trips: number;
  airline: Airline;
  __v: number;
}

function App() {
  const listRef = useRef<HTMLUListElement>(null);
  const currentPageRef = useRef<number>(0);

  const [passengers, setPassengers] = useState<Array<Passenger>>([]);
  const [isLast, setIsLast] = useState<boolean>(false);
  const [isScrollBottom, setIsScrollBottom] = useState<boolean>(false);

  const getPassengers = async (init?: boolean) => {
    const params = { page: currentPageRef, size: 30 };

    try {
      const response = await axios.get("https://api.instantwebtools.net/v1/passenger", { params });

      const passengers = response.data.data;
      const isLast = response.data.totalPages === currentPageRef.current;

      init ? setPassengers(passengers) : setPassengers((prev) => [...prev, ...passengers]);
      setIsLast(isLast);
    } catch (e) {
      console.error(e);
    }
  };

  const handleScroll = throttle(1000, () => {
    if (listRef.current) {
      const { scrollHeight, offsetHeight, scrollTop } = listRef.current;

      const offset = 50;

      console.log("trigger");

      console.log(scrollTop, scrollHeight, offsetHeight);

      setIsScrollBottom(scrollHeight - offsetHeight - scrollTop < offset);
    }
  });

  useEffect(() => {
    if (isScrollBottom) {
      currentPageRef.current += 1;

      !isLast && getPassengers();
    }
  }, [isScrollBottom, isLast]);

  useEffect(() => {
    getPassengers(true);
  }, []);

  return (
    <div>
      <ul ref={listRef} className="list" onScroll={handleScroll}>
        {passengers.map((passenger) => (
          <li className="item" key={passenger._id}>
            {passenger.name}
          </li>
        ))}
      </ul>
    </div>
  );
}

export default App;

src/index.css

body {
  margin: 0;
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

code {
  font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", monospace;
}

.list {
  overflow: hidden scroll;
  list-style: none;
  margin: 0;
  padding: 0;
  width: 100%;
  height: 100vh;
}

.item {
  font-size: 24px;
}
profile
😁

0개의 댓글