[React] React-Query 사용 이유 & Queries 개념편

조예진·2022년 7월 17일
34
post-thumbnail

📝 들어가기 전

회사에서 커스텀 훅 기반으로 React 프로젝트를 다시 구축하게 되면서
React-Query 라이브러리도 사용하게 되었다.
그래서 React-Query에 대해 한번 깊게 파보기 위해 차근차근 포스팅해보려고 한다.

우선, 공식문서 토대로 번역해서 요약한 거라 의역이 조금 많이 되어 있어
혹시라도 추가해야하거나 부족한 부분이 있다면 댓글로 마구마구 피드백 감사드립니다:)


React-Query란?

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

React-Query가 만들어진 이유

✅ 서버 쪽의 데이터들을 좀더 쉽게 관리하기 위해

✔️ 기존에 Redux, Mobx, Recoil과 같은 다양하고 훌륭한 상태 관리 라이브러리들이 있긴 하지만, 클라이언트 쪽의 데이터들을 관리하기에 적합할 순 있어도 서버 쪽의 데이터들을 관리하기에는 적합하지 않은 점들이 있다.

이 점을 해결하기 위해 React-Query가 만들어졌다.

React-Query의 장점

✔️ 위에서 설명한 것처럼 React 어플리케이션 내에서 데이터 패칭, 캐싱, 동기적, 그리고 서버의 상태의 업데이트를 좀 더 용이하기 위해 만들어준다.

✔️ 기존에는 직접 만들어서 사용했던 기능들을 별도의 옵션으로 지원하여 복잡하고 이해할 수 없는 수많은 코드를 대신 React-Query 로직을 통해 짧은 코드로 대체할 수 있게 되었다.

✔️ 프로젝트 구조가 기존보다 단순해져 애플리케이션을 유지 보수하기 쉽고, 새로운 기능을 쉽게 구축할 수 있다.

그 외에도 많은 장점이 있다.

→ React Query는 별도의 설정 없이 즉시 사용 가능하다

→ 캐싱을 효율적으로 관리해준다

→ 같은 데이터에 대한 여러번의 요청이 있을 시 중복을 제거한다.

→ 백그라운드에서 알아서 오래된 데이터를 업데이트해준다.

→ 데이터 업데이트 시 최대한 빨리 반영한다.

→ 페이징처리, 지연 로딩 데이터와 같은 성능 최적화해준다

→ 서버 쪽 데이터를 가비지 컬렉션을 이용하여 자동으로 메모리를 관리해준다.

→ 구조적 공유를 통해 쿼리의 결과를 기억해준다.


React-Query 간단하게 맛보기

import {
   useQuery,
   useMutation,
   useQueryClient,
   QueryClient,
   QueryClientProvider,
 } from 'react-query'

import { getTodos, postTodo } from '../my-api'

//먼저 client를 만들어준다.
const queryClient = new QueryClient()

function App() {
	return (
		// 애플리케이션에 클라이언트를 제공한다.
		<QueryClientProvider client={queryClient}>
			<Todos />
		</QueryClientProvider>
	)
}

function Todos() {

	// client에 접근하기
	const queryClient = useQueryClient()

	// Queries (데이터 패칭하기 위한 hook)
	const query = useQuery('todos', getTodos)

	// Mutation
	const mutation = useMutation(postTodo, {
		onSuccess: () => {
				// todos 라는 unique key에 대한 기존 데이터를 무효화하고 다시 가져오기
				queryClient.invalidateQuries('todos')
		},
		})
	})
	return (
	<div>
       <ul>
		// useQuery로 가져온 데이터는 아래와 같이 query.data로 꺼내올 수 있다.
         {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>
)
}

React-query 깊게 파보기

❇️ useQuery, useMutation 등 React-query에서 제공하는 hook에는 정말 다양하고 개발자들을 편리하게 해주는 옵션들이 많다!

이번편에서는 그 중 Queries 관련한 내용에 대해서 자세히 다뤄볼 예정이다.

다음 포스팅에서는 Mutation내용으로 이어나갈 예정이다


📍  Queries

Query Basic

✅ 사용법

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

✅ Query는 GET, POST 메소드 포함을 기반으로 서버에서 데이터를 패칭하기 위한 비동기 함수와 사용된다.

✅ 'todos': 첫번째 인자로, unique key를 명시해준다.
✔️ 해당 key는 내부적으로 데이터 재요청, 캐싱, 쿼리를 공유하기 위해 사용된다.

✅  fetchTodoList: 두번째 인자에는 우리가 요청할 비동기 함수를 넣어주는데, 데이터와 error를 return해준다.

✅  useQuery return값에 포함되어 있는 states

  • 주요 states
    - isLoading or status === 'loading' : 현재 데이터를 요청 중이나 아직 데이터가 없을 경우
    - isError or status === 'error': 쿼리에서 에러가 났을 경우
    error : 해당 property로 에러 메세지를 확인할 수 있다.
    - isSuccess or status === 'success' - 쿼리 요청 성공
    data : 해당 property로 성공한 데이터를 확인할 수 있다.
    - isIdle or status === 'idle': 이 쿼리는 현재 사용할 수 없을 때 나옴
    - isFetching : 데이터 요청 중일 때는 (내부적으로 리패칭 중 일때도 포함) 항상 True를 리턴한다
  • 사용방법
    - 방법 1. boolean 타입으로 체크하기
       function Todos() {
       const { isLoading, isError, data, error } = useQuery('todos', fetchTodoList)
        
        if (isLoading) {
          return <span>Loading...</span>
        }
       
        if (isError) {
          return <span>Error: {error.message}</span>
        }
       
        // We can assume by this point that `isSuccess === true`
        return (
          <ul>
            {data.map(todo => (
              <li key={todo.id}>{todo.title}</li>
            ))}
          </ul>
        )
       }
    • 방법 2. status 하나로 꺼내서 사용할 수도 있음
      function Todos() {
         const { status, data, error } = useQuery('todos', fetchTodoList)
       
         if (status === 'loading') {
           return <span>Loading...</span>
         }
       
         if (status === 'error') {
           return <span>Error: {error.message}</span>
         }
       
         // also status === 'success', but "else" logic works, too
         return (
           <ul>
             {data.map(todo => (
               <li key={todo.id}>{todo.title}</li>
             ))}
           </ul>
         )
       }

