Infinite scrolling is a web-design technique that loads content continuously as the user scrolls down the page, eliminating the need for pagination.
Infinite Scroll을 구현하는 방법은 크게 아래의 세가지로 나뉜다.(라이브러리 제외)
1. Scroll Event
2. IntersectionObserver
3. useRef
scroll
이벤트는, 세가지 방법중 그닥 추천이 되는 방법은 아니다. 유저의 scrolling에 따라서 이벤트가 굉장히 빈번하게 발생하기 때문에 throttle
와 같은 라이브러리를 통한 성능 최적화가 꼭 필요해보인다.
가장 보편적으로 사용되는 방법이 바로 IntersectionObserver
인 것 같다. list
요소의 가장 아래에 빈 div
을 생성하고, ref
을 달아준다. 이 ref
를 통해서 교차시점을 확인할 수 있다.
polyfill
필요useFetch
커스텀 훅 생성
// useFetch.js
import { useState, useEffect, useCallback } from "react";
import axios from "axios";
function useFetch(query, page) {
const [loading, setLoading] = useState(true);
const [error, setError] = useState(false);
const [list, setList] = useState([]);
const sendQuery = useCallback(async () => {
try {
await setLoading(true);
await setError(false);
const res = await axios.get(url);
await setList((prev) => [...prev, ...res.data];
setLoading(false);
} catch (err) {
setError(err);
}
}, [query, page]);
useEffect(() => {
sendQuery(query);
}, [query, sendQuery, page]);
return { loading, error, list };
}
export default useFetch;
useFetch
커스텀 훅과 함께 infinite scroll 구현
import useFetch from "hooks/useFetch";
function App() {
const [query, setQuery] = useState("");
const [page, setPage] = useState(1);
const { loading, error, list } = useFetch(query, page);
const loader = useRef(null);
const handleChange = (e) => {
setQuery(e.target.value);
};
const handleObserver = useCallback((entries) => {
const target = entries[0];
if (target.isIntersecting) {
setPage((prev) => prev + 1);
}
}, []);
useEffect(() => {
const option = {
root: null,
rootMargin: "20px",
threshold: 0
};
const observer = new IntersectionObserver(handleObserver, option);
if (loader.current) observer.observe(loader.current);
}, [handleObserver]);
return (
<div className="App">
<h1>Infinite Scroll</h1>
<h2>with IntersectionObserver</h2>
<input type="text" value={query} onChange={handleChange} />
<div>
{list.map((item, i) => (
<div key={i}>{item}</div>
))}
</div>
{loading && <p>Loading...</p>}
{error && <p>Error!</p>}
<div ref={loader} />
</div>
);
}
export default App;
유튜브에서 infinite scroll 검색하다가 발견한 useRef
를 사용하는 방법이다. (참고 영상) list
의 마지막 요소에만 선택적으로 ref
를 달아주고, ref
가 있을 때, 새롭게 데이터를 fetch
한다.
참고
set
을 사용하면 중복된 값을 제거할 수 있다.(유니크한 값만 반환)
setBooks((prev) => {
return [...new Set([...prev, ...res.data)])];
});
useFetch
커스텀 훅 생성
// useFetch.js
import React, { useState, useEffect } from "react";
import axios from "axios";
function useFetch(query, page) {
const [loading, setLoading] = useState(true);
const [error, setError] = useState(false);
const [list, setList] = useState([]);
const [hasMore, setHasMore] = useState(false);
const sendQuery = useCallback(async () => {
try {
await setLoading(true);
await setError(false);
const res = await axios.get(url);
await setList((prev) => [...new Set([...prev, ...res.data))];
await setHasMore(res.data.docs.length > 0);
setLoading(false);
} catch (err) {
setError(err);
}
}, [query, page]);
useEffect(() => {
sendQuery(query);
}, [query, sendQuery, page]);
return { isLoading, error, books, hasMore };
}
export default useFetch;
useFetch
커스텀 훅과 함께 infinite scroll 구현
import useFetch from "hooks/useFetch";
function App() {
const [query, setQuery] = useState("");
const [pageNum, setPageNum] = useState(1);
const { loading, error, list, hasMore } = useSearchBook(query, pageNum);
const observer = useRef(); // (*)
const lastBookElementRef = useCallback( // (*)
(node) => {
if (isLoading) return;
if (observer.current) observer.current.disconnect();
observer.current = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting && hasMore) {
setPageNum((prev) => prev + 1);
}
});
if (node) observer.current.observe(node);
},
[loading, hasMore]
);
const handleChange = (e) => {
setQuery(e.target.value);
setPageNum(1);
};
return (
<div className="App">
<h1>Infinite Scroll</h1>
<h2>with useRef</h2>
<input type="text" onChange={handleChange} value={query} />
{list.map((item, i) => {
const isLastElement = books.length === i + 1;
isLastElement ? (
<div key={i} ref={lastBookElementRef}>
{book}
</div>
) : (
<div key={i}>{book}</div>
)
})}
<div>{isLoading && "Loading..."}</div>
<div>{error && "Error..."}</div>
</div>
);
export default App;
잘봤습니다!