[React] React Query를 사용한 UX/DX 향상

Joowon Jang·2025년 4월 7일

React Query

React Query는 TanStack에서 제공하는 서버 상태 관리를 위한 라이브러리이다.
여기서 서버 상태란 간단하게, 서버에서 가져온 데이터의 상태를 말한다.
(ex - 게시판의 글 목록, 검색 필터의 검색구분 목록(제목, 내용, 제목+내용 등), 날씨 데이터 등)

Rest API나 GraphQL을 사용할 때 유용하며, 서버 상태 관리를 쉽게 할 수 있다는 점 외에도 여러 기능을 제공하여 UX/DX 향상에 사용하기 좋다고 생각한다.

TanStack Query는 Vue, Angular, Svelte 등 다양한 프레임워크를 위한 라이브러리를 제공하며, 여기서 다루는 React Query는 @tanstack/react-query이다.

패키지 설치

npm i @tanstack/react-query
npm i -D @tanstack/react-query-devtools # 서버에서 데이터를 가져온 시간을 확인할 수 있는 도구

주요 기능

Query

useQuery 훅을 사용해 서버에서 데이터를 가져온다.

  • useQuery 훅의 매개변수는 queryKey, queryFn 등의 속성으로 이루어진 객체를 받는다.
  • 다른 컴포넌트에서 서버 데이터를 가져와도 queryKey가 동일하면 같은 데이터로 간주하여 캐싱된 데이터를 사용한다.
  • isLoading, error 상태를 제공하기 때문에 로딩중, 에러 발생에 대한 처리를 간편하게 할 수 있다.
  • staleTime 등의 옵션을 설정하여 상태 관리를 효율적으로 할 수 있다.
// 사용 예시
import { useQuery } from '@tanstack/react-query';

// fetch 함수를 컴포넌트 외부로 분리
const fetchUsers = async () => {
  const res = await fetch('/api/users');
  if (!res.ok) throw new Error('Network response was not ok');
  return res.json();
};

function Users() {
  const { data, isLoading, error } = useQuery({
	queryKey: ['users'],
    queryFn: fetchUsers
  });

  if (isLoading) return <p>로딩 중...</p>;
  if (error) return <p>에러 발생: {error.message}</p>;

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

Mutation

useMutation 훅을 사용해 서버의 데이터를 변경한다.

  • useMutation 훅의 매개변수로 mutationFn, onSuccess, onError 등의 속성으로 이루어진 객체를 받는다.
  • onSuccess, onError, onSettled 메서드를 설정하여 try, catch 등을 사용하는 대신에 직관적인 콜백처리를 할 수 있다.
  • isPending, isError 등의 상태를 제공하기 때문에, useState로 별도의 상태를 관리하지 않고도 button의 disabled 속성을 조절하는 등의 처리를 간편하게 할 수 있다.
  • 낙관적 업데이트(Optimistic Update)를 지원하며, 실패 시 롤백도 가능하다.
  • useQuery와 함께 사용할 때, 서버의 데이터를 변경함에 따라 useQuery로 가져온 데이터를 자동으로 최신화할 수 있다. (onSuccess 설정하여 별도의 refetch 필요없음)
// 사용 예시
import { useMutation, useQueryClient } from '@tanstack/react-query';

const addUser = async ({ name }: { name: string }) => {
  const res = await fetch('/api/users', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ name }),
  });
  if (!res.ok) throw new Error('Failed to add user');
  return res.json();
};

function AddUser() {
  const queryClient = useQueryClient();

  const mutation = useMutation({
    mutationFn: addUser,
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['users'] });
    },
  });

  const handleAdd = () => {
    mutation.mutate({ name: '새 유저' });
  };

  return (
    <button onClick={handleAdd} disabled={mutation.isPending}>
      {mutation.isPending ? '추가 중...' : '유저 추가'}
    </button>
  );
}

PreFetch

queryClientprefetchQuery 메서드를 사용해 서버의 데이터를 미리 가져온다.

  • useQuery 훅과 동일하게 매개변수 객체에 queryKey, queryFn을 포함한다.
  • useQuery 훅과 함께 사용하여, useQuery를 실행할 때, prefetchQuery로 캐싱된 데이터가 있으면 즉시 사용하고, 없다면 서버에 요청을 보내는 방식으로 동작한다.
  • 사용자가 데이터를 요청할 것이라 예상되는 부분에 사용하여 서버 요청을 더 빨리 시작해서 서버 응답 속도가 빠른 것처럼 보여 UX를 향상시킬 수 있다.