📍 Query Keys

✅ React Query는 query 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 key는 파라미터를 사용해도 된다.

파라미터가 unique한 값이라면 인자로 받아 아래와 같이 사용할 수 있다.

function Todos({ todoId }) {
   const result = useQuery(['todos', todoId], () => fetchTodoById(todoId))
 }

📍Query Functions

✅ Query Function은 promise를 return하는 함수

✅ Promise는 data를 return하거나 에러가 나면 에러를 return한다.

아래와 같이 function을 작성할 수 있다.

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]))

✅ Query Function에서 발생된 에러는 query의 error state에 남는다.

const { error } = useQuery(['todos', todoId], async () => {
   if (somethingGoesWrong) {
     throw new Error('Oh no!')
   }
 
   return data
 })

✅ fetch와 같이 기본적으로 Http 요청 실패에 대한 에러를 제공하지 않는 경우 아래와 같이 직접 error를 던져줘야한다.

✔️ axios와 graphql-request과 같은 대부분의 함수가 기본적으로 HTTP 요청이 실패하면 에러를 반환하지만,

fetch와 같이 몇몇 함수는 오직 network error에 대해서만 에러를 내뱉는 경우가 있다.

이 경우에 아래와 같은 방법으로 에러를 직접 반환해줘야한다.


useQuery(['todos', todoId], async () => {
   const response = await fetch('/todos/' + todoId)
   if (!response.ok) {
     throw new Error('Network response was not ok')
   }
   return response.json()
 })

📝  fetch 공식문서 발췌

[fetch()](https://developer.mozilla.org/en-US/docs/Web/API/fetch) promise only rejects when a network error is encountered (which is usually when there's a permissions issue or similar). A [fetch()](https://developer.mozilla.org/en-US/docs/Web/API/fetch) promise does not reject on HTTP errors (404
, etc.).

✅ Query Function Variables

Query keys는 데이터를 요청하는동안 식별한는 것 뿐만아니라 또한 필요할 때 편리하게 Query Function에서 사용도 할 수 있다.

이렇게 하면, 필요할 때마다 아래와 같이 Query Function 에서 추출해서 사용할 수 있다.

function Todos({ status, page }) {
   const result = useQuery(['todos', { status, page }], fetchTodoList)
 }
 
 // Access the key, status and page variables in your query function!
 function fetchTodoList({ queryKey }) {
   const [_key, { status, page }] = queryKey
   return new Promise()
 }

✅ 여러개의 인자가 아니라 객체 하나로도 사용할 수 있다.

import { useQuery } from 'react-query'
 
 useQuery({
   queryKey: ['todo', 7],
   queryFn: fetchTodo,
   ...config,
 })

📍Parallel Queries : **useQueries**

✅ 여러개의 useQuery를 나란히 사용할 수 있다.

→ 아래와 같이 사용할 시 다른 쿼리가 실행되기 전에 첫번째 쿼리가 중단하고 에러를 반환할 수 있다.

function App () {
   // The following queries will execute in parallel
   const usersQuery = useQuery('users', fetchUsers)
   const teamsQuery = useQuery('teams', fetchTeams)
   const projectsQuery = useQuery('projects', fetchProjects)
   ...
 }

✔️ 그럴땐 , **useQueries hook을 사용할 수 있다.

useQueries는 Query Option 객체를 배열로 받을 수 있고, 마찬가지로 결과값 또한 배열로 return 해준다.

function App({ users }) {
   const userQueries = useQueries(
     users.map(user => {
       return {
         queryKey: ['user', user.id],
         queryFn: () => fetchUserById(user.id),
       }
     })
   )
 }

📍 Dependent Queries: 동기적으로 사용하기

✅ enabled: true이면 특정 조건이 trued 때만 실행하겠다

→ 이전 함수가 끝나고 다음 것이 영향을 받을 때 enabled option을 사용하면 좋다.

const { data: user } = useQuery(['user', email], getUserByEmail)
 
 const userId = user?.id
 
 // Then get the user's projects
 const { isIdle, data: projects } = useQuery(
   ['projects', userId],
   getProjectsByUser,
   {
     // The query will not execute until the userId exists
     enabled: !!userId, 
   }
 )

📍 Loading state 전역으로 사용하기

✅ useIsFetching

→  useQuery 내부 isLoading state 말고 backgroud에서의 fetching 체크가능하다

import { useIsFetching } from 'react-query'
 
 function GlobalLoadingIndicator() {
   const isFetching = useIsFetching()
 
   return isFetching ? (
     <div>Queries are fetching in the background...</div>
   ) : null
 }

--

profile
블로그 이전 중 -> https://devjooj.tistory.com/

1개의 댓글

comment-user-thumbnail
2023년 2월 7일

정리가 깔끔해서 이해하기 좋았습니다
좋은글 감사합니다!

답글 달기