React Query로 서버 데이터 효율적으로 관리하기

Doozuu·2025년 4월 10일
0

React

목록 보기
30/30
post-thumbnail

React Query란?

React Query는 서버에서 데이터를 가져오고 관리하는 과정을 도와주는 라이브러리이다.
React Query를 사용하면 데이터 패칭, 캐싱, 동기화, 업데이트 등 복잡한 로직을 쉽게 처리할 수 있다.

참조

React Query는 원래 React 애플리케이션 전용으로 설계된 라이브러리였으나, 이후 TanStack Query라는 이름 하에 다양한 프레임워크를 지원하는 범용적인 데이터 패칭 라이브러리로 발전했다.
TanStack Query는 React뿐만 아니라 Vue, Svelte 등 여러 프레임워크에서도 사용할 수 있게 되었고, React용 버전은 여전히 React Query라는 독립적인 이름을 유지하고 있다.


React Query의 주요 기능

1. 자동 데이터 패칭 (Auto-fetching)

React Query는 컴포넌트가 마운트될 때마다 자동으로 데이터를 패칭한다.
기본적으로, useQuery 훅을 사용하여 데이터를 가져오고, 쿼리 키를 기반으로 데이터를 캐싱하여 다시 패칭할 필요 없이 빠르게 데이터를 제공할 수 있다.
예를 들어, 아래 코드는 컴포넌트가 마운트될 때마다 fetchTodos 함수로 데이터를 자동으로 패칭한다.

import { useQuery } from 'react-query';

const fetchTodos = async () => {
  const response = await fetch('https://jsonplaceholder.typicode.com/todos');
  return response.json();
};

const TodoList = () => {
  const { data, isLoading, isError } = useQuery('todos', fetchTodos);

  if (isLoading) return <p>Loading...</p>;
  if (isError) return <p>Error fetching todos!</p>;

  return (
    <ul>
      {data.map(todo => (
        <li key={todo.id}>{todo.title}</li>
      ))}
    </ul>
  );
};

React-Query가 데이터를 Refetching하는 시점은 아래와 같다.

  • 브라우저에 포커스가 들어온 경우(refetchOnWindowFocus)
  • 새로운 컴포넌트 마운트가 발생한 경우(refetchOnMount)
  • 네트워크 재연결이 발생한 경우(refetchOnReconnect)

2. 캐싱 (Caching)

React Query는 데이터를 자동으로 캐싱하여 성능을 최적화한다.
동일한 쿼리 키로 요청이 오면, 서버에서 데이터를 다시 가져오는 대신 캐시된 데이터를 즉시 반환하여 불필요한 네트워크 요청을 줄일 수 있다. 예를 들어, 아래와 같이 동일한 쿼리 키(todos)로 요청하면 두 번째 요청은 캐시된 데이터로 응답을 반환한다.

const { data } = useQuery('todos', fetchTodos); // 첫 번째 호출
// 두 번째 호출 시 캐시된 데이터 반환

3. 데이터 동기화 (Data Synchronization)

React Query는 쿼리의 데이터를 최신 상태로 유지하기 위해 주기적으로 데이터를 갱신하거나, 특정 이벤트가 발생할 때 데이터를 동기화할 수 있다. 예를 들어, staleTime과 refetchInterval을 사용하여 데이터 갱신 주기를 설정할 수 있다.

const { data } = useQuery('todos', fetchTodos, {
  staleTime: 5000,  // 데이터가 5초 동안 갱신되지 않고 유지됨
  refetchInterval: 10000,  // 10초마다 데이터 리패치
});

4. 백그라운드에서의 데이터 갱신 (Background Refetching)

React Query는 데이터가 오래되었을 때 백그라운드에서 자동으로 데이터를 갱신할 수 있다. 이 방식은 사용자가 데이터를 최신 상태로 유지하도록 도와준다. refetchIntervalInBackground를 사용하여 백그라운드에서 데이터를 갱신할 수 있다.

const { data } = useQuery('todos', fetchTodos, {
  refetchIntervalInBackground: true,  // 백그라운드에서 자동 리패치
});

5. React Query의 서버 상태 관리

