[React Query] 리액트 쿼리 시작하기

민태홍·2022년 12월 8일
0

React

목록 보기
5/5

Overview

React-Query란 ?

React Query는 데이터 Fetching, 캐싱, 동기화, 서버 쪽 데이터 업데이트 등을 쉽게 만들어 주는 React 라이브러리입니다.

사용 이유 ?

그렇다면 사용하는 이유는 무엇일까요 ?
먼저 프론트엔드 개발을 하는 사람들이 가장 많이 하는 고민중에 하나는 바로 '상태관리' 입니다. 프론트엔드 개발자라면 상태관리와 뗄 수 없는 인연을 가지고 있습니다. 그리고 많은 사람들이 상태관리를 위해 Redux를 사용합니다.
그리고 Redux를 이용하여 서버 데이터를 활용하기 위해서는 Redux-saga와 같은 다른 미들웨어를 사용해야 합니다. 하지만 프로젝트를 계속 진행하면서 API가 계속 추가되고 API마다 액션과 액션 타입, Saga 파일 등으로 인해 프로젝트의 구성이 복잡해지는 문제성을 갖고 있습니다.

이 뿐만 아니라 서버로 부터 값을 가져오거나 업데이트 하는 로직을 store 내부에 개발하는 경우가 많습니다. 그렇다보니 store는 클라이언트 state를 유지해야하는데 어느 순간부터 store에 클라이언트 데이터와 서버 데이터가 공존 하게 됩니다.
서버 데이터를 위한 로직이 과도하게 커지고, 그로 인해서 Redux 를 활용하기 위한 보일러 플레이트가 비대해 진다는 점이 문제점이 됩니다.

그래서 React Query를 활용해 클라이언트와 서버의 데이터를 분리하여 사용합니다.

React Query

장점

  • 데이터를 캐싱한다. (캐싱 된 데이터로 인해서 API 콜을 줄여주며 서버에 대한 부담을 줄여준다)
  • client와 server의 데이터를 분리해준다.
  • get을 한 데이터에 대해 update를 하면 자동으로 get을 다시 수행한다. (예를 들면 게시판의 글을 가져왔을 때 게시판의 글을 생성하면 게시판 글을 get하는 api를 자동으로 실행 )
  • 데이터가 오래 되었다고 판단되면 다시 get 요청을 보낸다. (invalidateQueries)
  • 동일 데이터 여러번 요청하면 한번만 요청한다. (옵션에 따라 중복 호출 허용 시간 조절 가능)
  • 무한 스크롤 (Infinite Queries (opens new window))
  • 비동기 과정을 선언적으로 관리할 수 있다.
  • react hook과 사용하는 구조가 비슷하다.

사용법

react-query 설치

$ yarn add react-query
$ npm i react-query

QueryClientProvider

먼저 react의 가장 기본이 되는 곳에 react-query를 사용하도록 세팅합니다.

/* index.js */
 import { QueryClient, QueryClientProvider } from 'react-query'
 
 const queryClient = new QueryClient()
 
 export default function App() {
   return (
     <QueryClientProvider client={queryClient}>
       <Example />
     </QueryClientProvider>
   )
 }

react-query 맛보기

import { useQuery, useMutation, useQueryClient, QueryClient, QueryClientProvider } from "react-query";
import { getTodos, postTodo } from "../my-api";

// Create a client
const queryClient = new QueryClient();

function App() {
	return (
		// Provide the client to your App
		<QueryClientProvider client={queryClient}>
			<Todos />
		</QueryClientProvider>
	);
}

function Todos() {
	// Access the client
	const queryClient = useQueryClient();

	// Queries
	const query = useQuery("todos", getTodos);

	// Mutations
	const mutation = useMutation(postTodo, {
		onSuccess: () => {
			// Invalidate and refetch
			queryClient.invalidateQueries("todos");
		},
	});

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

			<button
				onClick={() => {
					mutation.mutate({
						id: Date.now(),
						title: "Do Laundry",
					});
				}}
			>
				Add Todo
			</button>
		</div>
	);
}

 render(<App />, document.getElementById('root'))

위의 예제는 공식문서에 나와있는 예제입니다. 이 예제는 react-query의 3가지 중요한 컨셉을 보여줍니다.

  • Queries
  • Mutations
  • Query Invalidation

