React에서 무한 스크롤 및 이미지 지연 로딩 구현하기

게시 됨: 2022-03-10
빠른 요약 ↬ 이 튜토리얼에서는 HTML Intersection Observer API를 사용하여 React 기능 구성요소에서 무한 스크롤 및 이미지 지연 로딩을 구현하는 방법을 배울 것입니다. 그 과정에서 우리는 React의 hooks를 어떻게 사용하는지와 Custom Hooks를 만드는 방법을 배우게 될 것입니다.

페이지 매김에 대한 대안을 찾고 있다면 무한 스크롤을 고려하는 것이 좋습니다. 이 기사에서는 React 기능 구성 요소의 컨텍스트에서 Intersection Observer API에 대한 몇 가지 사용 사례를 탐색할 것입니다. 독자는 React 기능 구성 요소에 대한 작업 지식을 가지고 있어야 합니다. React hooks에 대한 약간의 친숙함은 도움이 될 것이지만 몇 가지를 살펴볼 것이기 때문에 필수는 아닙니다.

우리의 목표는 이 기사의 끝에서 네이티브 HTML API를 사용하여 무한 스크롤 및 이미지 지연 로딩을 구현하는 것입니다. 또한 React Hooks에 대해 몇 가지 더 배웠을 것입니다. 이를 통해 필요한 경우 React 애플리케이션에서 무한 스크롤 및 이미지 지연 로딩을 구현할 수 있습니다.

시작하자.

React와 Leaflet으로 지도 만들기

CSV 또는 JSON 파일에서 정보를 파악하는 것은 복잡할 뿐만 아니라 지루합니다. 동일한 데이터를 시각 자료의 형태로 표현하는 것이 더 간단합니다. Shajia Abidi는 Leaflet 도구의 강력한 기능과 다양한 종류의 지도를 만드는 방법을 설명합니다. 관련 기사 읽기 →

점프 후 더! 아래에서 계속 읽기 ↓

교차로 관찰자 API

MDN 문서에 따르면 "Intersection Observer API는 상위 요소 또는 최상위 문서의 뷰포트와 대상 요소의 교차점에서 변경 사항을 비동기식으로 관찰하는 방법을 제공합니다".

이 API를 사용하면 무한 스크롤 및 이미지 지연 로딩과 같은 멋진 기능을 구현할 수 있습니다. 교차 관찰자는 생성자를 호출하고 콜백 및 옵션 객체를 전달하여 생성됩니다. 콜백은 target 이라고 하는 하나의 요소가 장치 뷰포트 또는 root 라고 하는 지정된 요소와 교차할 때마다 호출됩니다. options 인수에 사용자 정의 루트를 지정하거나 기본값을 사용할 수 있습니다.

 let observer = new IntersectionObserver(callback, options);