React Query는 클라이언트와 서버 간의 상태를 효율적으로 관리하는 데 유리하다.
서버 상태는 자주 변할 수 있으므로, React Query는 이를 자동으로 관리하고, 리액트 컴포넌트에서 최신 상태를 쉽게 반영할 수 있도록 도와준다. 예를 들어, 데이터를 변경한 후 이를 즉시 UI에 반영하기 위해 mutate 함수와 useMutation 훅을 사용할 수 있다.

import { useMutation, useQueryClient } from 'react-query';

const addTodo = async (newTodo) => {
  const response = await fetch('https://jsonplaceholder.typicode.com/todos', {
    method: 'POST',
    body: JSON.stringify(newTodo),
  });
  return response.json();
};

const AddTodo = () => {
  const queryClient = useQueryClient();
  const mutation = useMutation(addTodo, {
    onSuccess: () => {
      queryClient.invalidateQueries('todos');  // 데이터 변경 후 'todos' 쿼리 재패치
    },
  });

  const handleAdd = () => {
    mutation.mutate({ title: 'New Todo', completed: false });
  };

  return <button onClick={handleAdd}>Add Todo</button>;
};

6. React Query의 에러 처리

React Query는 요청이 실패할 경우, 자동으로 에러를 처리하는 기능을 제공한다. onError와 같은 콜백 함수를 통해 에러가 발생했을 때의 처리를 커스터마이즈할 수 있다. 예를 들어, useQuery 훅에서 데이터를 가져올 때 에러 발생 시 이를 처리하는 방법은 다음과 같다.

const { data, error } = useQuery("todos", fetchTodos, {
  onError: (error) => {
    console.error("Error fetching todos:", error);
  }
});

7. React Query의 성능 최적화

React Query는 데이터 패칭 및 상태 관리를 최적화할 수 있는 여러 기능을 제공한다.
예를 들어, 쿼리의 중복 요청을 방지하거나, 데이터 요청 간격을 최적화하는 등의 성능 최적화 기능이 포함되어 있다. debounce 또는 throttle을 사용하여 네트워크 요청 횟수를 줄일 수 있다.

const { data } = useQuery('todos', fetchTodos, {
  staleTime: 60000,  // 데이터가 1분 동안 갱신되지 않고 유지됨
});

8. React Query와 서버 사이드 렌더링 (SSR)

React Query는 Next.js와 같은 서버 사이드 렌더링(SSR) 프레임워크와 잘 호환된다.
React Query의 Hydration API를 사용하면 서버에서 패칭한 데이터를 클라이언트에서 재사용할 수 있다. 이를 통해 서버에서 미리 데이터를 패칭하고, 클라이언트에서 데이터를 재사용하는 방식으로 렌더링 성능을 최적화할 수 있다.

import { Hydrate, QueryClient, QueryClientProvider } from 'react-query';

const queryClient = new QueryClient();

const App = ({ dehydratedState }) => {
  return (
    <QueryClientProvider client={queryClient}>
      <Hydrate state={dehydratedState}>
        <TodoList />
      </Hydrate>
    </QueryClientProvider>
  );
};

React Query에서 제공하는 hook

React Query에서 제공하는 대표적인 hook으로는 useQuery, useMutation, useQuries, useQueryClient 등이 있다.
(이외에도 useQueryClient, useInfiniteQuery, useIsFetching, useIsMutating, useQueryErrorResetBoundary 등 다양한 훅들을 제공하고 있다.)

이 중 가장 자주 사용하는 useQuery와 useMutation에 대해 설명하려 한다.


useQuery

useQuery 훅은 React Query에서 데이터를 패칭하고 관리하는 가장 핵심적인 기능이다.
서버에서 데이터를 가져오는 작업을 단순화하고 데이터의 로딩, 에러 처리, 캐시 관리 등 다양한 기능을 제공한다.

기본 사용법

useQuery 훅을 사용하면 데이터를 간편하게 패칭할 수 있다. 기본적인 사용법은 다음과 같다.

const { data, error, isLoading } = useQuery("todos", fetchTodos);
  • "todos"는 쿼리 키로, 해당 쿼리의 고유한 식별자 역할을 한다.
  • fetchTodos는 데이터를 가져오는 함수로, 이 함수는 서버에서 데이터를 패칭하는 작업을 수행한다.