Quesries

import { useQuery } from 'react-query'
 
 function App() {
   const info = useQuery('todos', fetchTodoList, options)
 }

먼저 Queries의 사용법은 위와 같습니다.
useQuery는 서버에서 데이터를 가져오기(get) 위해 사용하는 hook 입니다. unique key, promise 기반의 함수, 옵션 값을 파라미터로 받아서 동작합니다. unique key는 애플리케이션 전역에서 해당 쿼리를 refetching, caching, sharing 하는 용도로 사용되며, 쿼리의 리턴 값으로는 status, data, error와 같은 템플릿을 포함하여 데이터 사용에 필요한 정보가 제공됩니다.

  1. 첫번째 인자로, unique key를 명시해줍니다.
    ✔️ 해당 key는 내부적으로 데이터 재요청, 캐싱, 쿼리를 공유하기 위해 사용된다.
  2. 두번째 인자에는 우리가 요청할 비동기 함수를 넣어주는데, 데이터와 error를 return해줍니다.
  3. 세번째 인자에는 성공과 실패와 같은 경우에 옵션을 넣어줄 수 있습니다.
status
  • isLoading
    쿼리에 데이터가 없고 fetching 하는 상태.
  • isError
    쿼리에 에러가 발생한 상태.
  • isSuccess
    쿼리가 성공적으로 실행되었고 데이터를 사용가능한 상태.
  • isIdle
    쿼리를 사용할 수 없는 상태. (disabled)
  • error
    쿼리가 isError 상태인 경우 에러 정보 확인을 위해 사용하는 프로퍼티.
  • data
    쿼리가 isSucess 상태인 경우 데이터 사용을 위해 사용하는 프로퍼티.
  • isFetching
    쿼리의 fetching/refetching 여부에 대한 boolean 값.
const {
  isSuccess,
  isError,
  isLoading,
  isFetching,
  data,
  error
} = useQuery(
  'todos',
  fetchTodoList,
  {
    onSuccess: (data) => {
      console.log('onSuccess', data);
    },
    onError: (error) => {
      console.log('onError', error);
    }
  }
);

if (isFetching) {
  console.log('fetching...');
}

if (isLoading) {
  console.log('loading...');
}

if (isError) {
  console.log('error', error);
}

if (isSuccess) {
  console.log('success', data);
}

위와 같이 상태값에 따라 다른 로직을 실행하도록 useQuery를 활용할 수 있으며 isLoading, isError, isSuccess는 status로 통일해서 사용할 수 있습니다.

if (status === 'loading') {
  console.log('loading...');
}

if (status === 'error') {
  console.log('error', error);
}

if (status === 'success') {
  console.log('success', data);
}
Query Keys
  • useQuery에서 파라미터로 사용되는 Query Key는 React Query에서 쿼리 캐싱을 관리하기 위한 unique key로 사용됩니다.
  • 문자열, 배열 , 중첩된 객체등 어떤 형태로든 가능하다.
  • Query key가 순차적 진행을 보장하는 직렬화 기법으로 쿼리의 데이터는 유일하다.
    ✔️ Query key가 문자열일 때
// A list of todos
 useQuery('todos', ...) // queryKey === ['todos']
 
 // Something else, whatever!
 useQuery('somethingSpecial', ...) // queryKey === ['somethingSpecial']

✔️ Query key가 배열일 때

// An individual todo
 useQuery(['todo', 5], ...)
 // queryKey === ['todo', 5]
 
 // An individual todo in a "preview" format
 useQuery(['todo', 5, { preview: true }], ...)
 // queryKey === ['todo', 5, { preview: true }]
 
 // A list of todos that are "done"
 useQuery(['todos', { type: 'done' }], ...)
 // queryKey === ['todos', { type: 'done' }]

✔️ Query key에 있는 object들의 순서는 중요하지 않다.

동일한 Query key -> array의 object내 에 있는 값들의 순서는 중요하지 않음 (동일함)

