React Query로 데이터 흐름, 이렇게 쉬워도 되나요?

고예진·2025년 1월 12일

리액트 어플리케이션의 경우 크게 3가지의 상태를 관리한다.

특정 컴포넌트 내부에서 사용하는 local state, 전역으로 관리하는 global state, 서버에서 받아온 데이터를 관리하는 server state가 있다.
React Query는 이 중 server state를 관리하는 최적화된 라이브러리이다.

이번 글에서는 React Query가 서버 상태를 어떻게 최적화하고 관리하는지에 대해 알아보려고 한다.

React Query란?

fetching, caching, 서버 데이터와의 동기화를 지원해주는 라이브러리

→ React 환경에서 서버의 상태를 불러오고, 캐싱하며, 지속적으로 동기화하고 업데이트하는 작업을 도와주는 라이브러리

React Query, 왜 사용할까?

  • 클라이언트 상태와 서버 상태의 분리
    • 클라이언트에서 두 상태를 혼합하여 사용할 경우 상태 관리와 로직 작성이 복잡해지고 유지보수가 어려워질 수 있어서 React Query를 사용해서 서버 상태를 따로 관리할 수 있다.
  • 최신 상태 유지
    • 일정 시간마다 상태를 업데이트하거나 특정 동작 이후에 상태를 업데이트 할 수 있다.
  • 캐싱, 중복 요청 방지
    • 서버에 API 요청하여 받아온 결과를 캐싱하며 중복 요청을 최소화할 수 있다.
  • 비동기 요청에 대한 상태 핸들링
    • 비동기 API 요청에 대한 로딩 상태, 결과 값, 에러 상태와 같은 여러가지 상태를 확인하는 기능을 제공한다.

React Query의 대표 기능

useQuery

useQuery Hook으로 수행되는 Query 요청은 GET 요청과 같이 서버에 저장되어 있는 상태를 불러와 사용한다.

const { data } = useQuery(
  queryKey, // 이 Query 요청에 대한 응답 데이터를 캐시할 때 사용할 Unique Key (required)
  queryFn, // 이 Query 요청을 수행하기 위한 Promise를 Return 하는 함수 (required)
  options, // useQuery에서 사용되는 Option 객체 (optional)
);
  • queryKey
    • queryKey는 쿼리를 식별하는 고유한 값으로, 배열 형태로 지정한다.
    • useQuery Hook은 요청마다(API마다) 구분되는 Unique Key(aka. Query Key)가 필요하다. React Query는 이 Unique Key로 서버 상태(aka. API Response)로 로컬에 캐시하고 관리한다.
    • 기본적으로 queryFn에서 사용하는 변수는, 변수가 변경될 때마다 자동으로 다시 가져올 수 있게 쿼리 키에 포함돼야 한다.
      • 변수와 상관없이 항상 하나의 쿼리로 처리하고 싶으면, ESLint exhaustive-deaps 규칙을 비활성화 하면 된다.
  • queryFn
    • queryFn은 데이터를 가져오는 비동기 함수
    • 반드시 데이터를 반환하거나 오류를 던져야 한다. 던져진 오류는 반환되는 error 객체로 확인할 수 있다.
import { useQuery } from '@tanstack/react-query'

type ResponseValue = {
  message: string
  time: string
}

export default function DelayedData() {
  const { data, error } = useQuery<ResponseValue>({
    queryKey: ['delay', wait],
    queryFn: async () => {
      const res = await fetch(`https://api.heropy.dev/v0/delay?t=${wait}`)
      const data = await res.json()
      if (!data.time) {
        throw new Error('문제가 발생했습니다!')
      }
      return data
    },
    staleTime: 1000 * 10, // 데이터 유효 기간(fresh -> stale)
    retry: 1 // 요청 재시도 횟수
  })
  return (
    <>
      {data && <div>{JSON.stringify(data)}</div>}
      {error && <div>{error.message}</div>}
    </>
  )
}

코드 흐름
1. queryKey로 요청 구분
2. queryFn으로 데이터 요청
3. 캐싱된 데이터가 있으면 반환, 없으면 새로 요청
4. 변수 변경 시 queryKey를 기준으로 자동 재요청

useMutation

useMutation Hook으로 수행되는 Mutation 요청은 POST,PUT,DELETE 요청과 같이 서버 데이터에 변경 요청을 처리할 때 사용한다.

서버와의 상호작용으로 상태를 변경하고, 실시간으로 UI를 업데이트하거나 에러를 처리하는데 유용하다.

const { mutate } = useMutation( // mutate: 요청을 트리거 하는 함수
  mutationFn, // 이 Mutation 요청을 수행하기 위한 Promise를 Return 하는 함수 (required)
  options, // useMutation에서 사용되는 Option 객체 (optional)
// onSuccess: 요청 성공 콜백 함수, onError: 요청 실패 콜백 함수, onSettled: 성공 여부 상관없이 콜백 함수
);
import React from "react";
import { useMutation } from "react-query";
import axios from "axios";

const addTodo = async (newTodo) => {
  const response = await axios.post("/api/todos", newTodo);
  return response.data;
};

