코딩 노트

React09 - 무한스크롤 본문

React

React09 - 무한스크롤

newbyeol 2023. 11. 7. 15:41

무한스크롤 

App.js에 구문 추가

              <Route path="/book2" element={<BookInfinite/>}></Route>
 

export default App;

Menu.js에 구문 추가

<li className="nav-item">
                                <NavLink className={`nav-link ${location.pathname === '/book2' ? 'active' : ''}`} to="/book2">도서</NavLink>
                            </li>

최초에 보여줄 데이터 개수가 필요하다.

 

최초에 보여줄 데이터 개수 30개

스크롤이 아래로 내려가면 추가 30개(반복)

bookInfinite.js 생성 후 구문 추가

import { useState } from "react";

const BookInfinite = (props)=>{

    const [bookList, setBookList] = useState([]);
   

    return (
        <>
            <div className="row">
                <div className="col">
            <       h1>무한 스크롤 예제</h1>
                </div>
            </div>

            <div className="row mt-4">
                <div className="col">
                    <table className="table">
                        <thead>
                            <tr>
                                <th>제목</th>
                                <th>저자</th>
                                <th>출판사</th>
                            </tr>
                        </thead>
                        <tbody>

                        </tbody>
                    </table>
                </div>
            </div>
        </>
    );
};
export default BookInfinite;

 

백엔드로 넘어와서

 

BookRestController에 구문 추가

//프론트엔드에서 페이지번호, 데이터 개수를 보낼 경우의 조회 매핑

@GetMapping("/page/{page}/size/{size}")

public List<BookDto> listByPage(@PathVariable int page, @PathVariable int size) {

return bookDao.selectListByPage(page, size);

}

BookDao에 구문 추가

List<BookDto> selectListByPage(int page, int size);

BookDaoImpl에 구문 추가

@Override

public List<BookDto> selectListByPage(int page, int size) {

int end = page * size;

int begin = end - (size-1);

Map params = Map.of("begin", begin, "end", end);

return sqlSession.selectList("book.selectListByPage", params);

}

book-mapper.xml에 구문 추가

<!-- 페이징 목록 -->

<select id="selectListByPage" resultType="BookDto">

select * from (

select rownum rn, TMP.* from (

select * from book order by book_id asc

)TMP

) where rn between #{begin} and #{end}

</select>

bookInfinite.js에 구문 추가

import axios from "axios";
import { useEffect, useState } from "react";

const BookInfinite = (props)=>{
    const [page, setPage] = useState(1);
    const [size, setSize] = useState(30);
    const [bookList, setBookList] = useState([]);

    const loadBook = ()=> {
        axios({
            url:`${process.env.REACT_APP_REST_API_URL}/book/page/${page}/size/${size}`,
            method:"get"
        })
        .then(response=>{
            //setBookList(response.data);//덮어쓰기
            setBookList([...bookList, ...response.data]);//spread 연산자
            //setBookList(bookList.concat(...response.data));//concat 함수
        });
    };
    useEffect(()=>{
        loadBook();
    }, [page]);

    //다음페이지
    const nextPage = ()=>{
        setPage(page+1);//페이지 1 증가
    };

    //개수가 변하면 페이지를 1로, 목록을 모두 지우고 다시 불러와야 한다
    useEffect(()=>{
        setPage(1);
        setBookList([]);
        //loadBook();
    }, [size]);

    return (
        <>
            <div className="row">
                <div className="col">
                    <h1>무한 스크롤 예제</h1>
                </div>
            </div>

            <div className="row mt-4">
                <div className="col-2 offset-10">
                    <select value={size} onChange={e=>setSize(e.target.value)}>
                        <option value="20">20개씩 보기</option>
                        <option value="30">30개씩 보기</option>
                        <option value="50">50개씩 보기</option>
                        <option value="100">100개씩 보기</option>
                    </select>
                </div>
            </div>

            <div className="row mt-4">
                <div className="col">
                    <table className="table">
                        <thead>
                            <tr>
                                <th>번호</th>
                                <th>제목</th>
                                <th>저자</th>
                                <th>출판사</th>
                            </tr>
                        </thead>
                        <tbody>
                            {bookList.map(book=>(
                            <tr key={book.bookId}>
                                <td>{book.bookId}</td>
                                <td>{book.bookTitle}</td>
                                <td>{book.bookAuthor}</td>
                                <td>{book.bookPublisher}</td>
                            </tr>
                            ))}
                        </tbody>
                    </table>
                </div>

                {/* 더보기 버튼 */}
               
                <div className="row mt-2">
                    <div className="col">
                        <button className="btn btn-primary w-100" onClick={nextPage}>
                            {size}개 더보기
                        </button>
                    </div>
                </div>
               
               
            </div>
        </>
    );
};
export default BookInfinite;

 

npm install lodash : throttle, debounce, 배열 셔플을 해주는 라이브러리

throttle: 발생 주기 조절, 발생 시간 조절 가능

debounce: 제일 마지막 것만 이벤트 실행 가능하게 함

설치 후 임포트를 해야한다.

import throttle from "lodash/throttle"; //특정 함수의 발생 주기를 설정(강제 성능 하향)
//사용법 - throttle(원래 쓰려고 했던 함수, 주기)
 
