Custom Hook을 이용한 무한 스크롤 구현하기

Custom Hook을 이용한 무한 스크롤 구현하기

리엑트 프로젝트에서 종종 무한 스크롤 기능을 구현할 때가 있습니다. 이를 구현하기 위해 useEffect 훅을 여러 컴포넌트에서 반복해서 사용하게 되는데, 이런 중복 코드를 제거하고 재사용성을 높이기 위해 custom hook을 만드는 방법을 안내하겠습니다.

Custom Hook 정의하기

useInfiniteScroll.ts라는 파일을 생성하고 다음과 같은 디렉토리 구조를 작성하겠습니다.

1
2
3
4
5
src/
|-- hooks/
|-- useInfiniteScroll.ts
|-- components
|-- InfiniteScrollComponent.tsx

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);
  • observerIntersection 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의 연결을 끊습니다.

InfiniteScrollComponent.tsx

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 refdiv 요소에 할당하여, 해당 요소가 화면에 나타날 때마다 setPage 함수가 호출되어 페이지가 증가합니다.
  • hasMore 상태를 적절히 사용하여 page가 마지막 페이지인지 여부를 판단하고, 추가 데이터를 가져올 수 있는지 여부를 결정합니다.

마치며

custom hook은 무한 스크롤 기능을 구현할 때 중복되는 코드를 제거하고, 재사용성을 높이는 데 도움이 됩니다.

공유하기