const TodoApp = () => {
  const { mutate, isLoading, isError, isSuccess } = useMutation(addTodo, {
    onSuccess: (data) => {
      console.log("Todo 추가 성공:", data);
      alert("새로운 Todo가 추가되었습니다!");
    },
    onError: (error) => {
      console.error("Todo 추가 실패:", error);
      alert("오류가 발생했습니다. 다시 시도해주세요.");
    },
  });

  const handleAddTodo = () => {
    const newTodo = { title: "React Query 배우기", completed: false };
    mutate(newTodo);
  };

  return (
    <div>
      <h1>Todo List</h1>
      <button onClick={handleAddTodo} disabled={isLoading}>
        {isLoading ? "추가 중..." : "Todo 추가"}
      </button>
      {isSuccess && <p>Todo가 성공적으로 추가되었습니다!</p>}
      {isError && <p>오류가 발생했습니다. 다시 시도해주세요.</p>}
    </div>
  );
};

export default TodoApp;

React Query로 서버 상태를 관리하기

클라이언트 상태와 서버 상태는 웹 애플리케이션에서 데이터를 관리할 때 핵심적으로 구분해야 할 개념이다. React Query는 서버 상태 관리를 간소화하는 데 중점을 둔 라이브러리이다.

클라이언트 상태 vs 서버 상태

구분클라이언트 상태서버 상태
정의사용자의 브라우저 내에서 관리되는 데이터서버에 저장되어 클라이언트 요청으로 가져오는 데이터
저장 위치브라우저 메모리 (ex. useState, useReducer 등)서버 (데이터베이스, API)
특징애플리케이션 실행 중 유지되며, 초기화되기 쉬움외부에서 관리되며, 항상 최신 상태로 동기화 필요
갱신 방식로컬에서 즉시 갱신서버로 요청을 보내 데이터를 가져오거나 갱싱
예시모달 열림 여부, 입력 폼 상태, 페이지 로컬 필터 값유저 정보, 상품 목록 주문 상태 등
관리의 복잡성간단 (내부 로직으로 제어 가능)복잡 (네트워크 요청, 에러 처리 등 필요)

[ React Query의 역할 ]

React Query는 서버 상태를 가져오고, 캐싱하며, 필요할 때 자동으로 데이터를 갱신하는 작업을 처리해준다. 이를 통해 클라이언트는 데이터 요청 및 관리의 복잡성을 줄이고, 핵심 로직에만 집중할 수 있다.

React Query를 활용한 서버 상태 관리 방법

  1. Query Key로 데이터 상태를 명확히 정의

    • Query Key는 배열 형태로 구성
    • 첫 번째 값은 데이터의 종류를 정의, 이후에는 동적으로 변경되는 변수들을 추가
    const { data } = useQuery(['users', userId], fetchUserData);

    Query Key를 정의함으로써, React Query는 서버에서 데이터를 가져오거나 캐시된 데이터를 반환할 때 이 키를 기준으로 상태 추적

  2. 데이터 캐싱과 staletime의 활용

    • 데이터 캐싱: 데이터를 가져온 후, 기본적으로 React Query는 해당 데이터를 메모리에 저장하고, 동일한 요청에 대해 다시 서버로 요청을 보내지 않아 불필요한 네트워크 요청 방지한다.
    • staleTime: 데이터를 얼마나 “신선한” 상태로 유지할지 설정하는 옵션이다. staleTime을 늘리면 데이터를 오래 동안 fresh 상태로 유지할 수 있어 불필요한 요청을 줄이고, 사용자에게 빠른 응답을 제공할 수 있다.
    const { data } = useQuery(
      ['posts'],
      fetchPosts,
      {
        staleTime: 1000 * 60 * 5 // 5분 동안 데이터를 fresh 상태로 유지
      }
    );
  3. 네트워크 재연결, 요청 실패 등의 자동 갱신

    • 자동 재연결: 네트워크 연결이 끊어진 경우, React Query는 자동으로 네트워크가 재연결되면 데이터를 다시 가져온다.
    • 요청 실패 처리: 요청이 실패할 경우, React Query는 자동으로 재시도하거나, 옵션에 따라 수동으로 재시도를 유도할 수 있다.
    const { data, error } = useQuery(
      'posts',
      fetchPosts,
      {
        retry: 3, // 요청 실패 시 최대 3번까지 재시도
        refetchOnWindowFocus: true // 윈도우 포커스를 다시 얻을 때 자동으로 데이터 갱신
      }
    );

마무리

React Query를 알기 전, fetch나 axios로 데이터를 가져오고, 로딩 상태를 관리하며, 실패 시 재시도 로직, 에러 메세지를 표시하는 작업을 전부 직접 구현하며 서버 상태를 관리했다. 이 방식은 코드가 반복적이고, 상태관리가 복잡해지는 문제가 있었다. 또한 모든 상황을 일일이 다뤄야 해서 코드의 가독성이나 유지 보수가 어려웠다.

이런 문제를 해결하기 위해 React Query를 적용해보았고 이전에 모든 경우를 직접 설정해야 했지만, 자동 캐싱과 데이터의 자동 갱신을 통해 데이터를 효율적으로 관리해 주었다. 이후 서버 상태 관리가 훨씬 간소화되고, 코드의 중복도 줄어드는 등 많은 장점이 있었다.

이전처럼 단순히 코드 구현에만 집중하는 것이 아니라, 하나의 API 연동하더라도 서버와 클라이언트 간의 데이터 흐름 등 고려해야하는 부분이 많다는 것을 느꼈다. 앞으로도 더 나은 코드 품질과 성능을 위해 다양한 도구와 패턴을 공부하며 적용할 것이다.

Ref
카카오페이 프론트엔드 개발자들이 React Query를 선택한 이유
TanStack Query(React Query) 핵심 정리

0개의 댓글