티스토리 뷰

반응형

안녕하세요 오늘은 무한스크롤 데이터를 다루는 InfinityList 컴포넌트를 생성한 경험을 소개해보고자 합니다.

 

리액트 네이티브의 FlatList 를 아시나요? 

 

리액트 네이티브의 FlatList는 데이터가 많을 때 사용되는 컴포넌트로, 화면에 보여지는 부분만 렌더링하는 기능을 제공합니다. 이 컴포넌트를 사용하면 대규모 데이터 목록을 효율적으로 처리할 수 있습니다. 이전 글에서는 리액트 네이티브의 FlatList와 무한스크롤 API를 React Query를 활용하여 InfinityFlatList 컴포넌트를 만드는 과정에 대해 다룬 바 있습니다.

 

 

[React Native] React Native에서 useInfiniteQuery를 사용하여 무한 스크롤 구현하기: InfinityFlatList

안녕하세요. 오늘은 React Native의 FlatList를 통한 무한스크롤을 구현하는 방법을 소개하고자 합니다. React Native에서 제공하는 FlatList에서 무한 스크롤 기능을 갖춘 InfinityFlatList 컴포넌트를 만들었

algoroot.tistory.com

 

그 당시 동료 팀원분이 InfinityFlatList를 사용하면서 "웹에서도 이런 컴포넌트가 있으면 좋겠다"라는 의견을 제시해주셨습니다. 시간이 조금 지났지만, 웹에서도 활용할 수 있는 InfinityList 컴포넌트를 구현하게 되었습니다. 

 

구현하는 것은 어렵지 않았지만, 사용성이 편리하고, 추상화가 잘되어 글에서도 꼭 다뤄야겠다 싶었어요.

 

 


배경지식

InfinityList를 살펴보기 전, 무한스크롤, 페이지네이션에 대한 이해가 있어야합니다. 간략히 예시를 들어보겠습니다.

 

limit을 주면 count, cursor, results를 반환해주는 API가 있습니다. 

  • limit : 한 페이지에 가져올 데이터 수
  • count: 총 데이터의 수 
  • cursor: 다음 페이지의 키 값
  • results: 한 페이지에 해당하는 데이터 배열 

 

Request Url

{BASE_URL}/book/?limit=10


Response type

 

export interface PaginatedSomeListType {
  count?: number;
  cursor?: string | null;
  results?: EpListType[];
}

 

반환 값으로 오는 cursor는 null이 오기 전까지는 렌더링할 다음 페이지의 데이터가 있다는 뜻입니다.

저는 react query의 useInfiniteQuery 와 화면 요소를 관찰하는 Intersection observer API를 사용하여 cursor가 null 이면 hasNextPage가 false로 반환되어 스크롤을 하더라도 데이터 패칭을 하지 않게 사용하고 있습니다.  

const { data, hasNextPage, isFetchingNextPage, fetchNextPage } =
	useSomeListInfiniteQuery({
      variables: { query: { limit: 10 } },
});

 

 

 


InfinityFlatList 소개 

 

주요 특징

1. 데이터 로딩의 간편함

InfinityList 컴포넌트를 사용하면 데이터를 무한 스크롤에 맞게 로딩하는 데 필요한 로직을 간소화할 수 있습니다. 데이터가 스크롤에 도달하면 추가 데이터를 로드하고 화면에 표시할 수 있습니다.

2. 사용성 뛰어남

InfinityList는 데이터 렌더링에 사용자 친화적이며, 데이터가 없을 때 표시할 컴포넌트나 로딩 중인 경우 스피너와 같은 UI를 손쉽게 지정할 수 있습니다.

 

 

어떻게 사용하나요?

InfinityList 컴포넌트를 사용하는 방법은 간단합니다. 데이터 배열과 데이터 항목을 렌더링하는 함수를 제공하고, 무한 스크롤이 가능한지 여부, 데이터 로딩 중인지 여부, 스크롤 이벤트 감지 옵션, 더 많은 데이터를 가져오는 함수 등을 설정할 수 있습니다

  • data: InfinityList가 표시할 데이터 배열
  • renderItem: 각 데이터 항목을 렌더링하는 함수 JSX.Element를 리턴합니다.
  • hasMore: 무한 스크롤이 가능한지 여부를 나타내는 불리언 값
  • isFetching: 데이터를 로딩 중인지 나타내는 불리언 값
  • options: 스크롤 이벤트 감지 옵션을 설정하는 객체
  • onFetchMore: 더 많은 데이터를 가져오기 위한 콜백 함수
  • ListEmptyComponent 프로퍼티: 데이터가 없을 때 표시할 컴포넌트
