오늘은 지난시간과 다르게 react로 무한스크롤을 구현해보자.
소스코드는 다음과 같다
import axios from "axios";
import { useState, useEffect } from "react";
const useBookSearch = (query, pageNumber) => {
const [loading, setLoading] = useState(false);
const [error, setError] = useState(false);
const [hasMore, setHasMore] = useState(false);
const [books, setBooks] = useState([]);
useEffect(() => {
setBooks([]);
}, [query]); //쿼리가 바뀔때마다 빈배열로 초기화 하고
useEffect(() => {
//여기서 데이터 패칭 다시 하는거
setLoading(true);
setError(false);
let cancel;
axios
.get("http://openlibrary.org/search.json", {
params: { q: query, page: pageNumber },
cancelToken: new axios.CancelToken((c) => (cancel = c)),
})
.then((res) => {
setBooks((prevBooks) => {
//업데이트함수
return [
...new Set([
//중복제거
...prevBooks, //업데이트함수의 초기값
...res.data.docs.map((book) => book.title), //내용을 추가하는거
]),
];
});
setHasMore(res.data.docs.length > 0);
setLoading(false);
})
.catch((e) => {
if (axios.isCancel(e)) return;
setError(true);
});
return () => cancel();
}, [query, pageNumber]);
return { loading, error, books, hasMore };
};
export default useBookSearch;
위와같이 커스텀 훅을 사용해 만들었다.
axios를 통해 데이터를 패칭해오고 여러가지 상태를 만들었다.
로딩,에러,더 보여줄건지,데이터를 담아놓을 공간을 말이다.
먼저 첫번째 useEffect의 의존성배열안에 쿼리를 넣어두었다 . 눈치가 빠른 사람들은 알아차렸을것이다.
바로 저 커스텀 훅을 상위 컴포넌트에서 호출하고 그 함수가 차례대로 진행되게끔 , 또한 쿼리랑 pageNumber를 전달해 상태를 변경시켜 useEffect 훅을 반복적으로 호출하게 만들것이란걸 말이다.
위의 axios의 캔슬토큰 만들기는 내 이전 블로그를 참고하길 바란다.(캔슬토큰이란?)axios요청을 멈추기위한 개념이라고 보면 된다.
그리고 then문에서 res를 받아 업데이트 함수를통해 데이터를 업데이트해준다. state가 바뀌었으니 컴포넌트를 호출할것이고 마운트 언마운트가 계속 이루어지면서 불필요하게 데이터 패칭을 하는것을 막기위해 쿼리가 바뀔때 즉 언마운트 되기 직전에 클린업 함수를 호출하게된다.
클린업의 개념은 내 이전 블로그 useEffect편을 보고 예제코드를 반드시 연습해보길 바란다. 또한 new Set을 통해 중복을 제거한다.
hasMore의 내용은 사실 코드구문을 봤을때 응답이 성공적으로 이루어지고 쿼리를 통한 검색 즉 res.data.docs(제목임).length가 0보다 클때 즉 데이터를 받아오기만 하면 truthy한 값을 내뱉어 계속 반복적으로 데이터를 패칭하겠다는 이야기다.
import React, { useState, useRef, useCallback } from "react";
import useBookSearch from "./useBookSearch";
export default function App() {
const [query, setQuery] = useState("");
const [pageNumber, setPageNumber] = useState(1);
const { books, hasMore, loading, error } = useBookSearch(query, pageNumber);
//구조분해하면서 //함수호출
const observer = useRef(); //빈값으로 남겨둠 그리고 이후에 데이터를 넣어줄거임 Current로
const lastBookElementRef = useCallback(
(node) => {
if (loading) return; //로딩중이면 아직받아오지않고 종료
if (observer.current) observer.current.disconnect(); //커런트가 없기때문에 이 구문은 통과함 하지만 커런트가 생기고
//마지막 요소가 잡히면 다음 인터섹팅이 이루어질때 삭제함
observer.current = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting && hasMore) {
setPageNumber((prevPageNumber) => prevPageNumber + 1);
} //위코드는 현재 entries[0]을 감시하고 hasMore(데이터를 받아왔다면)
//페이지넘버를 업데이트 함수로갱신하고
});
if (node) observer.current.observe(node); //노드를 추적한다 . 즉 마지막 요소를 추적함
//추적이 이미 끝난 녀석들은 위 useCallback이 다시 호출될때
//if (observer.current) observer.current.disconnect();구문을 통해 추적되지않음
},
[loading, hasMore] //로딩중이고 hasMore은 데이터를 받아오기만 하면 무조건 true가 되게 설계됨
);
//함수자체가 무겁기때문에 콜백으로 메모이제이션 가능하게하고
// loading, hasMore가 바뀔때마다 계속
function handleSearch(e) {
setQuery(e.target.value);
setPageNumber(1);
}
return (
<>
<input type="text" value={query} onChange={handleSearch}></input>
{books.map((book, index) => {
if (books.length === index + 1) { //마지막요소의 인덱스번호를 찾는거
return (
<div ref={lastBookElementRef} key={book}>
{book}
</div>
);
} else {
return <div key={book}>{book}</div>; //일반요소
}
})}
<div>{loading && "Loading..."}</div>
<div>{error && "Error"}</div>
</>
);
}
app컴포넌트는 위에서 말했듯이 useBookSearch를 query,pageNumber를 넣어 호출한다.
위 주석을 보고 한 줄 한 줄 이해해보고 이전시간에 포스팅한 바닐라js로 무한스크롤 만들기 편을 꼭 보고 오길 바란다.
react에서의 무한스크롤은 생각보다 난이도가 쉽지많은 않다.
필연적으로 useEffect 클린업 , 업데이트함수의 개념을 알아야하고
ref로 dom을 잡는것도 필요하다.
또한 IntersectionObserver의 기본적인 동작원리와 여러 상태를 useState로 관리해 적절한 시기에 setState를 통해 상태를 변경시켜주어야한다.