쿼리 옵션 (Query Options)

useQuery 훅은 여러 가지 옵션을 제공하여 데이터 패칭 방식을 세밀하게 제어할 수 있다.
주요 옵션은 다음과 같다.

  • queryKey : 쿼리의 고유 식별자 역할을 한다. 문자열이나 배열로 지정할 수 있으며 데이터를 캐시하고 업데이트를 추적하는 데 사용된다.
  • queryFn : 데이터를 실제로 가져오는 함수이다. 이 함수는 queryKey와 함께 실행되어 서버에서 데이터를 받아온다. 이 함수는 반드시 데이터를 반환해야 하며, 비동기적으로 데이터를 가져오는 경우 async와 await를 사용할 수 있다.
  • enabled : 쿼리의 실행 여부를 제어하는 옵션이다. 이 값을 false로 설정하면 쿼리가 자동으로 실행되지 않으며, 다른 조건에 따라 수동으로 refetch()를 호출하여 실행할 수 있다. 이 옵션은 조건부로 데이터를 로드해야 할 때 유용하다.
  • staleTime : 데이터가 "구식(stale)" 상태가 되기까지의 시간을 설정하는 옵션이다. 이 시간 동안 데이터는 갱신되지 않는다. 기본적으로는 0이 설정되어 있으며, staleTime을 설정하면 해당 시간 동안은 데이터가 구식 상태로 간주되지 않는다.
  • cacheTime : 캐시된 데이터가 메모리에서 삭제되기까지의 시간을 설정하는 옵션이다. cacheTime이 지나면 해당 데이터는 자동으로 메모리에서 제거된다. 기본적으로는 5분(300,000ms)으로 설정된다.
  • retry : 요청 실패 시 재시도 횟수를 설정하는 옵션이다. 기본적으로는 3번까지 재시도하며, retry: false로 설정하면 재시도를 하지 않도록 할 수 있다.
  • select : 서버에서 받은 데이터를 변형하거나 필터링하는 함수이다. 이를 사용하여 반환된 데이터를 필요한 형태로 변환할 수 있다. 예를 들어, 배열에서 일부 항목만 필터링하거나, 데이터를 특정 형식으로 변환할 때 사용한다.
  • onSuccess / onError : 쿼리가 성공적으로 데이터를 패칭하거나 실패했을 때 호출되는 콜백 함수이다. onSuccess는 데이터 패칭이 성공했을 때, onError는 에러가 발생했을 때 호출된다. 이 옵션을 통해 패칭 결과에 따른 후속 작업을 처리할 수 있다.

예제

const fetchTodos = async () => {
  const response = await fetch("/api/todos");
  return response.json();
};

const { data, isLoading, error, refetch } = useQuery("todos", fetchTodos, {
  enabled: true,               // 쿼리 실행 여부 제어
  staleTime: 10000,             // 10초 동안 데이터 갱신하지 않음
  cacheTime: 300000,            // 5분 동안 캐시 유지
  retry: 3,                    // 실패 시 3번까지 재시도
  select: (data) => data.filter(todo => todo.completed), // 완료된 항목만 필터링
  onSuccess: (data) => {
    console.log("데이터 패칭 성공:", data);
  },
  onError: (error) => {
    console.error("데이터 패칭 실패:", error);
  },
});

if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;

return (
  <div>
    <button onClick={refetch}>리패칭</button>
    <ul>
      {data.map(todo => (
        <li key={todo.id}>{todo.title}</li>
      ))}
    </ul>
  </div>
);

쿼리 상태 (Query State)

React Query는 쿼리의 상태를 자동으로 관리한다. useQuery 훅에서 반환되는 상태 변수들을 통해 데이터의 로딩, 에러 상태 등을 쉽게 처리할 수 있다.
주요 상태 변수는 다음과 같다.

  • isLoading: 데이터가 로딩 중일 때 true를 반환
  • isError: 데이터 패칭 중 에러가 발생했을 때 true를 반환
  • isSuccess: 데이터 패칭이 성공적으로 완료되었을 때 true를 반환
  • isFetching: 데이터가 로딩 중이거나 리패칭 중일 때 true를 반환
  • isStale: 데이터가 구식 상태일 때 true를 반환