// user 정보보기 페이지로 이동하는 링크 컴포넌트
import { useQueryClient } from '@tanstack/react-query';
import { Link } from 'react-router-dom';

function UserPreviewLink({ userId }) {
  const queryClient = useQueryClient();

  // 마우스를 링크에 올리기만 
  const handleMouseEnter = () => {
    queryClient.prefetchQuery({
      queryKey: ['user', userId],
      queryFn: () =>
        fetch(`/api/users/${userId}`).then((res) => {
          if (!res.ok) throw new Error('유저 정보를 불러오지 못했습니다.');
          return res.json();
        }),
    });
  };

  return (
    <Link
  		to={`/users/${userId}`}
  		onMouseEnter={handleMouseEnter}
  		onFocus={handleMouseEnter} // 키보드 tab 접근 시 prefetch
	>
  		유저 프로필 보기
	</Link>
  );
}
// /users/:userId
import React from 'react';
import { useParams } from 'react-router-dom';
import { useQuery } from '@tanstack/react-query';

const fetchUser = async (userId) => {
  const res = await fetch(`/api/users/${userId}`);
  if (!res.ok) throw new Error('유저 정보를 가져오지 못했습니다.');
  return res.json();
};

export default function UserDetail() {
  const { userId } = useParams();

  const {
    data: user,
    isLoading,
    isError,
    error,
  } = useQuery({
    queryKey: ['user', userId],
    queryFn: () => fetchUser(userId),
    staleTime: 1000 * 60, // 1분 동안은 캐시 유지
  });

  if (isLoading) return <p>⏳ 유저 정보를 불러오는 중...</p>;
  if (isError) return <p style={{ color: 'red' }}>❌ 오류: {error.message}</p>;

  return (
    <div style={{ padding: '1rem', border: '1px solid #ccc' }}>
      <h2>👤 유저 상세 정보</h2>
      <p><strong>ID:</strong> {user.id}</p>
      <p><strong>이름:</strong> {user.name}</p>
      <p><strong>이메일:</strong> {user.email}</p>
    </div>
  );
}

주의할 점

staleTime

useQuery 훅을 사용할 때, staleTime을 설정하면 캐싱된 데이터를 사용하므로 불필요한 서버 요청을 줄여 UX를 향상시키고 서버 부담을 줄일 수 있다.
하지만, 서버의 데이터가 자주 변경되는 데이터의 staleTime을 길게 설정하면 서버의 데이터가 변경되었음에도 최신 데이터를 가져오지 않기 때문에 사용자에게 잘못된 정보를 제공하고 오류를 유발할 수 있다.
-> 상황에 따라 알맞은 staleTime을 설정하자.

queryKey

useQuery 훅을 사용할 때, 서로 다른 데이터임에도 queryKey 옵션을 동일하게 설정하면, 오류가 발생할 가능성이 크다.
-> queryKey 설정에 주의하자. queryKey 옵션은 배열로 설정하기 때문에, 아래와 같이 설정하는 방법도 좋다고 생각한다.

const { data: user1 } = useQuery({
  queryKey: ['user', '1'],
  // ...
});
const { data: user2 } = useQuery({
  queryKey: ['user', '2'],
  // ...
});

전역 상태 관리 용도로 사용하지말 것

react-query는 서버 상태 관리를 위한 도구이지, 전역 상태를 관리하기 위하 도구가 아니다.
전역 상태를 관리할 때는 zustand, Context API 같은 전역 상태 관리에 최적화된 도구를 사용하자.

더 많은 기능이 있지만, 포스팅에서 전부 다룰 수는 없기 때문에 여기서 마치겠습니다

TanStack Query
https://tanstack.com/query/latest

사용하면서 느낀점

UX 향상에 관심이 많은 개발자로서, 이런 도구를 사용할 수 있다는 게 너무 행복했다...!
처음에는 staleTime을 무조건 길게 설정하는게 좋다고 생각하는 여러 실수들도 있었고, 사용법이 좀 헷갈렸지만 완벽히 적응한 나 칭찬해~ 😏
너무 남용하면 캐싱된 데이터가 많아져 오히려 앱 성능에 악영향을 미치진 않을까 걱정되기도 하지만 아직까지 눈에 띄는 문제는 없었으니 조심해서 사용하면 괜찮을 것 같다. 👍
그리고, Next.js같이 라우팅 기능이 포함된 프레임워크를 사용하지 않는다면 TanStack Router를 사용해서 prefetch같은 기능과 함께 사용하면 더 좋은 시너지가 있을거라 생각된다. 😁

profile
깊이 공부하는 웹개발자

0개의 댓글