App.js

import React, { useRef, useState, useCallback } from "react";
import Counter from "./component/Counter";
import useBookSearch from "./component/useBookSearch";

function App() {
  const [query, setQuery] = useState("");  // input 입력 쿼리
  const [pageNumber, setPageNumber] = useState(1);  // 페이지 넘버.
  const { loading, error, books, hasMore } = useBookSearch(query, pageNumber);

  const observer = useRef();
  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)  // 새 page를 더
        }
      });
      if (node) observer.current.observe(node); // <div>마지막 책</div>을 계속 observe한다.
    },
    [loading, hasMore]  // loading과 hasMore가 바뀌는 경우에만 이 함수 실행
  );

  /* input에 쿼리를 입력하여 조사할 때. */
  function handleSearch(e) {
    setQuery(e.target.value);
    setPageNumber(1);  // 기존 쿼리에서 새로 쿼리를 검색할 때 맨 첫 페이지부터 보여주어야 하므로.
  }

  useBookSearch(query, pageNumber);

  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>
    </>
  );
}

export default App;

useBookSearch.js

import { useEffect, useState } from "react";
import axios from "axios";
import { computeHeadingLevel } from "@testing-library/react";

function useBookSearch(query, pageNumber) {
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(false);
  const [books, setBooks] = useState([]);
  const [hasMore, setHasMore] = useState(false);

/* 쿼리에 변화가 있으면 이전 쿼리의 검색 목록 아예 지워야 한다. */
  useEffect(() => {
    setBooks([]);
  }, [query]);

  useEffect(() => {
    setLoading(true);
    setError(false);
    let cancel;
    axios({
      method: "GET",
      url: "<http://openlibrary.org/search.json>",
      params: { q: query, page: pageNumber }, // page = res.data.start와 같다.
      cancelToken: new axios.CancelToken((c) => (cancel = c)),  // 
    })
      .then((res) => {
        console.log(res.data)
        setBooks((prevBooks) => {
          return [...new Set([...prevBooks, ...res.data.docs.map((b) => b.title)])];
        });
        console.log("res.data.docs.length", res.data.docs.length)
        setHasMore(res.data.docs.length > 0);
        setLoading(false);
      })
      .catch((e) => {
        if (axios.isCancel(e)) return;
        setError(true);
      });
    return () => {
      console.log("종료")
      cancel();
    };
  }, [query, pageNumber]);
  return { loading, error, books, hasMore };
}

export default useBookSearch;