예제 (로딩, 에러, 성공에 따른 return 처리)

const { data, isLoading, isError, isSuccess, isFetching } = useQuery("todos", fetchTodos);

if (isLoading) return <div>Loading...</div>;
if (isError) return <div>Error occurred</div>;

return (
  <div>
    {isSuccess && (
      <ul>
        {data.map(todo => (
          <li key={todo.id}>{todo.title}</li>
        ))}
      </ul>
    )}
  </div>
);

참조 ) useQuery에서 제공하는 전체 옵션과 상태는 아래와 같다. (상세한 설명은 블로그 하단 공식 문서 참조)

const {
  data,
  dataUpdatedAt,
  error,
  errorUpdatedAt,
  failureCount,
  failureReason,
  fetchStatus,
  isError,
  isFetched,
  isFetchedAfterMount,
  isFetching,
  isInitialLoading,
  isLoading,
  isLoadingError,
  isPaused,
  isPending,
  isPlaceholderData,
  isRefetchError,
  isRefetching,
  isStale,
  isSuccess,
  promise,
  refetch,
  status,
} = useQuery(
  {
    queryKey,
    queryFn,
    gcTime,
    enabled,
    networkMode,
    initialData,
    initialDataUpdatedAt,
    meta,
    notifyOnChangeProps,
    placeholderData,
    queryKeyHashFn,
    refetchInterval,
    refetchIntervalInBackground,
    refetchOnMount,
    refetchOnReconnect,
    refetchOnWindowFocus,
    retry,
    retryOnMount,
    retryDelay,
    select,
    staleTime,
    structuralSharing,
    subscribed,
    throwOnError,
  },
  queryClient,
)

useMutation

useMutation은 데이터를 수정하거나 업데이트할 때 사용한다. (PUT, POST, DELETE)

기본 사용법

useMutation 훅을 사용하면 데이터를 수정하거나 업데이트하는 작업을 간편하게 처리할 수 있다.
기본적인 사용법은 다음과 같다.

const { mutate, isError, isLoading, error } = useMutation(addTodo, {
  onSuccess: () => {
    // 성공 시 추가 작업 수행
  },
});
  • addTodo는 데이터를 추가하는 함수로, 서버에서 데이터를 수정하는 작업을 수행한다.
  • mutate는 뮤테이션을 실행하는 함수로, 이 함수에 필요한 변수를 전달하여 데이터를 수정할 수 있다.

뮤테이션 옵션 (Mutation Options)

useMutation 훅은 다양한 옵션을 제공하여 뮤테이션의 동작 방식을 제어할 수 있다.

  • mutationFn: 뮤테이션을 실행하는 함수이다. 데이터를 수정하거나 서버와의 상호작용을 통해 변경을 처리한다.
  • gcTime: 성공적으로 처리된 뮤테이션 결과가 캐시에서 삭제되기까지의 시간이다.
  • meta: 뮤테이션과 관련된 메타데이터를 설정할 수 있다.
  • mutationKey: 뮤테이션의 고유 식별자 역할을 한다. 여러 뮤테이션이 동일한 mutationKey를 사용할 수 있다.
  • networkMode: 네트워크 요청의 모드를 설정하는 옵션이다.
  • onError: 뮤테이션이 실패했을 때 호출되는 콜백 함수이다.
  • onMutate: 뮤테이션 실행 전에 호출되는 콜백 함수이다.
  • onSettled: 뮤테이션이 완료된 후 호출되는 콜백 함수이다.
  • onSuccess: 뮤테이션이 성공적으로 완료된 후 호출되는 콜백 함수이다.
  • retry: 요청 실패 시 재시도 횟수를 설정하는 옵션이다.
  • retryDelay: 재시도 전에 대기할 시간(밀리초)을 설정한다.
  • scope: 뮤테이션을 특정 범위에 제한할 때 사용하는 옵션이다.
  • throwOnError: 실패 시 예외를 던지도록 설정하는 옵션이다.
