ReactQuery란?

ho_vi·2025년 7월 23일

React

목록 보기
19/19

최근 네이버 재직자님의 스터디를 계기로 React Query에 관심이 생겨 개념과 활용법을 정리해보려 한다.

1. ReactQuery

💡 react-query 를 사용하면 서버의 상태를 간단하고 효율적이게 처리 할 수 있습니다.
  1. 자동 캐싱 & 재사용
    • 동일한 쿼리를 여러 컴포넌트에서 사용할 때 데이터를 자동으로 캐싱하고 재사용합니다.
    • 덕분에 불필요한 네트워크 요청을 줄일 수 있습니다.
  2. 자동 리페치 (Refetch)
    • 윈도우 포커스 변경 시, 네트워크가 재연결되었을 때 등 자동으로 데이터 갱신 가능.
    • refetchOnWindowFocus, refetchOnReconnect 같은 옵션 제공.
  3. 로딩/에러/성공 상태 자동 관리
    • isLoading, isError, isSuccess 같은 상태를 자동으로 제공합니다.
    • 수동으로 상태 관리를 하지 않아도 됨.
  4. 간편한 쿼리 무효화 (invalidate) 및 갱신
    • useMutation과 queryClient.invalidateQueries() 조합으로 데이터 일관성을 쉽게 유지할 수 있음.
    • 예: POST/PUT/DELETE 이후 목록 자동 새로고침.
  5. 서버 상태를 전역으로 공유
    • 서버 데이터를 React Context처럼 공유 가능 (별도 상태관리 라이브러리 불필요).
    • 전역 상태 관리 도구 (Recoil, Redux 등) 없이도 서버 데이터 관리에 충분.
  6. 개발자 도구 제공
    • React Query Devtools로 쿼리 상태, 캐시 등 실시간으로 확인 가능.
  7. Suspense, Pagination, Infinite Query 등 고급 기능
    • React.Suspense 연동 지원
    • 페이지네이션 및 무한 스크롤 기능 지원 (useInfiniteQuery)

2. gctime과 stale time

gcTime (Garbage Collection Time)

캐시된 쿼리 데이터가 모든 컴포넌트에서 언마운트된 후에도 얼마 동안 메모리에 유지될지를 밀리초 단위로 설정합니다.

staleTime

데이터가 신선하다고 간주되는 시간. 이 시간내에는 refetch 를 하지 않는다.

시나리오. 만약 stale time이 2초, gcTime이 10초라면 ?

1. 처음 마운트

  • useQuery()가 실행되며 네트워크에서 데이터를 fetch함.
  • 캐시에 저장됨, 그리고 staleTime 타이머(2초)가 시작됨.

2. 2초 내 재사용

  • 다른 컴포넌트에서 같은 쿼리를 사용하거나 리렌더 되어도 → refetch 안 함 (신선함 유지)

3. 2초 이후에도 마운트 중

  • staleTime이 지나면 데이터는 stale 상태로 바뀜.
  • 이후 조건이 맞으면(윈도우 focus, refetch interval 등) → refetch 발생할 수 있음

4. 컴포넌트 언마운트

  • 해당 쿼리를 쓰는 모든 컴포넌트가 사라지면 → React Query는 "아무도 안 씀" 상태로 인식
  • 이제부터 gcTime (10초) 타이머 시작됨

5. 10초 안에 다시 마운트되면?

  • 쿼리 캐시가 살아있으므로 → 기존 데이터 재사용 (신선한지 여부에 따라 refetch 여부 결정)

6. 10초가 지나면

  • 캐시 완전히 삭제됨 → 다시 마운트되면 처음부터 fetch

2-1. stale time 설정

$ pnpm install ms
$ pnpm install -D @types/ms
  • QueryClientProvider.tsx

추가적으로 gcTime 또한 설정 가능

2-2. React Query Hook 만들기

💡 서버에서 데이터를 보낼 때는 useMuation을, 데이터를 가져올 때는 useQuery를 사용하시면 됩니다.
  • useWorkerList.ts
import { useQuery } from "@tanstack/react-query";
import { getWorkerList } from "../selfApi";
import { useAtomValue } from "jotai";
import { headerParamsAtom } from "@shared/stores/atom";

// 근로자 리스트를 조회할 때 사용하는 파라미터 타입 정의
interface WorkerListParams {
  selectedPage: number; // 현재 선택된 페이지
  limit: number; // 페이지당 항목 수
  contentParams: {
    start_date?: string; // 검색 시작일 (선택)
    end_date?: string; // 검색 종료일 (선택)
    search_text?: string; // 검색어 (선택)
  };
}