useQuery(['todos', { status, page }], ...)
useQuery(['todos', { page, status }], ...)
useQuery(['todos', { page, status, other: undefined }], ...)
//동일 하지 않은 Query key. -> array 값의 순서는 중요하다
useQuery(['todos', status, page], ...)
useQuery(['todos', page, status], ...)
useQuery(['todos', undefined, page, status], ...)
Query Functions
  • Query Function은 promise를 return하는 함수이다.
  • Promise는 data를 return하거나 에러가 나면 에러를 return한다.
useQuery(['todos'], fetchAllTodos)
useQuery(['todos', todoId], () => fetchTodoById(todoId))
useQuery(['todos', todoId], async () => {
   const data = await fetchTodoById(todoId)
   return data
 })
useQuery(['todos', todoId], ({ queryKey }) => fetchTodoById(queryKey[1]))

아래의 코드는 에러를 핸들링해서 throw해주는 예제이다.

const { error } = useQuery(['todos', todoId], async () => {
   if (somethingGoesWrong) {
     throw new Error('Oh no!')
   }
   return data
 })
useQuery 비동기적으로 사용하기

useQuery는 기본적으로 비동기로 동작합니다. useQuery에 다음과 같이 enabled 옵션을 false로 사용하면 동기적으로 사용할 수 있습니다.

enabled 옵션을 false로 사용하게 되면 컴포넌트가 mount 되거나 window focus 되어도 쿼리가 자동으로 실행되지 않습니다. 또한 queryClient에서 invalidateQueries 또는 refetchQueries 함수를 호출해도 refetching 되지 않습니다. 쿼리는 캐싱되지 않은 idle 상태이며 fetching을 위해서는 refetch 함수를 트리거로 사용해야 합니다.

const {
  isSuccess,
  isError,
  isLoading,
  isFetching,
  data,
  error
} = useQuery(
  'todos',
  fetchTodoList,
  {
    enabled: false,
    onSuccess: (data) => {
      console.log('onSuccess', data);
    },
    onError: (error) => {
      console.log('onError', error);
    }
  }
);
useQueries를 이용하여 useQuery 여러개 사용하기

useQuery는 기본적으로 비동기로 동작하기 때문에 컴포넌트 내에 useQuery가 여러 개 있다면 순서대로 실행되지 않고 동시에 실행됩니다.

이러한 경우에 다음과 같이 useQueries를 이용하면 여러개의 쿼리를 하나로 묶어서 사용할 수 있습니다. 아래 예제를 실행하면 UseQueryResult가 배열로 반환됩니다.

const users = [1,2,3,4,5]
const userQueries = useQueries(
     users.map(user => {
       return {
         queryKey: ['user', user],
         queryFn: () => fetchUserById(user),
       }
     })
   )

useQuery의 옵션

useQuery에는 다양한 옵션을 사용할 수 있는데 이 중에서 몇가지 유용한 옵션에 대해 정리하면 다음과 같습니다.

  • enabled
    false로 설정하면 쿼리가 자동으로 실행되지 않음.
  • retry
    쿼리가 실패한 경우에 대한 재시도 횟수
  • staleTime
    데이터가 stale state로 변경되는 시간 (Infinity로 설정하면 stale state로 변경되지 않음)
  • cacheTime
    inactive state의 캐시 데이터가 메모리에 남아있는 시간
  • refetchInterval
    설정한 시간(밀리초)에 따라 주기적으로 fetching 실행.
  • refetchOnWindowFocus
    창에 포커스가 된 경우에 대한 refetch 여부.
  • initialData
    쿼리의 초기값 설정

useMutation

useMutation은 서버를 대상으로 데이터를 수정 (create, update, delete) 하기 위해 사용하는 hook 입니다.
리턴값과 사용하는 방법은 useQuery와 비슷합니다.

  • isIdle
    mutation이 실행 되지 않아 아직 캐싱되지 않은 상태.
  • isLoading
    mutation이 실행중인 상태.
  • isError
    mutation에 에러가 발생한 상태.
  • isSuccess
    mutation이 성공적으로 실행되었고 데이터를 사용 가능한 상태.
  • error
    mutation이 isError 상태인 경우 에러 정보 확인을 위해 사용하는 프로퍼티.
  • data
    mutation이 isSucess 상태인 경우 데이터 사용을 위해 사용하는 프로퍼티.

