-
[React] Infinite Scroll(무한 스크롤) with Intersection ObserverFrontend 2020. 4. 19. 20:04
Intersection Observer API는 "상위 레벨 Document의 Viewport 또는 부모 요소"와 "타겟 요소" 간 교차 지점의 변화를 비동기적으로 관찰할 수 있는 방법을 제공한다.
Intersection Observer가 등장하기 전에는 어떤 요소가 화면에 보여지는지 감지하는 것은 매우 복잡한 일이었다. 따라서 요소의 visibility를 감지하고 이벤트를 주는 것은 사용자가 접근하는 여러 브라우저와 웹 사이트들을 느리게 만드는 원인 중 하나였다. 그러나 웹 기술이 발전하면서 요소 visibility 감지의 필요성이 점점 높아졌고, Intersection 정보들이 필요하게 되었다.
- 이미지의 Lazy Loading이나 페이지 스크롤 시 컨텐츠 로딩
- 무한 스크롤 구현
- 광고들이 실제로 유저에게 노출되는지 보고하기 위해
- 유저가 결과를 보는지에 따라 특정 애니메이션이나 task를 수행할 지 여부를 결정하기 위해
과거에는 이러한 작업을 하기 위해서 event handlers들을 이용했다. 그러나 이러한 코드들이 모두 메인 쓰레드에서 실행되기 때문에 성능 문제로 연결될 수 있다는 단점이 있었다. 예를 들어서 scroll event listener를 등록한다고 하자. 유저가 아주 약간만 스크롤을 움직여도 scroll listener가 계속 발생할 것이고 성능에 안좋은 영향을 미칠 것이다.
반면 Intersection Observer API는 관찰하고자 하는 타켓 요소가 "다른 요소 또는 viewport"에 들어오거나 나갈 때마다 감지하여 미리 등록된 콜백 함수를 실행시킨다. 이 경우에 더 이상 웹 사이트는 요소의 intersection을 관찰하기 위해 메인 쓰레드를 사용할 필요가 없어진다. 다만 Intersection Observer로는 아주 정확하게 몇 pixel이 교차되었는지는 알아낼 수 없다. 그렇기 때문에 정확한 pixel 값 보다는 대략 N%가 교차되었는지 정하는 식으로 사용한다.
let options = { root: document.querySelector('#scrollArea'), rootMargin: '0px', threshold: 1.0 } let observer = new IntersectionObserver(callback, options);
Intersection Observer API는 위와 같이 callback 함수와 옵션 객체를 인자로 받는다.
- Callback: target이라고 불리는 특정 요소가 device의 viewport나 특정 요소(옵션의 root)에 교차되었을때 callback 함수가 실행된다.
- options:
- root: target의 visibility를 검사하기 위한 viewport로 사용될 요소로 target의 상위 요소여야만 한다.
- 기본값: browser viewport (아무 값도 설정하지 않거나 null로 설정할 경우 browser viewport를 기본값으로 한다.)
- rootMargin: root 주변의 margin을 말하며, CSS의 margin 속성과 비슷하게 "10px 20px 30px 40px" (top, right, bottom, left)로 줄 수 있다. %값으로도 줄 수 있으며 이 값에 의해 root 요소의 박스 모델을 기준으로 범위가 늘어나거나 줄어들거나 한다.
- 기본값: 0px
- threshold: 단일 숫자이거나 숫자가 든 배열을 지정할 수 있다. target 요소와 root 간의 intersection 비율을 말한다. (target 요소가 얼마나 보여지는지 0 ~ 1의 비율로 나타낸 값)
- 만약 50% 정도만 교차되었을 때 콜백 함수를 실행시키고 싶다면 0.5를 지정하면 된다.
- 0은 1 px이라도 보여지면 콜백을 실행시킨다는 뜻이고, 1은 모든 px이 전부 화면에 보여져야만 콜백을 실행시킨다는 뜻이다.
- 기본값: 0
- root: target의 visibility를 검사하기 위한 viewport로 사용될 요소로 target의 상위 요소여야만 한다.
지난 번 포스팅에서 redux/toolkit과 redux-saga를 적용하여 unsplash 이미지들을 불러오는 간단한 페이지에
Intersection Observable API를 이용하여 무한 스크롤을 구현해보았다.
import { useEffect } from 'react'; export const useInfinteScroll = ({ root = null, target, onIntersect, threshold = 1.0, rootMargin = '0px', }) => { useEffect(() => { const observer = new IntersectionObserver(onIntersect, { root, rootMargin, threshold, }); if (!target) { return; } observer.observe(target); return () => { observer.unobserve(target); }; }, [target, root, rootMargin, onIntersect, threshold]); };
먼저 useInfiniteScroll이라는 함수를 만들었다.
이 함수는 Intersection Observer의 callback 함수와 설정 값들을 인자로 받아서
useEffect 콜백 함수 안에서 observer 인스턴스를 생성한 후 구독을 해주는 역할을 한다.
import React, { useEffect, useState } from 'react'; import { useSelector, useDispatch } from 'react-redux'; import { unsplashAction, unsplashSelector } from './slice'; import { useInfinteScroll } from '../../hooks'; import './styles.css'; import Loader from '../../components/Loader'; import ErrorView from '../../components/ErrorView'; const ImageGrid = () => { const dispatch = useDispatch(); const [target, setTarget] = useState(null); const { isLoading, images, error } = useSelector(unsplashSelector.all); useInfinteScroll({ target, onIntersect: ([{ isIntersecting }]) => { if (isIntersecting) { dispatch(unsplashAction.loadMore()); } } }); useEffect(() => { dispatch(unsplashAction.load()); }, []); if (isLoading) { return <Loader />; } if (error) { return <ErrorView />; } return ( <div className='content'> <section className='grid'> {images.map(image => ( <div key={ image.id } className={`item item-${Math.ceil( image.height / image.width, )}`} > <img src={ image.urls.small } alt={ image.user.username } /> </div> ))} <div ref={ setTarget } className='last-item' > <Loader size='s' /> </div> </section> </div> ); }; export default ImageGrid;
이제 처음 페이지 로드 시에는 unsplashAction.load() 액션을 실행시켜 이미지를 로드한후,
observer를 생성, 등록해준다.
observer 생성 시 target 요소는 리스트의 맨 마지막 빈 div로 설정했다.
그리고 observer의 콜백 함수에서 isIntersecting이 true인 경우에 unsplashAction.loadMore() 액션을 로드하여
추가 이미지를 불러오게끔 해주었다.
Github repo:
https://github.com/jy7123943/redux-saga-practice/blob/redux-toolkit/src/features/ImageGrid/index.js
Reference:
https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API
https://y0c.github.io/2019/06/30/react-infinite-scroll/
https://scotch.io/tutorials/infinite-scroll-in-react-using-intersection-observer
반응형'Frontend' 카테고리의 다른 글
비율에 따라 줄어드는 SVG 이미지 구현하기 with CSS 고군분투 (251) 2020.05.24 memo, useMemo, useCallback으로 React 성능 최적화하기 (253) 2020.05.17 [Redux saga] 리덕스 사가에 대해서 (Redux toolkit과 같이 사용해보기) (254) 2020.04.12 [Js] 자바스크립트, Generator 함수에 대하여 (253) 2020.03.29 [JS] 자바스크립트, Nullish Coalescing Operator (다른 논리 연산자와 비교) (252) 2020.03.12 COMMENT