본문 바로가기
Project History/- Seoul Dev Competition

Nextjs에서 React-Query를 사용한 무한 스크롤 구현 경험 정리

by Yoojacha 2023. 5. 27.

무한 스크롤 선택 이유

웹 개발 공모전에서 기획이 나온 것이 자유게시판과 교육정보게시판이었습니다. 각각의 게시판을 어르신 친화적으로 UI를 생각해 봤을 때, 현재 서비스되는 대기업들의 앱만 봐도 무한스크롤이 많았습니다. 사람들을 중독되게 만드는 유튜브 숏츠마저도 엄지로 드레그 하여 무한으로 영상을 보는 방식이었습니다. 이 방식이 휴대폰으로 가장 손쉽게 연령 불문하고 목록들을 보여주는 방법이라는 생각이 들었습니다.


실패한 과정

무한스크롤을 구현하는 방법으로 처음에는 Redux-toolkit의 createAsyncThunk를 이용하려고 시도했었습니다. 그 이유는 기존에 상태관리툴로 Redux-toolkit을 사용하고 있었습니다. 그래서 이미 boilerplate code들이 구성되어 있어서, createAsyncThunk를 사용하면 충분할 것이라 생각이 들었습니다. 

 

하지만, 개발과정에서 상태관리툴인 RTK와 fetch 요청을 동시에 관리하다보니, 중간에 상태관리와 API 요청을 보내는 것에 혼동이 오면서 코드가 지저분해지고 이해가 안 되는 동작들이 생겼었습니다. 같은 팀원의 경우 global state에 대해서 아직 써본 경험이 없었기에 이 상태로는 무한 스크롤 구현이 어려워 보였습니다. 꾸역꾸역 리서치를 통해 구현을 해나갔지만 결국 무한 스크롤이 되면서 100개, 200개, 1000개 등 넘어가면 브라우저가 사용하는 리소스가 거대해지면서 렉이 걸리는 현상이 생겨서 본질적으로 잘못된 선택이라는 판단을 했습니다.


React-Query 선택 이유

1년 전에 노마드코더에서 react-query를 접하던 시기엔 tanstack으로 회사명을 앞에 붙이면서 버전이 변경됨에 따라 제가 학습하기에 불편해서 기억에서 지웠다 보니 다시 찾아보았습니다. tanstack의 웹사이트에선 설명을 "High-quality open-source software for web developers."라고 설명을 하네요. 당시에 react-query를 통해서 캐싱기능을 통해 무한스크롤도 구현할 수 있도록 한다는 것이 기억에 남아서, 사용해 보고자 자료들을 찾아본 결과, useInifniteQueries 함수로 아주 간단하게 구현이 가능했으며, 실제로 21년, 22년 유튜브 영상 자료들을 찾아보니 우아한 테크, 카카오 등이 사용하는 것으로 확인이 되어서 신뢰가 생겼습니다. 영상 자료들을 봤을 때 nextjs와 호환이 좋은 SWR 등과도 비교하여 React-Query가 가장 좋은 해답이라는 판단이 섰습니다. React-Query와 이외의 패키지의 상세한 비교표


React-Query를 사용한 후 느낀 좋은 점

  • use~ 라는 식으로 사용되어 기존의 React Hook의 개념과 비슷하게 사용되다 보니 이해하는 코드를 이해하는 속도가 빨라졌습니다.
  • 캐싱기능이 매우 유용했습니다. 캐싱 옵션을 직관적이게 조절할 수 있었습니다.
  • axios 요청을 보내는 함수와 react-query 함수를 리턴하는 커스텀 함수를 같은 파일 안에 따로 분리하여, 가독성과 유지보수 측면에서 긍정적이었습니다.
  • devtools가 매우 유용했습니다. 데이터가 어떤 방식으로 불러와지고, 문제가 있는지 바로 파악이 되었습니다.
  • useMutate를 통해 UX 가 좋아지는 부분도 정말 놀라운 기술이었습니다.
  • invalidateQureies 를 통해서 캐싱된 데이터도 refetch 하도록 조절하는 부분이 좋았습니다.

실제 구현한 방식

src/api 폴더 안에 cors 설정을 해결해줄 axiosInstance.ts 파일을 가장 바깥에 두고,

받아올 데이터의 유형 별로 폴더를 나눴습니다.

 

이 방식이 옳지 못하다라는 것을 구현을 하던 중간에 알아차렸지만, 팀원과 이미 합의를 했던 상황이었기 때문에, CRUD에 따라서 , creat, read, update, delete를 앞에 붙이며, 추가로 하는 요청들은 역할에 따라 동사를 다르게 붙여주었습니다. 이방식은 제 경험상 좋지 못해서 참고 안하셨으면 좋겠습니다!

 

// src/api/educations/readEducations.ts

import axios from "@api/axiosInstance";
import { useInfiniteQuery } from "@tanstack/react-query";
import { useAppSelector } from "@toolkit/hook";
import {
  IEducationsDataPerPage,
  IEducationsQueryParams,
} from "@type/educations";
import { TDate, TPrice, TStatus } from "@type/filter";
import { TSearchCategory } from "@type/search";
import { AxiosError } from "axios";