// 근로자 리스트를 가져오는 커스텀 훅
const useWorkerList = ({
  selectedPage,
  limit,
  contentParams,
}: WorkerListParams) => {
  // 전역 상태에서 헤더에 필요한 파라미터 가져오기
  const headerParams = useAtomValue(headerParamsAtom);

  // 데이터를 페이지 단위로 잘라주는 유틸 함수
  function disassemble(index: number, data: [], size: number) {
    const res = [];
    for (let i = 0; i < data.length; i += size) {
      res.push(data.slice(i, i + size)); // size 단위로 잘라서 배열에 push
    }
    return res[index] || []; // 요청한 페이지에 해당하는 데이터 반환
  }

  // useQuery를 사용하여 서버에서 데이터 패칭
  return useQuery({
    queryKey: [useWorkerList.getKey(), selectedPage, limit], // 쿼리 키는 리스트 식별용 + 페이지, 제한 수
    queryFn: async () => {
      // API에 넘길 모든 파라미터 병합
      const params = Object.assign(
        {},
        contentParams,
        headerParams
      );

      // API 호출
      const res = await getWorkerList(params);

      // 응답에서 데이터 추출 (rsMap이 없으면 빈 배열)
      const tableData = res.data?.rsMap ?? [];

      // 분할된 페이지 데이터와 전체 수를 반환
      return {
        data: disassemble(selectedPage - 1, tableData, limit),
        totalCount: tableData.length,
      };
    },
  });
};

// 쿼리 키를 외부에서 재사용할 수 있도록 static 키 제공
useWorkerList.getKey = () => ["workerList"];

export default useWorkerList;

2-3. useQuery Hook의 isFetching, isError를 활용한 고차 컴포넌트

  • workerManagement.tsx
...
// useWorkerList 훅을 호출하여 데이터를 패칭하고 상태를 받아옴
const { data, refetch, isFetching, isError, error } = useWorkerList({
  selectParams,     // 필터링용 select 박스 값
  selectedPage,     // 현재 페이지
  limit,            // 한 페이지당 아이템 수
  contentParams,    // 검색 조건 및 날짜 범위
});
...

// 콘텐츠 Wrapper 컴포넌트, 검색 필터 및 범위를 지정
<Content
  label=""                        // 화면 타이틀
  type="range"                              // 검색 필터 타입 (기간 검색)
  searchText={searchText}                  // 검색어 입력값
  onSearchText={setSearchText}            // 검색어 입력 변경 핸들러
  selected={searchDate}                    // 선택된 날짜 범위
  onSelect={setSearchDate}                // 날짜 변경 핸들러
  onSearchEvent={onSearchEvent}           // 검색 버튼 클릭 시 실행할 함수
>
  {/* 테이블 틀(헤더 포함)을 감싸는 컴포넌트 */}
  <Table label="" width={width}>
    
    {/* 고차 컴포넌트: 로딩, 에러, 정상 데이터를 분기 처리함 */}
    <AsyncTableBody
      isFetching={isFetching}                // 데이터 로딩 중 여부
      isError={isError}                      // 에러 발생 여부
      error={error}                          // 에러 객체
      refetch={refetch}                      // 다시 요청할 수 있는 함수

      // 로딩 상태일 때 보여줄 컴포넌트 (콜백 형태로 전달)
      loadingFallback={() => (
        <Skeleton
          width={width}                      // 셀 너비
          size={limit}                       // 몇 줄 그릴지 (페이지당 항목 수)
          menu={menuList.length}             // 셀 개수
        />
      )}

      // 에러 상태일 때 보여줄 컴포넌트 (에러와 refetch 전달 가능)
      errorFallback={(err, refetch) => (
        <DataFetchError
          label={""}              // 어떤 화면에서 에러났는지 라벨
          error={err}                        // 에러 내용
          refetch={refetch}                  // 다시 시도할 수 있는 버튼 등에서 사용
        />
      )}
    >
      {/* 실제 데이터를 렌더링하는 테이블 본문 */}
      <TableBody
        label=""                  // 타이틀
        data={data?.data}                    // 테이블에 표시할 데이터
        width={width}                        // 셀 너비
        filteredIndex={filteredIndex}        // NO 표시 시 시작 인덱스 계산값
      />
    </AsyncTableBody>
  </Table>

  {/* 페이지네이션: 페이지 변경 및 limit 변경 가능 */}
  <CommonPagination
    totalCount={data?.totalCount}            // 전체 데이터 개수
    selectedPage={selectedPage}              // 현재 선택된 페이지
    limit={limit}                            // 페이지당 보여줄 항목 수
    onPageChange={(page) => {                // 페이지 번호 변경 시
      setSelectedPage(page);
      refetch();                             // 새 페이지 데이터 패칭
    }}
    onLimitChange={(newLimit) => {           // 한 페이지 항목 수 변경 시
      setLimit(newLimit);
      setSelectedPage(1);                    // 페이지 초기화
      refetch();                             // 데이터 다시 요청
    }}
  />
</Content>

2-4. Query Key를 통한 데이터 구조

const queryClient = useQueryClient();

// 예: 쿼리 무효화
queryClient.invalidateQueries(["workerList", selectedPage, limit]);

// 예: 캐시된 데이터 직접 가져오기
const cachedData = queryClient.getQueryData(["workerList", selectedPage, limit]);

// 예: 캐시된 데이터 수동으로 설정
queryClient.setQueryData(["workerList", selectedPage, limit], newData);
profile
FE 개발자🌱

0개의 댓글