const addTodo = async (newTodo) => {
  const response = await fetch("/api/todos", {
    method: "POST",
    body: JSON.stringify(newTodo),
  });
  return response.json();
};

const {
  data,
  error,
  isError,
  isIdle,
  isPending,
  isPaused,
  isSuccess,
  failureCount,
  failureReason,
  mutate,
  mutateAsync,
  reset,
  status,
  submittedAt,
  variables,
} = useMutation(addTodo, {
  onSuccess: (data) => {
    console.log("Todo added successfully:", data);
  },
  onError: (error) => {
    console.error("Error adding todo:", error);
  },
  retry: 3, // 실패 시 최대 3번 재시도
  retryDelay: 1000, // 재시도 간 1초 대기
});

mutate({ title: "New Todo" });

뮤테이션 상태 (Mutation State)

React Query는 뮤테이션의 상태를 자동으로 관리한다. useMutation 훅에서 반환되는 상태 변수들을 통해 데이터 수정 작업의 진행 상황을 쉽게 처리할 수 있다.

  • data: 뮤테이션이 성공적으로 완료되었을 때 반환된 데이터이다.
  • error: 뮤테이션이 실패했을 때 발생한 오류 객체이다.
  • isError: 뮤테이션이 실패했을 때 true로 반환되는 불리언 값이다.
  • isIdle: 뮤테이션이 아직 실행되지 않은 상태일 때 true로 반환되는 불리언 값이다.
  • isPending: 뮤테이션이 진행 중일 때 true로 반환되는 불리언 값이다.
  • isPaused: 뮤테이션이 일시 중지된 상태일 때 true로 반환되는 불리언 값이다.
  • isSuccess: 뮤테이션이 성공적으로 완료되었을 때 true로 반환되는 불리언 값이다.
  • failureCount: 뮤테이션이 실패한 횟수이다.
  • failureReason: 뮤테이션이 실패한 이유이다.
  • mutate: 뮤테이션을 실행하는 함수이다.
  • mutateAsync: 비동기 방식으로 뮤테이션을 실행하는 함수이다.
  • reset: 뮤테이션 상태를 초기화하는 함수이다.
  • status: 뮤테이션의 현재 상태를 나타내는 문자열 값("idle", "loading", "error", "success")이다.
  • submittedAt: 뮤테이션이 제출된 시간이다.
  • variables: 뮤테이션에 전달된 변수이다.
const { data, isLoading, isError, isSuccess, isPending } = useMutation(addTodo);

if (isLoading) return <div>Loading...</div>;
if (isError) return <div>Error occurred</div>;

return (
  <div>
    {isSuccess && <div>Todo added successfully: {data.title}</div>}
  </div>
);

참조 ) useMutation에서 제공하는 전체 옵션과 상태는 아래와 같다. (상세한 설명은 공식 문서 참조)

const {
  data,
  error,
  isError,
  isIdle,
  isPending,
  isPaused,
  isSuccess,
  failureCount,
  failureReason,
  mutate,
  mutateAsync,
  reset,
  status,
  submittedAt,
  variables,
} = useMutation(
  {
    mutationFn,
    gcTime,
    meta,
    mutationKey,
    networkMode,
    onError,
    onMutate,
    onSettled,
    onSuccess,
    retry,
    retryDelay,
    scope,
    throwOnError,
  },
  queryClient,
)
mutate(variables, {
  onError,
  onSettled,
  onSuccess,
})

React Query의 고급 기능

페이지네이션

React Query는 페이지네이션을 구현할 때 유용한 여러 기능을 제공한다.
예를 들어, getNextPage와 getPreviousPage를 사용하면 다음 페이지와 이전 페이지의 데이터를 손쉽게 불러올 수 있다. 이를 통해, 데이터를 한 번에 전부 로딩하지 않고 필요한 만큼만 로드하여 성능을 최적화할 수 있다.