//! axios GET 요청 함수
export const fetchEducations = async (
  searchCategory: TSearchCategory,
  page: number,
  name: string,
  status: TStatus,
  startDate: TDate,
  endDate: TDate,
  minPrice: TPrice,
  maxPrice: TPrice
) => {
  const params: IEducationsQueryParams = { page };

  //! 쿼리 파람 추가
  if (name !== "") params.name = name;
  if (status !== "전체") params.status = status;
  if (startDate !== "") params.startDate = startDate;
  if (endDate !== "") params.endDate = endDate;
  if (minPrice !== "0") params.minPrice = minPrice;
  if (maxPrice !== "100000") params.maxPrice = maxPrice;

  //! 요청 받기
  try {
    const response = await axios.get(`/${searchCategory}`, {
      params,
    });

    return response.data;
  } catch (err) {
    return { data: [], currentPage: 0, totalPages: 0, totalElements: 0 };
  }
};

//! 검색 결과 useInfiniteQuery 함수
export const useInfiniteEducations = () => {
  const searchCategory = "educations";

  //! search state와 filter state 값 받아오기
  const { searchKeyword } = useAppSelector((state) => state.search);
  const { status, startDate, endDate, minPrice, maxPrice } = useAppSelector(
    (state) => state.filter
  );

  //! react-query hook 반환
  return useInfiniteQuery<IEducationsDataPerPage, AxiosError>({
    queryKey: [
      {
        category: searchCategory,
        keyword: searchKeyword,
        status: status,
        startDate: startDate,
        endDate: endDate,
        minPrice: minPrice,
        maxPrice: maxPrice,
      },
    ],
    queryFn: ({ pageParam = 0 }) =>
      fetchEducations(
        searchCategory,
        pageParam,
        searchKeyword,
        status,
        startDate,
        endDate,
        minPrice,
        maxPrice
      ),
    getNextPageParam: (lastPage) => {
      if (lastPage.currentPage < lastPage.totalPages) {
        return lastPage.currentPage + 1;
      } else {
        return undefined;
      }
    },
    cacheTime: 300000, // 5분
    staleTime: 240000, // 4분
    refetchOnMount: true, //페이지 재방문시 refetch 금지
    refetchOnWindowFocus: false, // 브라우저 포커징시 refetch 금지
  });
};

무한 스크롤을 구현하기 위해서 백엔드에서 totalPages와 currentPage를 응답으로 주며, data라는 이름의 키에 배열 형태로 데이터를 20개씩 넣어주는 것으로 합의했었습니다. 

위의 경우에 RTK를 통해 search state와 filter state를 받아봐서 그대로 useInfiniteQueries 함수안에 queryKey로 넣어줍니다. 이를 통해서 axios 요청을 하는 함수에 인자로 값을 주어서 쿼리 파람을 추가해주어서 검색과 필터링 기능도 같이 구현을 했습니다. 이 방식이 따로 검색하는 페이지를 두지 않고 무한 스크롤을 하는 페이지에서 그대로 검색을 진행하고, 캐싱이 되기 때문에 UX 측면에서 매우 큰 장점이라 생각했습니다.

 

쿼리키가 저에게는 잘 맞는다 생각했지만, 후에 다른 사람들이 이 방식을 사용한다면 어디서 요청을 받은 데이터가 queryKey가 이렇게 되었는지 이해하는데 시간이 걸릴 것으로 파악이 되어서 url 자체를 queryKey에 넣어주는 것이 좋다고 느꼈습니다.


개선해야할 점

  • 폴더 구조 개선 (queries, mutations 폴더 안에 목적별로 파일 생성)
  • swagger를 통한 orval과 react-query 조합으로 자동 생성 코드 구현
  • QueryKey Convention 설정

https://develogger.kro.kr/blog/LKHcoding/152

 

react-query 사내 도입 회고 1편

도입 배경 1. 첫 인상 취준 당시 develogger 프로젝트를 처음 개발할때 react-query를 도입해서 사용했었다. 이때 react-query를 도입하게 된 이유도 많은 고민이 있었다. 먼저 대부분의 상태가 백엔드에서

develogger.kro.kr

https://orval.dev/guides/react-query

 

React query

React query You should have an OpenApi specification and an Orval config where you define the mode as react-query. Example with React query 1 module.exports = {2 petstore: {3 output: {4 mode: 'tags-split',5 target: 'src/petstore.ts',6 schemas: 'src/model',

orval.dev


참고

https://tanstack.com/query/v4/docs/react/examples/react/load-more-infinite-scroll

 

React Query Load More Infinite Scroll Example | TanStack Query Docs

An example showing how to implement Load More Infinite Scroll in React Query

tanstack.com

https://velog.io/@cnsrn1874/react-query-useInfiniteQuery

 

[react-query] useInfiniteQuery

useInfiniteQuery로 데이터 무한히 불러오기

velog.io

https://tanstack.com/query/v4/docs/react/typescript

 

TypeScript | TanStack Query Docs

React Query is now written in TypeScript to make sure the library and your projects are type-safe! Things to keep in mind:

tanstack.com

react-query 정리

https://github.com/ssi02014/react-query-tutorial#%EC%BA%90%EC%8B%9C-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%A6%89%EC%8B%9C-%EC%97%85%EB%8D%B0%EC%9D%B4%ED%8A%B8

 

GitHub - ssi02014/react-query-tutorial: 😃 TanStack Query(aka. react query) 에서 자주 사용되는 개념 정리

😃 TanStack Query(aka. react query) 에서 자주 사용되는 개념 정리 - GitHub - ssi02014/react-query-tutorial: 😃 TanStack Query(aka. react query) 에서 자주 사용되는 개념 정리

github.com

 

댓글