<InfinityList
  data={data}
  renderItem={renderItem}
  hasMore={hasMore}
  isFetching={isFetching}
  options={options}
  onFetchMore={onFetchMore}
  ListEmptyComponent={<div>No data available</div>}
/>

 

renderItem 에는 렌더링할 아이템 카드를 넣어 사용할 수 있습니다. 

const SomePage = ({ ...basisProps }: SomePageProps) => {
  const { data, hasNextPage, isFetchingNextPage, fetchNextPage } =
    useSomeListInfiniteQuery({
      variables: { query: { limit: 10 } },
    });
  return (
    <LoadingView isLoading={isLoading} fallback={<SomeCard.LoadingView />}>
      <InfinityList
        data={data}
        hasMore={!!hasNextPage}
        isFetching={isFetchingNextPage}
        onFetchMore={fetchNextPage}
        renderItem={({ item, index }) => (
          <SomeCard
            key={index}
            data={item}
            onCancel={() => cancel.show(item.paymentId)}
          />
        )}
        ListEmptyComponent={<div>No data available</div>}
      />
    </LoadingView>
  );
};

export default SomePage;

 


InfinityList 세부 구현 사항

 

Properties

InfinityList 컴포넌트의 property type입니다. 렌더링할 아이템의 타입을 제네릭<T> 로 받으며 VirtualizedListProps 타입을 extends합니다 .

interface InfinityListProps<T> extends VirtualizedListProps {
  data?: ReadonlyArray<T>;
  renderItem?: ListRenderItem<T>;

 

renderItem은 함수입니다. 하나의 아이템은 맵핑될 아이템들이기 때문에 index와 아이템 타입인 item을 받아 ReactNode를 리턴합니다. 

// ./types
export type ListRenderItemInfo<T> = {
  item: T;
  index: number;
};
export type ListRenderItem<T> = (
  info: ListRenderItemInfo<T>,
) => ReactNode | null;

 

무한 데이터 렌더링에 필요한 추가적인 속성들입니다. 

// ./types
export type VirtualizedListProps = {
  hasMore: boolean;
  isFetching: boolean;
  onFetchMore: () => void;
  options?: IntersectionObserverInit;
  ListEmptyComponent?: ReactNode;
};

 

 

 

관찰자 설정 (intersection observer)

무한 데이터의 한정된 데이터를 렌더링 하기 위해서는 화면에 보여지는 지에 대한 관찰자를 설정해야합니다. 

intersectionObserver api를 사용했는데요. 사용경험이 익숙하지 않으신 분들은 관련한 글이 있으니 보고 오시는 것을 추천합니다. 

 

useIntersectionObserver hook은 IntersectionObserver API을 편리하게 사용하기 위해 만든 hook입니다. targetRef를 넘기고, 욧ref 요소가 화면에 보여졌을 때 callback 함수를 실행해줍니다. 
 
  • 불러올 다음 데이터가 있을 때
  • 데이터 패칭중이 아닐 때

다음 데이터를 불러오는 onFetchMore함수를 실행시킵니다 .

 

 

  const { targetRef: bottomRef } = useIntersectionObserver({
    callback: () => {
      if (!isFetching && hasMore) {
        onFetchMore();
      }
    },
    options: {
      root: options?.root || null,
      rootMargin: options?.rootMargin || '0px',
      threshold: options?.threshold || 0.5,
    },
  });

targetRef는 View 데이터의 가장 하단에 위치한 Box(div) 컴포넌트에 등록하고 패칭중일 때는 스피너를 보여줘 로딩처리도 함께 수행해줍니다. 

<Box ref={bottomRef}>
    {isFetching && (
      <Center>
        <Spinner size={'sm'} />
      </Center>
    )}
</Box>

 

데이터 렌더링 (renderItem)

전달한 renderItem 함수는 맵핑되어 그대로 렌더링시켜줍니다. 

  {data?.map((item, index) => (
     <Box key={index}>{renderItem?.({ item, index })}</Box>
  ))}

 

빈 데이터 처리 (ListEmptyComponent)

data가 비어있는 경우를 체크해줍니다. 

  if (isEmpty(data))
    return (
      <Center w={'100%'} flex={1}>
        {ListEmptyComponent}
      </Center>
  );

 

 


 

종합

InfinityList를 직접 만들고 사용해보니 페이지 단에서 반복되어 사용되던 불필요한 로직이 제거되고 코드의 복잡성이 간소화되어 사용성이 굉장히 좋다는 생각이 들었습니다. 컴포넌트가 잘 추상화되었기 때문에 제가 예시로 든 react-query와의 조합이 아니더라도 필요한 속성만 넘기면 유용하게 사용하실 수 있을 것입니다. 

반응형