const {
  data,
  fetchNextPage,
  fetchPreviousPage,
  hasNextPage,
  hasPreviousPage,
  isFetchingNextPage,
  isFetchingPreviousPage,
} = useInfiniteQuery('items', fetchItems, {
  getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
  getPreviousPageParam: (firstPage, pages) => firstPage.prevCursor,
});
  • getNextPageParam: 다음 페이지의 데이터를 가져오는 데 필요한 매개변수를 정의하는 함수이다.
  • getPreviousPageParam: 이전 페이지의 데이터를 가져오는 데 필요한 매개변수를 정의하는 함수이다.
  • hasNextPage: 더 많은 데이터가 있는지 여부를 나타내는 불리언 값이다.
  • hasPreviousPage: 이전 페이지 데이터가 있는지 여부를 나타내는 불리언 값이다.
  • isFetchingNextPage, isFetchingPreviousPage: 각각 다음 페이지와 이전 페이지의 데이터를 로딩 중인지 여부를 나타낸다.

무한 스크롤 (Infinite Scroll)

React Query의 useInfiniteQuery 훅을 사용하면 무한 스크롤을 쉽게 구현할 수 있다.

const {
  data,
  fetchNextPage,
  isLoading,
  isFetchingNextPage,
  hasNextPage,
} = useInfiniteQuery('posts', fetchPosts, {
  getNextPageParam: (lastPage, allPages) => lastPage.nextCursor,
});

if (isLoading) return <div>Loading...</div>;

return (
  <div>
    {data.pages.map((page) => (
      <div key={page.id}>
        {page.items.map((item) => (
          <p key={item.id}>{item.title}</p>
        ))}
      </div>
    ))}
    {isFetchingNextPage && <div>Loading more...</div>}
    {hasNextPage && (
      <button onClick={() => fetchNextPage()} disabled={isFetchingNextPage}>
        Load More
      </button>
    )}
  </div>
);
  • useInfiniteQuery: 데이터를 무한히 로드할 수 있도록 도와주는 훅이다.
  • getNextPageParam: 다음 페이지 데이터를 가져오기 위한 파라미터를 정의하는 함수이다.
  • fetchNextPage: 다음 페이지 데이터를 불러오는 함수이다.
  • hasNextPage: 더 많은 페이지가 있는지 여부를 나타내는 불리언 값이다.

옵티미스틱 업데이트 (Optimistic Updates)

옵티미스틱 업데이트는 클라이언트가 서버 응답을 기다리지 않고 즉시 UI를 업데이트하는 방식이다. React Query는 이 기능을 지원하여 사용자가 데이터를 수정할 때 서버 응답을 기다리지 않고 UI를 즉시 변경할 수 있도록 한다. 이 방식은 사용자 경험을 향상시키고, 반응성이 좋은 UI를 제공하는 데 유용하다.

const { mutate } = useMutation(addTodo, {
  onMutate: (variables) => {
    const previousTodos = queryClient.getQueryData('todos');
    queryClient.setQueryData('todos', (oldData) => [
      ...oldData,
      { id: new Date().toISOString(), ...variables },
    ]);
    return { previousTodos };
  },
  onError: (err, variables, context) => {
    queryClient.setQueryData('todos', context.previousTodos);
  },
  onSettled: () => {
    queryClient.invalidateQueries('todos');
  },
});

mutate({ title: 'New Todo' });
  • onMutate: 뮤테이션이 실행되기 전 호출되는 함수로, 옵티미스틱 UI 업데이트를 설정한다. 서버 응답을 기다리지 않고 UI 상태를 변경한다.
  • onError: 뮤테이션이 실패했을 때 호출되는 함수로, 실패 시 UI를 원래 상태로 되돌린다.
  • onSettled: 뮤테이션이 완료된 후 호출되며, 데이터를 다시 가져오거나 쿼리를 무효화하는 작업을 수행한다.

써보며 느낀 장점

  • 서버 데이터를 불러오고 저장하기 위해 useEffect와 useState를 매번 새로 정의하지 않아도 된다.
  • 로딩 및 에러 처리가 쉽다. (로딩, 에러, 데이터 상태를 자동으로 관리하고 isLoading, isError, data 등의 상태를 반환해줘서 직접 처리할 필요가 없다.)
  • enabled 옵션으로 useEffect의 deps처럼 조건부 패치가 가능하다.
  • 캐싱을 통해 성능 최적화를 할 수 있다.

참고 자료

profile
모든게 새롭고 재밌는 프론트엔드 새싹

0개의 댓글