다음 예제를 통해 useMutation의 사용 방법에 대해 알아보겠습니다. useMutation에 사용한 파라미터는 순서대로 다음과 같은데 파라미터의 구성도 useQuery와 동일합니다.

  • mutationKey
    mutation에 사용할 unique key 값.
  • mutationFn
    mutation에 사용할 promise 기반의 비동기 API 함수.
  • options
    mutation에 사용할 옵션 값.
const mutation = useMutation(
  'addUser',
  addUserFuc,
  {
    onMutate: (variables) => {
      console.log('onMutate', variables);
    },
    onError: (error, variables, context) => {
      console.log('onError', context);
    },
    onSuccess: (data, variables, context) => {
      console.log('onSuccess', data);
    },
    onSettled: (data, error, variables, context) => {
      console.log('onSettled', data);
    }
  }
);

위에 사용된 옵션은 다음과 같습니다.

  • onMutate
    mutation이 실행되기 전에 실행되는 함수.
    mutationFn에 전달되는 파라미터를 동일하게 받음.
    mutation 실패시 onError, onSettled 함수에 return 값을 전달함.
    rollback 처리가 필요한 경우에 사용되며 return 값은 context 파라미터로 사용 가능.
  • onError
    mutation 실행과정에서 에러가 발생했을때
  • onSuccess
    mutation이 성공 했을 때
  • onSettled
    mutation 성공 또는 실패시 데이터나 에러를 전달.
useMutation에서 쿼리 Invalidation 처리하기

일반적으로 mutation이 성공적으로 동작한 이후에는 다른 관련된 쿼리의 refetch를 필요로 할 가능성이 높습니다. 이러한 경우엔 다음과 같이 QueryClient의 invalidQueries 함수를 사용해줍니다. 이렇게 하면 mutation 성공 이후에 해당 쿼리가 stale 상태로 변경 되어 캐시에서 삭제되고 refetch가 실행되게 됩니다.

const mutation = useMutation(postTodo, {
  onSuccess: () => {
    // postTodo가 성공하면 todos로 맵핑된 useQuery api 함수를 실행합니다.
    queryClient.invalidateQueries("todos");
  }
});

만약 mutation에서 return된 값을 이용해서 get 함수의 파라미터를 변경해야할 경우 setQueryData를 사용합니다.

const queryClient = useQueryClient();

const mutation = useMutation(editTodo, {
  onSuccess: data => {
    // data가 fetchTodoById로 들어간다
    queryClient.setQueryData(["todo", { id: 5 }], data);
  }
});

const { status, data, error } = useQuery(["todo", { id: 5 }], fetchTodoById);

mutation.mutate({
  id: 5,
  name: "nkh"
});

react Suspense와 react-query 사용하기

react-query를 사용하는 또 하나의 이유는 비동기를 좀 더 선언적 사용할 수 있어서 인 것 같습니다.
Suspense (opens new window)를 사용하며 loading을, Error buundary (opens new window)를 사용하여 에러 핸들링을 더욱 직관적으로 할 수 있습니다.
suspense를 사용하기 위해 QueryClient에 옵션을 하나 추가합니다. 아래 방법은 global하게 suspense를 사용한다고 정의할 때 예시입니다.

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      retry: 0,
      suspense: true
    }
  }
});

ReactDOM.render(
  <React.StrictMode>
    <QueryClientProvider client={queryClient}>
      <App />
    </QueryClientProvider>
  </React.StrictMode>,
  document.getElementById("root")
);

아래는 suspense를 사용하는 예제입니다.

const { data } = useQurey("test", testApi, { suspense: true });

위처럼 세팅을 완료 했을 경우 react에서 제공하는 Suspense를 사용하면 됩니다.

const { data } = useQurey("test", testApi, { suspense: true });

return (
  // isLoading이 true이면 Suspense의 fallback 내부 컴포넌트가 보여집니다.
  // isError가 true이면 ErrorBoundary의 fallback 내부 컴포넌트가 보여집니다.
  <Suspense fallback={<div>loading</div>}>
    <ErrorBoundary fallback={<div>에러 발생</div>}>
      <div>{data}</div>
    </ErrorBoundary>
  </Supense>
);

위와 같이 react query를 왜 사용하는지 장점은 무엇이 있는지, 사용법은 어떻게 되는지와 관련한 글이었습니다.

0개의 댓글