Custom Hook을 이용한 무한 스크롤 구현하기
리엑트 프로젝트에서 종종 무한 스크롤
기능을 구현할 때가 있습니다. 이를 구현하기 위해 useEffect
훅을 여러 컴포넌트에서 반복해서 사용하게 되는데, 이런 중복 코드
를 제거하고 재사용성을 높이기 위해 custom hook
을 만드는 방법을 안내하겠습니다.
Custom Hook 정의하기
useInfiniteScroll.ts
라는 파일을 생성하고 다음과 같은 디렉토리 구조를 작성하겠습니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| import { useEffect, useRef } from "react";
interface UseInfiniteScrollProps { loading: boolean; hasMore: boolean; setPage: (callback: (prevPage: number) => number) => void; }
const useInfiniteScroll = ({ loading, hasMore, setPage, }: UseInfiniteScrollProps) => { const observer = useRef<IntersectionObserver | null>(null); const loader = useRef<HTMLDivElement | null>(null);
useEffect(() => { if (loading) return; if (observer.current) observer.current.disconnect();
observer.current = new IntersectionObserver((entries) => { if (entries[0].isIntersecting && hasMore) { setPage((prevPage) => prevPage + 1); } });
if (loader.current) observer.current.observe(loader.current);
return () => { if (observer.current) observer.current.disconnect(); }; }, [loading, hasMore, setPage]);
return { loader }; };
export default useInfiniteScroll;
|
1 2 3 4 5
| interface UseInfiniteScrollProps { loading: boolean; hasMore: boolean; setPage: (callback: (prevPage: number) => number) => void; }
|
UseInfiniteScrollProps
인터페이스는 useInfiniteScroll
hook
이 받는 세 가지 props
를 정의합니다.
loading
: 현재 데이터를 로딩 중인지 여부를 나타내는 boolean
값.
hasMore
: 추가 데이터를 가져올 수 있는지 여부를 나타내는 boolean
값.
setPage
: 페이지 번호를 증가시키는 함수.
1 2
| const observer = useRef<IntersectionObserver | null>(null); const loader = useRef<HTMLDivElement | null>(null);
|
observer
는 Intersection Observer
객체를 저장하는 ref
입니다.
loader
는 관찰할 DOM 요소
를 저장하는 ref
입니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| useEffect(() => { if (loading) return; if (observer.current) observer.current.disconnect();
observer.current = new IntersectionObserver((entries) => { if (entries[0].isIntersecting && hasMore) { setPage((prevPage) => prevPage + 1); } });
if (loader.current) observer.current.observe(loader.current);
return () => { if (observer.current) observer.current.disconnect(); }; }, [loading, hasMore, setPage]);
|
useEffect
훅은 loading
, hasMore
, setPage
가 변경될 때마다 실행됩니다.
if (observer.current) observer.current.disconnect();
: 기존의 observer
가 존재한다면, 연결을 끊습니다.
observer.current = new IntersectionObserver((entries) => { ... })
: 새로운 Intersection Observer
를 생성합니다.
entries[0].isIntersecting && hasMore
: loader
요소가 화면에 보이고, 추가 데이터를 가져올 수 있는 경우 setPage
함수를 호출하여 페이지 번호를 증가시킵니다.
if (loader.current) observer.current.observe(loader.current);
: loader ref
가 가리키는 DOM 요소를 관찰합니다.
return () => { if (observer.current) observer.current.disconnect(); }
: cleanup
함수로 컴포넌트가 언마운트되거나 업데이트되기 전에 observer
의 연결을 끊습니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| import React, { useState } from "react"; import useInfiniteScroll from "../hooks/useInfiniteScroll";
const InfiniteScrollComponent: React.FC = () => { const [loading, setLoading] = useState(false); const [hasMore, setHasMore] = useState(true); const [page, setPage] = useState(1);
const { loader } = useInfiniteScroll({ loading, hasMore, setPage });
return ( <div> {} <div ref={loader} /> </div> ); };
export default InfiniteScrollComponent;
|
useInfiniteScroll
hook
을 사용하여 loader ref
를 받아옵니다.
loading
, hasMore
, setPage
등의 상태를 정의하고, hook
에 전달합니다.
loader
ref
를 div
요소에 할당하여, 해당 요소가 화면에 나타날 때마다 setPage
함수가 호출되어 페이지가 증가합니다.
hasMore
상태를 적절히 사용하여 page
가 마지막 페이지인지 여부를 판단하고, 추가 데이터를 가져올 수 있는지 여부를 결정합니다.
마치며
이 custom hook
은 무한 스크롤 기능을 구현할 때 중복되는 코드를 제거하고, 재사용성을 높이는 데 도움이 됩니다.