//import debounce from "lodash/debounce"; //특정 이벤트의 마지막만 실행하도록 설정(강제 성능 하향), (ex: 아이디 중복 검사)
//사용법 - debounce(원래 쓰려고 했던 함수, 주기)

무한 스크롤 구현

bookInfinite.js에 구문 추가

import axios from "axios";
import { useEffect, useRef, useState } from "react";
import throttle from "lodash/throttle";//특정 함수의 발생 주기를 설정(강제 성능 하향)
//사용법 - throttle(원래쓰려고했던함수, 주기)
//import debounce from "lodash/debounce";//특정 이벤트의 마지막만 실행하도록 설정
//사용법 - debounce(원래쓰려고했던함수, 주기)

const BookInfinite = (props)=>{
    const [page, setPage] = useState(1);
    const [size, setSize] = useState(30);
    const [bookList, setBookList] = useState([]);

    //ref를 이용해서 flag 변수를 생성(화면과는 무관)
    const loading = useRef(false);
    const loadBook = ()=> {
        loading.current = true;//로딩중으로 설정(state가 아닌 ref인 이유는 순서 보장)
        axios({
            url:`${process.env.REACT_APP_REST_API_URL}/book/page/${page}/size/${size}`,
            method:"get"
        })
        .then(response=>{
            //setBookList(response.data);//덮어쓰기
            setBookList([...bookList, ...response.data]);//spread 연산자
            //setBookList(bookList.concat(...response.data));//concat 함수
            loading.current = false;//로딩완료로 설정(순서 보장)
        });
    };
    useEffect(()=>{
        loadBook();
    }, [page]);

    //다음페이지
    const nextPage = ()=>{
        setPage(page+1);//페이지 1 증가
    };

    //개수가 변하면 페이지를 1로, 목록을 모두 지우고 다시 불러와야 한다
    useEffect(()=>{
        setPage(1);
        setBookList([]);
        //loadBook();
    }, [size]);

    ////////////////////////////////////////////////////////////////////////////
    // 스크롤 이벤트 처리(scroll event handling)
    // - 화면이 실행되면 window에 scroll 이벤트 설정
    // - 화면이 해제되기 직전에 이벤트 해제
    ////////////////////////////////////////////////////////////////////////////
    useEffect(()=>{
        if(loading.current === true) {//로딩이 진행중인 상황
            return;//그만해
        }

        //화면 생성 시 할 작업
        const listener = throttle(()=>{
            //console.log("스크롤이 굴러간다!");
            const percent = calculateScrollPercentage();
            console.log("퍼센트 = " + percent);
            if(percent >= 60) {
                nextPage();
            }
        }, 750);
        window.addEventListener("scroll", listener);
        console.log("걸렸다");

        //화면 해제 시 할 작업
        return ()=>{
            window.removeEventListener("scroll", listener);
            console.log("풀렸다");
        };
    });

    //스크롤 퍼센트 구하는 함수(Feat.GPT)
    const calculateScrollPercentage = ()=>{
        // 현재 스크롤 위치
        var scrollPosition = window.scrollY || window.pageYOffset || document.documentElement.scrollTop;
     
        // 문서의 전체 높이
        var documentHeight = Math.max(
          document.body.scrollHeight,
          document.body.offsetHeight,
          document.documentElement.clientHeight,
          document.documentElement.scrollHeight,
          document.documentElement.offsetHeight
        );
     
        // 브라우저의 뷰포트 높이
        var windowHeight = window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight;
     
        // 스크롤 퍼센트 계산
        var scrollPercentage = (scrollPosition / (documentHeight - windowHeight)) * 100;
     
        return scrollPercentage;
      }

    return (
        <>
            <div className="row">
                <div className="col">
                    <h1>무한 스크롤 예제</h1>
                </div>
            </div>

            <div className="row mt-4">
                <div className="col-2 offset-10">
                    <select value={size} onChange={e=>setSize(e.target.value)}>
                        <option value="20">20개씩 보기</option>
                        <option value="30">30개씩 보기</option>
                        <option value="50">50개씩 보기</option>
                        <option value="100">100개씩 보기</option>
                    </select>
                </div>
            </div>

            <div className="row mt-4">
                <div className="col">
                    <table className="table">
                        <thead>
                            <tr>
                                <th>번호</th>
                                <th>제목</th>
                                <th>저자</th>
                                <th>출판사</th>
                            </tr>
                        </thead>
                        <tbody>
                            {bookList.map(book=>(
                            <tr key={book.bookId}>
                                <td>{book.bookId}</td>
                                <td>{book.bookTitle}</td>
                                <td>{book.bookAuthor}</td>
                                <td>{book.bookPublisher}</td>
                            </tr>
                            ))}
                        </tbody>
                    </table>
                </div>

                {/* 더보기 버튼 */}
               
                <div className="row mt-2">
                    <div className="col">
                        <button className="btn btn-primary w-100" onClick={nextPage}>
                            {size}개 더보기
                        </button>
                    </div>
                </div>
               
               
            </div>
        </>
    );
};
export default BookInfinite;