거의 알고리즘 일기장

React Native를 이용한 사이드 프로젝트 만들기 -12. react-query를 이용하여 무한스크롤 기능 개선하다! (with 오늘의 그림일기) 본문

react-native

React Native를 이용한 사이드 프로젝트 만들기 -12. react-query를 이용하여 무한스크롤 기능 개선하다! (with 오늘의 그림일기)

건우권 2023. 8. 10. 20:52

왜 개선하는가??

오늘의 그림일기 앱의 무한스크롤은

 

1. useEffect의 dependencyArray에 따라 fetch 함수 실행 ->

2. fetch 함수 success시, state를 기존의 state에 추가

하는 방식으로 짜여져 있었다.

 

이러다보니 꽤나 많은 부분을 신경써줘야 했고 많은 부분이 불편했는데, 내가 느낀 단점은 이랬다.


1. useEffect 자체가 api를 fetching하는데 적합하지 않다.

단적인 예로

1. strictMode에서 2번씩 실행되도록 되어있음.

2. dependencyArray를 이용하는 방식이라 이걸 컨트롤하기가 쉽지가 않다.

 

2. 짜야할 로직이 많다.

loading, refresh, error에 대한 컨트롤 로직이 꽤나 많다.

 

그래서 개선해야할 필요성이 있다고 생각하고 react-query의 useInfiniteQuery를 이용해 리팩토링을 하기로 했다.

수정할 화면


 

왜 react-query의 useInfiniteQuery를 이용하나요?

1. react-query에 익숙한 상태이다.

회사에서도 주로 서버 상태를 react-query를 이용해서 개발하였고 한번 해봤었기 때문에 익숙해서 선택하였다.

 

2. useQuery를 사용해도 되지만, 무한스크롤에 이용하라고 나온 전용 hook인 useInfiniteQuery가 있기 때문에 사용안할 이유가 없다.

 

다음과 같은 이유로 이 기술을 이용하였다.


코드

useInfiteQuery는 server에서 내려주는 meta 데이터에 의존적이다.
이 데이터에 따라서 다음 요청의 param은 어떻게 될지 다음 요청이 없는지에 대해서 판단한다.

 

하지만, 현재 나는 firestore를 이용하고 있으며, 여기에서 내 요청에 대해 따로 meta 데이터를 내려주지 않는다.

그래서 firestore 요청을 wrapping하는 함수를 따로 만들어서 내 요청에 대한 meta데이터를 만들어 주었다.

 

api

import firestore from '@react-native-firebase/firestore';

import {
  GetPictureDiariesRequest,
  GetPictureDiariesResponse,
} from '@/api/types';
import { ShowOffPictureDiary } from '@/types/pictureDiary';

/**
 * @description firestore 에서 pictureDiaries 데이터 가져오기
 * @param limit 개수
 * @param order 정렬
 * @param lastDoc 마지막 document
 */
export const getPictureDiaries = async ({
  limit,
  order,
  lastDoc,
}: GetPictureDiariesRequest): Promise<GetPictureDiariesResponse> => {
  let query = firestore()
    .collection('pictureDiaries')
    .orderBy(order.by, order.direction);

  if (lastDoc !== undefined) {
    query = query.startAfter(lastDoc); // fetch data following the last document accessed
  }
  const querySnapshot = await query.limit(limit).get();

  const data = querySnapshot.docs.map((doc) =>
    doc.data(),
  ) as Array<ShowOffPictureDiary>;

  // 데이터들중 svgPath 가 있는지 확인하고 있는경우 Json.parse 해서 넣어주기
  data.forEach((item) => {
    if (item.svgPaths) {
      item.svgPaths = JSON.parse(item.svgPaths as any);
    }
  });

  const meta = {
    isEnd: querySnapshot.docs.length < limit,
    limit,
    lastDoc: querySnapshot.docs[querySnapshot.docs.length - 1],
    order,
  };

  return {
    meta,
    data,
  };
};

코드를 보면 알겠지만, 이 함수는 요청에 대한 응답을 받으면 response를 보고 meta 데이터를 만들어 받은 응답과 같이 내려준다.

 

그러므로 서버에서 meta 데이터를 내려준것과 같은 효과가 있다. 이제는 useInfiteQuery hook을 작성해보자.

 

hook

  const { data, refetch, fetchNextPage, isLoading, isFetchingNextPage } =
    useInfiniteQuery(
      ['showOffPictureDiaries', orderBy],
      async ({ pageParam = DEFAULT_PAGE_PARAM }) => {
        return await getPictureDiaries({
          ...pageParam,
          order: {
            by: 'createAt',
            direction: orderBy,
          },
        });
      },
      {
        getNextPageParam: (lastPage) => {
          // meta의 isEnd 가 true 면 더이상 가져올 데이터가 없음
          if (lastPage.meta.isEnd) {
            return;
          }
          return {
            ...lastPage.meta,
          };
        },
        select: (data) => {
          // block user 제거
          if (blockUser) {
            return produce(data, (draft) => {
              draft.pages.forEach((page) => {
                page.data = page.data.filter((item) => {
                  let result = true;
                  blockUser.forEach((bu) => {
                    if (bu.uid === item.uid) {
                      result = false;
                    }
                  });
                  return result;
                });
              });
            });
          }
          // block user 데이터가 없다면, 그냥 리턴
          return data;
        },
      },
    );

여기서는 build in option들을 이용하였다. 
getNextPageParam을 통해 다음 페이지를 칠지 말지, select를 통해 block user를 제거한 데이터들만 캐싱하도록 하였다.

 

그리고 react-query에서 제공하는 refetch, fetchNextPage, isLoading, isFetchingNextPage 이러한 변수, 함수들을 이용해서 loading 상태에 대한 처리, refresh 상태에 대한 처리, 다음 페이지 요청 등과 같은 상태를 처리하기 쉬워졌다.

 

코드중 일부

<FlatList
        ref={scrollRef}
        refreshControl={
          <RefreshControl
            tintColor={Platform.OS === 'ios' ? Colors.black : undefined}
            refreshing={isLoading}
            onRefresh={onRefresh}
          />
        }
        onEndReached={onScrollEndReach}
        onEndReachedThreshold={1}
        data={//데이터}
        onScroll={onScroll}
        scrollEventThrottle={100}
        renderItem={renderItem}
        numColumns={Dimension.isPad ? 4 : 2}
        keyExtractor={(item) => item.id}
        ListEmptyComponent={
          isLoading ? <ShowOffPictureDiaryListItemSkeleton /> : null
        }
        ListFooterComponent={
          isFetchingNextPage ? <ShowOffPictureDiaryListItemSkeleton /> : null
        }
      />

동작

https://www.youtube.com/shorts/V89xnv1awv8

기존 로딩에서 skeleton으로 로딩 ui도 변경하여 ux도 개선해보았다!


후기

ux를 개선하려다가 시도한 작업이었는데, 꽤나 만족스럽다. 추후에 새로운 기능을 넣기에도 편할거 같다.

반응형
Comments