app

김태헌

4. React Native: 무한스크롤 구현

모바일 어플리케이션에서는 무한스크롤을 구현해야 하는 경우가 꽤 있다.

단순히 컴포넌트를 Map 함수를 통해 렌더링 해주어도 되겠으나, 데이터의 수가 불확실하다던가, 너무 많다면 한 번에 렌더링하는 것은 비효율적이고 성능에 치명적일 수 있기 때문에 FlatList 컴포넌트를 사용한다. (ScrollView도 역시 같은 문제점 존재)

또 공부하다보니 리액트쿼리의 useInfiniteQuery 와 찰떡 궁합인듯 하니, 이 둘을 어떻게 사용하는 지에 대해 기록을 남겨보고자 한다.

게시글을 가져와야 하는 상황을 가정했을 때, 흐름을 파악해보자.

InfiniteQuery + FlatList

useInfiniteQuery

일단은 게시글을 가져오는 함수를 먼저 정의한다.

const getPosts = async (page : number) => {
    const { data } = await axios.get(`/posts?page=${page}`)
    return data;
}

이때, 함수는 페이지 번호를 파라미터로 받아와야 한다. 이를 쿼리스트링으로 엔드포인트에 추가해준다. 그러므로 서버 API는 반드시 페이지번호를 받아야 한다. response 데이터 또한 페이지네이션한 데이터를 주어야할 것이다.

useInfiniteQuery를 이용해 기본 설정을 해준다. useQuery와 유사하되, 부가적인 반환값들과 옵션들이 존재한다.

const {
    data, 
    fetchNextPage, 
    hasNextPage, 
    isFetchingNextPage
    } = useInfiniteQuery({
        queryKey = ['posts'],
        queryFn : ({ pageParam }) => getPosts(pageParam),    
        getNextPageParam: (lastPage, allPages) => {
          const lastPost = lastPage[lastPage.length - 1];
          return lastPost ? allPages.length + 1 : undefined;
        }
    });

일반적으로 이렇게 생겼다. 하나 씩 살펴보자.

useInfiniteQuery 의 반환값

data.pages : 모든 페이지를 포함하는 배열 [[글1, 글2, 글3], [글4, 글5, 글6]] 형태 data.pageParams : 모든 페이지 매개변수를 포함하는 배열 fetchNextPage : 다음 페이지를 fetch 할 수 있다. (보통 onEndReached 와 연결한다.) hasNextPage : 가져올 수 있는 다음 페이지가 있는 경우 true isFetchingNextPage : fetchNextPage다음 페이지를 가져오는 동안에는 true refetch : 처음부터 다시 가져오기. (onRefresh에 연결해서 사용한다.)

useInfiniteQuery 옵션

queryKey : useQuery 와 동일하다. 쿼리의 고유 key 값이다. initialPageParam : 첫 페이지를 가져올 때 사용하는 기본 페이지 값이다. 보통 1로 쓴다. queryFn : fetch를 실행할 함수다. pageParam 라는 현재 페이지 값을 나타내는 파라미터를 받을 수 있다. getNextPageParam : 다음 페이지의 매개변수 (pageParam)를 가져오는 함수를 정의한다. -> 리턴값이 undefined | null 이면 끝이라고 인식한다. 해당 함수의 리턴 값은 pageParam이다.

getNextPageParam: (lastPage, allPages) => {
  const lastPost = lastPage[lastPage.length - 1];
  return lastPost ? allPages.length + 1 : undefined;
}

위의 코드를 자세히 살펴보자.

lastPage : fetch 해온 가장 최근의 페이지 목록이다. allPages : 현재까지 가져온 모든 페이지 목록이다. [ [1페이지 글들], [2페이지 글들] ] 형태를 가진다.

위의 코드를 해석해보면, const lastPost = lastPage[lastPage.length - 1] -> 가장최근의 페이지 목록 중 가장 마지막 게시글을 lastPost로 정의한다.

return lastPost ? allPages.length + 1 : undefined; -> 마지막 게시글이 있다면 ? 현재 페이지 수 + 1을 리턴한다. ex) 현재 2페이지이고 마지막 게시글이 존재하면, 3페이지 반환 -> 마지막 게시글이 없다면? undefined 를 리턴하므로 다음 페이지를 반환하지 않는다. -> hasNextPage == false

FlatList

FlatList 에 이제 데이터를 꽂아 넣어줄 차례이다.

리액트쿼리는 데이터를 페이지 별로 보관한다. [[1페이지], [2페이지], [3페이지]] 형식으로 저장한다. 하지만 FlatList 는 데이터로 1차원 배열만 받기 때문에 .flat() 함수를 통해 1차원 배열로 바꿔준다. ex) [[글1, 글2, 글3], [글4, 글5, 글6]] -> [글1, 글2, 글3, 글4, 글5, 글6]

// 현재 새로고침 중인지 저장
const [isRefreshing, setIsrefreshing] = useState(false);

// 밑바닥에 닿았을 때 실행할 콜백함수 정의
const handleEndReached = () => {
    // 다음 페이지가 존재하고, 다음 데이터를 불러오는 중이 아닐 때 다음 페이지 fetch
    if (hasNextPage && !isFetchingNextPage) {
      fetchNextPage();
    }
};

// 새로고침 요청 시 실행할 콜백함수 정의
const handleRefresh = async () => {
    setIsrefreshing(true); // 새로고침 상태를 true
    await refetch(); // refetch 실행
    setIsrefreshing(false); // 새로고침 상태 false
};

return (
<FlatList
  data={posts?.pages.flat()} // 데이터 1차원 배열로 차원 축소
  renderItem={({ item }) => <FeedItem post={item} />} // 렌더링 할 컴포넌트 정의 
  keyExtractor={(item) => String(item.id)} // key 값 지정
  onEndReached={handleEndReached} // 화면이 다 내려왔을 때 실행할 함수
  onEndReachedThreshold={0.5} // 화면 어느정도 내려왔을 때 end로 판별할 지
  refreshing={isRefreshing} // true 일 때, 상단에 로딩바 생성
  onRefresh={handleRefresh} // 화면을 가장 위에서 아래로 당겼을 때 실행할 함수
/>

처음 보면 조금 어질어질하다. useInfiniteQuery의 리턴값들을 FlatList의 프롭스에 잘 연결시켜 주어야 한다. 해당 사항은 자주 보면서 눈에 익히는 것이 더 좋은 것 같다.

첫 페이지의 글 리스트 컴포넌트가 가장 아래에 닿으면 다음 페이지를 로딩해 더 스크롤 할 수 있으며, 화면을 위에서 아래로 당겼을 시, 새로고침 표시와 함께 새로고침이 가능하도록 하는 코드이다.

2026년 3월 29일 AM 11:29

댓글 닫기
댓글이 없습니다.
로그인 필요