API는 사용하기 쉽습니다. 일반적인 예는 다음과 같습니다.

 var intObserver = new IntersectionObserver(entries => { entries.forEach(entry => { console.log(entry) console.log(entry.isIntersecting) // returns true if the target intersects the root element }) }, { // default options } ); let target = document.querySelector('#targetId'); intObserver.observe(target); // start observation

entriesIntersectionObserverEntry 개체의 목록입니다. IntersectionObserverEntry 개체는 하나의 관찰 대상 요소에 대한 교차 변경 사항을 설명합니다. 콜백은 메인 스레드에서 실행되기 때문에 시간이 많이 걸리는 작업을 처리해서는 안 됩니다.

Intersection Observer API는 현재 caniuse에 표시된 것처럼 광범위한 브라우저 지원을 즐깁니다.

교차로 관찰자 브라우저 지원. (큰 미리보기)

리소스 섹션에 제공된 링크에서 API에 대한 자세한 내용을 읽을 수 있습니다.

이제 실제 React 앱에서 이 API를 사용하는 방법을 살펴보겠습니다. 우리 앱의 최종 버전은 무한 스크롤되는 사진 페이지가 될 것이며 각 이미지는 느리게 로드됩니다.

useEffect 훅으로 API 호출하기

시작하려면 이 URL에서 시작 프로젝트를 복제하십시오. 최소한의 설정과 몇 가지 스타일이 정의되어 있습니다. 또한 스타일링을 위해 해당 클래스를 사용할 것이기 때문에 public/index.html 파일에 Bootstrap 의 CSS에 대한 링크를 추가했습니다.

원하는 경우 자유롭게 새 프로젝트를 만드십시오. repo를 따르려면 yarn 패키지 관리자가 설치되어 있는지 확인하십시오. 여기에서 특정 운영 체제에 대한 설치 지침을 찾을 수 있습니다.

이 튜토리얼에서는 공개 API에서 사진을 가져와 페이지에 표시할 것입니다. 우리는 Lorem Picsum API를 사용할 것입니다.

이 자습서에서는 그림 개체의 배열을 반환하는 끝점인 https://picsum.photos/v2/list?page=0&limit=10 을 사용합니다. 다음 10장의 사진을 얻기 위해 page의 값을 1로 변경한 다음 2로 변경하는 식으로 변경합니다.

이제 App 구성 요소를 하나씩 빌드합니다.

src/App.js 를 열고 다음 코드를 입력합니다.

 import React, { useEffect, useReducer } from 'react'; import './index.css'; function App() { const imgReducer = (state, action) => { switch (action.type) { case 'STACK_IMAGES': return { ...state, images: state.images.concat(action.images) } case 'FETCHING_IMAGES': return { ...state, fetching: action.fetching } default: return state; } } const [imgData, imgDispatch] = useReducer(imgReducer,{ images:[], fetching: true}) // next code block goes here }

먼저 감속기 함수 imgReducer 를 정의합니다. 이 감속기는 두 가지 작업을 처리합니다.

  1. STACK_IMAGES 작업은 images 배열을 연결합니다.
  2. FETCHING_IMAGES 작업은 fetching 변수의 값을 truefalse 사이에서 토글합니다.

다음 단계는 이 감속기를 useReducer 후크에 연결하는 것입니다. 완료되면 다음 두 가지를 반환합니다.

  1. 두 개의 변수를 포함하는 imgData : images 는 그림 객체의 배열입니다. fetching 는 API 호출이 진행 중인지 여부를 알려주는 부울입니다.
  2. imgDispatch 는 리듀서 객체를 업데이트하는 함수입니다.

React 문서에서 useReducer 후크에 대해 자세히 알아볼 수 있습니다.

코드의 다음 부분은 API를 호출하는 부분입니다. App.js 의 이전 코드 블록 아래에 다음 코드를 붙여넣습니다.

 // make API calls useEffect(() => { imgDispatch({ type: 'FETCHING_IMAGES', fetching: true }) fetch('https://picsum.photos/v2/list?page=0&limit=10') .then(data => data.json()) .then(images => { imgDispatch({ type: 'STACK_IMAGES', images }) imgDispatch({ type: 'FETCHING_IMAGES', fetching: false }) }) .catch(e => { // handle error imgDispatch({ type: 'FETCHING_IMAGES', fetching: false }) return e }) }, [ imgDispatch ]) // next code block goes here

useEffect 후크 내에서 fetch API를 사용하여 API 엔드포인트를 호출합니다. 그런 다음 STACK_IMAGES 작업을 전달하여 API 호출 결과로 이미지 배열을 업데이트합니다. API 호출이 완료되면 FETCHING_IMAGES 작업도 전달합니다.

다음 코드 블록은 함수의 반환 값을 정의합니다. useEffect 후크 뒤에 다음 코드를 입력합니다.

 return ( <div className=""> <nav className="navbar bg-light"> <div className="container"> <a className="navbar-brand" href="/#"> <h2>Infinite scroll + image lazy loading</h2> </a> </div> </navv <div id='images' className="container"> <div className="row"> {imgData.images.map((image, index) => { const { author, download_url } = image return ( <div key={index} className="card"> <div className="card-body "> <img alt={author} className="card-img-top" src={download_url} /> </div> <div className="card-footer"> <p className="card-text text-center text-capitalize text-primary">Shot by: {author}</p> </div> </div> ) })} </div> </div> </div> );

이미지를 표시하기 위해 imgData 객체의 이미지 배열을 매핑합니다.

이제 앱을 시작하고 브라우저에서 페이지를 봅니다. 반응형 그리드에 이미지가 멋지게 표시되어야 합니다.

마지막 비트는 앱 구성 요소를 내보내는 것입니다.

 export default App;
반응형 그리드의 사진. (큰 미리보기)

이때 해당 분기는 01-make-api-calls입니다.

이제 페이지를 스크롤할 때 더 많은 그림을 표시하여 이를 확장해 보겠습니다.

무한 스크롤 구현

페이지 스크롤에 따라 더 많은 사진을 제공하는 것을 목표로 합니다. API 엔드포인트의 URL인 https://picsum.photos/v2/list?page=0&limit=10 에서 새로운 사진 세트를 얻으려면 page 값만 증가시키면 됩니다. 표시할 사진이 부족할 때도 이 작업을 수행해야 합니다. 여기서 우리의 목적을 위해 페이지 하단에 도달했을 때 이미지가 부족하다는 것을 알게 될 것입니다. Intersection Observer API가 이를 달성하는 데 어떻게 도움이 되는지 확인할 때입니다.

src/App.js 를 열고 imgReducer 아래에 새로운 감속기 pageReducer 를 만듭니다.

 // App.js const imgReducer = (state, action) => { ... } const pageReducer = (state, action) => { switch (action.type) { case 'ADVANCE_PAGE': return { ...state, page: state.page + 1 } default: return state; } } const [ pager, pagerDispatch ] = useReducer(pageReducer, { page: 0 })

하나의 작업 유형만 정의합니다. ADVANCE_PAGE 액션이 실행될 때마다 page 의 값은 1씩 증가합니다.

아래와 같이 페이지 번호를 동적으로 수락하도록 fetch 기능의 URL을 업데이트합니다.

 fetch(`https://picsum.photos/v2/list?page=${pager.page}&limit=10`)

pager.page 와 함께 종속성 배열에 imgData 를 추가합니다. 이렇게 하면 pager.page 가 변경될 때마다 API 호출이 실행됩니다.

 useEffect(() => { ... }, [ imgDispatch, pager.page ])

API 호출에 대한 useEffect 후크 후 아래 코드를 입력합니다. 수입 라인도 업데이트하십시오.

 // App.js import React, { useEffect, useReducer, useCallback, useRef } from 'react'; useEffect(() => { ... }, [ imgDispatch, pager.page ]) // implement infinite scrolling with intersection observer let bottomBoundaryRef = useRef(null); const scrollObserver = useCallback( node => { new IntersectionObserver(entries => { entries.forEach(en => { if (en.intersectionRatio > 0) { pagerDispatch({ type: 'ADVANCE_PAGE' }); } }); }).observe(node); }, [pagerDispatch] ); useEffect(() => { if (bottomBoundaryRef.current) { scrollObserver(bottomBoundaryRef.current); } }, [scrollObserver, bottomBoundaryRef]);

변수 bottomBoundaryRef 를 정의하고 그 값을 useRef(null) 로 설정합니다. useRef 를 사용하면 구성 요소가 렌더링되는 동안 변수가 값을 유지할 수 있습니다. 즉, 포함하는 구성 요소가 다시 렌더링될 때 변수의 현재 값이 유지됩니다. 값을 변경하는 유일한 방법은 해당 변수에 .current 속성을 다시 할당하는 것입니다.

우리의 경우 bottomBoundaryRef.currentnull 값으로 시작합니다. 페이지 렌더링 주기가 진행됨에 따라 현재 속성을 노드 <div id='page-bottom-boundary'> 설정합니다.

할당 문 ref={bottomBoundaryRef} 를 사용하여 이 할당이 선언된 div가 되도록 bottomBoundaryRef.current 를 설정하도록 React에 지시합니다.

따라서,

 bottomBoundaryRef.current = null

렌더링 주기가 끝나면 다음이 됩니다.

 bottomBoundaryRef.current = <div></div>

이 할당이 어디에서 수행되는지 잠시 후에 살펴보겠습니다.

다음으로 관찰자를 설정할 scrollObserver 함수를 정의합니다. 이 함수는 관찰할 DOM 노드를 허용합니다. 여기서 주목해야 할 요점은 관찰 중인 교차점에 도달할 때마다 ADVANCE_PAGE 작업을 전달한다는 것입니다. 그 효과는 pager.page 의 값을 1만큼 증가시키는 것입니다. 이것이 발생하면 종속성으로 가지고 있는 useEffect 후크가 다시 실행됩니다. 이 재실행은 차례로 새 페이지 번호로 가져오기 호출을 호출합니다.

이벤트 행렬은 다음과 같습니다.

관찰 중인 교차점 적중 → ADVANCE_PAGE 작업 호출 → pager.page 의 값을 1씩 증가 → 호출 호출 fetch 을 위한 useEffect 후크 → 호출 호출 실행 → 반환된 이미지가 images 배열에 연결됩니다.

후크의 종속성이 변경될 때만 함수가 실행되도록 useEffect 후크에서 scrollObserver 를 호출합니다. useEffect 후크 내에서 함수를 호출하지 않으면 함수는 모든 페이지 렌더링에서 실행됩니다.

bottomBoundaryRef.current<div id="page-bottom-boundary" style="border: 1px solid red;"></div> 를 참조한다는 것을 기억하세요. scrollObserver 에 전달하기 전에 값이 null이 아닌지 확인합니다. 그렇지 않으면 IntersectionObserver 생성자가 오류를 반환합니다.

useEffect 후크에서 scrollObserver 를 사용했기 때문에 끝없는 구성 요소가 다시 렌더링되는 것을 방지하기 위해 useCallback 후크로 이를 래핑해야 합니다. React 문서에서 useCallback에 대해 자세히 알아볼 수 있습니다.

<div id='images'> div 뒤에 아래 코드를 입력하세요.

 // App.js <div id='image'> ... </div> {imgData.fetching && ( <div className="text-center bg-secondary m-auto p-3"> <p className="m-0 text-white">Getting images</p> </div> )} <div id='page-bottom-boundary' style={{ border: '1px solid red' }} ref={bottomBoundaryRef}></div>

API 호출이 시작되면 fetchingtrue 로 설정하고 이미지 가져오기 텍스트가 표시됩니다. 완료되자마자 fetchingfalse 로 설정하고 텍스트가 숨겨집니다. 생성자 옵션 개체에서 다른 threshold 을 설정하여 경계에 정확히 도달하기 전에 API 호출을 트리거할 수도 있습니다. 끝에 있는 빨간 선을 보면 페이지 경계에 도달한 시점을 정확히 알 수 있습니다.

이때 해당 분기는 02-infinite-scroll입니다.

이제 이미지 지연 로딩을 구현합니다.

이미지 지연 로딩 구현

아래로 스크롤하면서 네트워크 탭을 검사하면 빨간색 선(하단 경계)을 누르는 즉시 API 호출이 발생하고 보기에 도달하지 않은 경우에도 모든 이미지가 로드되기 시작하는 것을 볼 수 있습니다. 그들을. 이것이 바람직한 동작이 아닐 수 있는 다양한 이유가 있습니다. 사용자가 이미지를 보고 싶어할 때까지 네트워크 호출을 저장하고 싶을 수 있습니다. 이러한 경우 이미지를 느리게 로드하도록 선택할 수 있습니다. 즉, 보기로 스크롤될 때까지 이미지를 로드하지 않습니다.

src/App.js 를 엽니다. 무한 스크롤 기능 바로 아래에 다음 코드를 입력합니다.

 // App.js // lazy loads images with intersection observer // only swap out the image source if the new url exists const imagesRef = useRef(null); const imgObserver = useCallback(node => { const intObs = new IntersectionObserver(entries => { entries.forEach(en => { if (en.intersectionRatio > 0) { const currentImg = en.target; const newImgSrc = currentImg.dataset.src; // only swap out the image source if the new url exists if (!newImgSrc) { console.error('Image source is invalid'); } else { currentImg.src = newImgSrc; } intObs.unobserve(node); // detach the observer when done } }); }) intObs.observe(node); }, []); useEffect(() => { imagesRef.current = document.querySelectorAll('.card-img-top'); if (imagesRef.current) { imagesRef.current.forEach(img => imgObserver(img)); } }, [imgObserver, imagesRef, imgData.images]);

scrollObserver 와 마찬가지로 관찰할 노드를 허용하는 imgObserver 함수를 정의합니다. en.intersectionRatio > 0 에 의해 결정된 대로 페이지가 교차점에 도달하면 요소의 이미지 소스를 교체합니다. 교체를 수행하기 전에 먼저 새 이미지 소스가 존재하는지 확인합니다. scrollObserver 함수와 마찬가지로 imgObserver를 useCallback 후크에 래핑하여 구성 요소가 계속 다시 렌더링되는 것을 방지합니다.

또한 대체 작업이 끝나면 img 요소 관찰을 중단합니다. 우리는 이것을 unobserve 하지 않는 방법으로 합니다.

다음 useEffect 후크에서 document.querySelectorAll 이 있는 페이지에서 .card-img-top 클래스의 모든 이미지를 가져옵니다. 그런 다음 각 이미지를 반복하고 관찰자를 설정합니다.

imgData.imagesuseEffect 후크의 종속성으로 추가했습니다. 이것이 변경되면 useEffect 후크가 트리거되고 차례로 imgObserver 가 각 <img className='card-img-top'> 요소와 함께 호출됩니다.

아래와 같이 <img className='card-img-top'/> 요소를 업데이트합니다.

 <img alt={author} data-src={download_url} className="card-img-top" src={'https://picsum.photos/id/870/300/300?grayscale&blur=2'} />

모든 <img className='card-img-top'/> 요소에 대한 기본 소스를 설정하고 표시하려는 이미지를 data-src 속성에 저장합니다. 기본 이미지는 일반적으로 크기가 작아서 가능한 한 적게 다운로드합니다. <img/> 요소가 표시되면 data-src 속성의 값이 기본 이미지를 대체합니다.

아래 그림에서 우리는 일부 공간에 여전히 표시되는 기본 등대 이미지를 봅니다.

이미지가 느리게 로드됩니다. (큰 미리보기)

이 시점에서 해당 분기는 03-lazy-loading입니다.

이제 이러한 모든 기능을 추상화하여 재사용할 수 있는 방법을 살펴보겠습니다.

가져오기, 무한 스크롤 및 사용자 정의 후크로 지연 로드 추상화

가져오기, 무한 스크롤 및 이미지 지연 로딩을 성공적으로 구현했습니다. 애플리케이션에 유사한 기능이 필요한 다른 구성 요소가 있을 수 있습니다. 이 경우 이러한 기능을 추상화하고 재사용할 수 있습니다. 우리가 해야 할 일은 그것들을 별도의 파일로 옮기고 필요한 곳으로 가져오기만 하면 됩니다. 우리는 그것들을 Custom Hooks로 바꾸고 싶습니다.

React 문서에서는 Custom Hook을 이름이 "use" 로 시작하고 다른 후크를 호출할 수 있는 JavaScript 함수로 정의합니다. 우리의 경우 useFetch , useInfiniteScroll , useLazyLoading 이라는 세 개의 후크를 만들고 싶습니다.

src/ 폴더 안에 파일을 생성합니다. 이름을 customHooks.js 로 지정하고 내부에 아래 코드를 붙여넣습니다.

 // customHooks.js import { useEffect, useCallback, useRef } from 'react'; // make API calls and pass the returned data via dispatch export const useFetch = (data, dispatch) => { useEffect(() => { dispatch({ type: 'FETCHING_IMAGES', fetching: true }); fetch(`https://picsum.photos/v2/list?page=${data.page}&limit=10`) .then(data => data.json()) .then(images => { dispatch({ type: 'STACK_IMAGES', images }); dispatch({ type: 'FETCHING_IMAGES', fetching: false }); }) .catch(e => { dispatch({ type: 'FETCHING_IMAGES', fetching: false }); return e; }) }, [dispatch, data.page]) } // next code block here

useFetch 후크는 디스패치 함수와 데이터 객체를 허용합니다. 디스패치 함수는 API 호출의 데이터를 App 구성 요소로 전달하는 반면 데이터 개체를 사용하면 API 끝점 URL을 업데이트할 수 있습니다.

 // infinite scrolling with intersection observer export const useInfiniteScroll = (scrollRef, dispatch) => { const scrollObserver = useCallback( node => { new IntersectionObserver(entries => { entries.forEach(en => { if (en.intersectionRatio > 0) { dispatch({ type: 'ADVANCE_PAGE' }); } }); }).observe(node); }, [dispatch] ); useEffect(() => { if (scrollRef.current) { scrollObserver(scrollRef.current); } }, [scrollObserver, scrollRef]); } // next code block here

useInfiniteScroll 후크는 scrollRefdispatch 함수를 허용합니다. scrollRef 는 우리가 구현한 섹션에서 이미 논의한 바와 같이 관찰자를 설정하는 데 도움이 됩니다. 디스패치 기능은 API 끝점 URL에서 페이지 번호를 업데이트하는 작업을 트리거하는 방법을 제공합니다.

 // lazy load images with intersection observer export const useLazyLoading = (imgSelector, items) => { const imgObserver = useCallback(node => { const intObs = new IntersectionObserver(entries => { entries.forEach(en => { if (en.intersectionRatio > 0) { const currentImg = en.target; const newImgSrc = currentImg.dataset.src; // only swap out the image source if the new url exists if (!newImgSrc) { console.error('Image source is invalid'); } else { currentImg.src = newImgSrc; } intObs.unobserve(node); // detach the observer when done } }); }) intObs.observe(node); }, []); const imagesRef = useRef(null); useEffect(() => { imagesRef.current = document.querySelectorAll(imgSelector); if (imagesRef.current) { imagesRef.current.forEach(img => imgObserver(img)); } }, [imgObserver, imagesRef, imgSelector, items]) }

useLazyLoading 후크는 선택기와 배열을 수신합니다. 선택기는 이미지를 찾는 데 사용됩니다. 배열이 변경되면 각 이미지에서 관찰자를 설정하는 useEffect 후크가 트리거됩니다.

새 파일로 추출한 src/App.js 에 있는 것과 동일한 기능임을 알 수 있습니다. 이제 좋은 점은 인수를 동적으로 전달할 수 있다는 것입니다. 이제 App 구성 요소에서 이러한 사용자 지정 후크를 사용하겠습니다.

src/App.js 를 엽니다. 사용자 정의 후크를 가져오고 데이터 가져오기, 무한 스크롤 및 이미지 지연 로딩을 위해 정의한 함수를 삭제합니다. 리듀서와 useReducer 를 사용하는 섹션은 그대로 둡니다. 아래 코드를 붙여넣습니다.

 // App.js // import custom hooks import { useFetch, useInfiniteScroll, useLazyLoading } from './customHooks' const imgReducer = (state, action) => { ... } // retain this const pageReducer = (state, action) => { ... } // retain this const [pager, pagerDispatch] = useReducer(pageReducer, { page: 0 }) // retain this const [imgData, imgDispatch] = useReducer(imgReducer,{ images:[], fetching: true }) // retain this let bottomBoundaryRef = useRef(null); useFetch(pager, imgDispatch); useLazyLoading('.card-img-top', imgData.images) useInfiniteScroll(bottomBoundaryRef, pagerDispatch); // retain the return block return ( ... )

무한 스크롤 섹션에서 이미 bottomBoundaryRef 에 대해 이야기했습니다. pager 객체와 imgDispatch 함수를 useFetch 에 전달합니다. useLazyLoading 은 클래스 이름 .card-img-top 허용합니다. 에 유의하십시오 . 클래스 이름에 포함됩니다. 이렇게 하면 document.querySelectorAll 을 지정할 필요가 없습니다. useInfiniteScrollpage 값을 증가시키기 위해 ref와 디스패치 함수를 모두 허용합니다.

이 시점에서 해당 분기는 04-custom-hooks입니다.

결론

HTML은 멋진 기능을 구현하기 위한 멋진 API를 제공하는 데 점점 더 능숙해지고 있습니다. 이 게시물에서 우리는 React 기능 구성 요소에서 교차 관찰자를 사용하는 것이 얼마나 쉬운지 보았습니다. 그 과정에서 우리는 React의 일부 hook을 사용하는 방법과 우리 자신의 hook을 작성하는 방법을 배웠습니다.

자원

  • "무한 스크롤 + 이미지 지연 로딩", Orji Chidi Matthew, GitHub
  • "무한 스크롤, 페이지 매김 또는 "더 로드" 버튼? 전자 상거래의 사용성 발견,” Christian Holst, Smashing Magazine
  • "Lorem Picsum", David Marby & Nijiko Yonskai
  • "IntersectionObserver의 등장," Surma, Web Fundamentals
  • 사용할 수 있습니까… IntersectionObserver
  • "Intersection Observer API", MDN 웹 문서
  • "구성 요소와 소품", React
  • " useCallback ," 리액트
  • " useReducer ," 리액트