이세상 쿼리가 아니다 TanStack Query다.

구름미각·2024년 3월 27일
3


React Query는 React 애플리케이션에서 데이터를 관리하고 API 요청을 처리하는 데 사용되는 라이브러리입니다. Tanstack Query는 이 라이브러리의 이전 버전인 React Query의 일부분입니다. React Query는 데이터 캐시, API 요청 및 데이터 동기화를 단순화하고 최적화하기 위해 설계되었습니다.

사용 이유:

  1. 간편한 데이터 캐시: React Query는 API 요청의 결과를 캐시하여 성능을 최적화합니다. 데이터를 캐시하고 자동으로 최신 상태를 유지하여 UI의 데이터를 업데이트할 때 불필요한 네트워크 요청을 줄일 수 있습니다.

  2. 오프라인 지원: React Query는 로컬 캐시를 사용하여 오프라인 상태에서도 데이터를 제공하고 처리할 수 있습니다. 이는 네트워크 연결이 끊어진 경우에도 애플리케이션의 성능을 유지하는 데 도움이 됩니다.

  3. API 요청 관리: React Query는 API 요청을 간편하게 처리하고 관리할 수 있습니다. RESTful API, GraphQL, WebSockets 등 다양한 유형의 API와 통합할 수 있습니다.

  4. 자동화: React Query는 데이터를 가져오고 캐시하며 오류를 처리하는 데 필요한 많은 작업을 자동화합니다. 이를 통해 개발자는 더욱 효율적으로 코드를 작성하고 관리할 수 있습니다.

  5. 쉬운 사용성: React Query는 간단하고 직관적인 API를 제공하여 쉽게 사용할 수 있습니다. React 애플리케이션에서 빠르게 통합하고 사용할 수 있습니다.

React Query는 React 애플리케이션에서 데이터 관리를 단순화하고 성능을 최적화하는 데 매우 유용한 도구입니다. 이를 통해 개발자는 더욱 빠르고 효율적으로 애플리케이션을 개발할 수 있습니다.

기초:

기본적으로 TanStack 쿼리는 공격적이지만 정상적인 기본값으로 구성됩니다. 때때로 이러한 기본값은 새로운 사용자를 당황하게 만들거나 사용자가 이를 모르는 경우 학습/디버깅을 어렵게 만들 수 있습니다. TanStack 쿼리를 계속 배우고 사용하면서 다음 사항을 염두에 두십시오.

useQuery 또는 useInfiniteQuery를 통한 쿼리 인스턴스는 기본적으로 캐시된 데이터를 오래된 것으로 간주합니다.
이 동작을 변경하려면 staleTime 옵션을 사용하여 전역적으로 쿼리별로 쿼리를 구성할 수 있습니다. staleTime을 더 길게 지정하면 쿼리가 데이터를 자주 다시 가져오지 않음을 의미합니다.

다음과 같은 경우 오래된 쿼리가 백그라운드에서 자동으로 다시 가져옵니다. :

  • 쿼리 마운트의 인스턴스가 새로 생성될 때
  • 창의 초점이 다시 맞춰질때.
  • 네트워크가 다시 연결될 때.
  • 쿼리가 선택적으로 다시 가져올 때.

이 기능을 변경하려면 refetchOnMount, refetchOnWindowFocus, refetchOnReconnectrefetchInterval과 같은 옵션을 사용할 수 있습니다.

  1. useQuery, useInfiniteQuery 또는 쿼리 관찰자의 활성 인스턴스가 더 이상 없는 쿼리 결과는 "비활성"으로 표시되고 나중에 다시 사용될 경우를 대비해 캐시에 남아 있습니다.

  2. 기본적으로 "비활성" 쿼리는 5분 후에 Garbage Collect 되어 사라집니다.
    이를 변경하려면 쿼리의 기본 gcTime1000 * 60 * 5ms(5분)가 아닌 다른 값으로 변경할 수 있습니다.

  3. 쿼리가 실패됐을 경우 오류는 캡쳐되고 UI에 표시하기 전에 기하급수적인 백오프 지연하여 자동으로 3번 다시 시도됩니다.
    이를 변경하려면 쿼리에 대한 기본 retryretryDelay 옵션을 3이 아닌 다른 값과 기본 지수 백오프 기능으로 변경할 수 있습니다.

  4. 기본적으로 쿼리 결과는 데이터가 실제로 변경되었는지 감지하기 위해 구조적으로 공유되며, 그렇지 않은 경우 useMemouseCallback과 관련된 값 안정화에 더 나은 도움을 주기 위해 데이터 참조는 변경되지 않은 상태로 유지됩니다. 이 개념이 낯설게 들린다면 걱정하지 마세요! 99.9%의 경우 이 기능을 비활성화할 필요가 없으며 이를 통해 무료로 앱 성능을 높일 수 있습니다.
    구조적 공유는 JSON 호환 값에서만 작동하며 다른 값 유형은 항상 변경된 것으로 간주됩니다. 예를 들어 큰 응답으로 인해 성능 문제가 발생하는 경우 config.structuralSharing 플래그를 사용하여 이 기능을 비활성화할 수 있습니다. 쿼리 응답에서 JSON과 호환되지 않는 값을 처리하고 데이터가 변경되었는지 여부를 계속 감지하려는 경우 config.structuralSharing으로 고유한 사용자 지정 함수를 제공하여 참조를 유지하면서 이전 응답과 새 응답에서 값을 계산할 수 있습니다. 필요에 따라.

라이프 사이클 상태

  • fetching : 데이터 요청 상태
  • fresh : 데이터가 프레시한 (만료되지 않은 상태)
    • 컴포넌트의 상태가 변경되더라도, 데이터를 다시 요청하지 않는다.
    • 새로고침하면 다시 fetching 한다.
  • stale : 데이터가 만료된 상태.
    • 한번 프론트로 내려준 서버 데이터는, 최신화가 필요한 데이터라고 볼 수 있다.
    • 그 사이에 다른 유저가 데이터를 추가, 수정, 삭제 등을 할 수 있기 때문에
    • 컴포넌트가 마운트, 업데이트되면 데이터를 다시 요청한다.
    • fresh 에서 stale 로 넘어가는 시간의 디폴트는 0이다.
  • inactive : 사용하지 않는 상태
    • 일정 시간이 지나면 가비지 콜렉터가 캐시에서 제거한다.
    • 기본값 5분
  • delete : 가비지 콜렉터에 의해 캐시에서 제거된 상태.

예제:

React Query를 사용한 간단한 예제를 제공하겠습니다. 이 예제에서는 공공 API인 JSONPlaceholder에서 포스트 데이터를 가져와서 표시하는 React 애플리케이션을 만들어 보겠습니다.

먼저, React Query를 설치합니다.

npm i @tanstack/react-query

이제 예제 코드를 작성해 보겠습니다.

import {
  useQuery,
  useMutation,
  useQueryClient,
  QueryClient,
  QueryClientProvider,
} from '@tanstack/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({ queryKey: ['todos'], queryFn: getTodos })

  // Mutations 변화
  const mutation = useMutation({
    mutationFn: postTodo,
    onSuccess: () => {
      // Invalidate and refetch 무효화 & 재패치
      queryClient.invalidateQueries({ queryKey: ['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'))

해설:

이 코드는 Tanstack의 React Query를 사용하여 간단한 To-Do 애플리케이션을 만드는 예제입니다. 이 코드에서는 useQuery, useMutation, useQueryClient, QueryClient, QueryClientProvider 등을 사용하여 React Query의 다양한 기능을 활용하고 있습니다.

주요 기능은 다음과 같습니다:

  1. QueryClient 생성: const queryClient = new QueryClient()를 사용하여 React Query의 클라이언트를 생성합니다.

  2. App 컴포넌트 내에서 QueryClientProvider 제공: QueryClientProvider를 사용하여 애플리케이션의 루트 컴포넌트인 App에 클라이언트를 제공합니다.

  3. Todos 컴포넌트 내에서 QueryClient 사용: useQueryClient 훅을 사용하여 클라이언트에 접근합니다.

  4. useQuery 훅을 통한 데이터 가져오기: useQuery 훅을 사용하여 getTodos 함수를 호출하여 To-Do 목록을 가져옵니다.

  5. useMutation 훅을 통한 데이터 변경: useMutation 훅을 사용하여 postTodo 함수를 호출하여 새로운 To-Do를 추가합니다. 성공적으로 To-Do가 추가되면 onSuccess 콜백에서 쿼리를 무효화하고 다시 가져옵니다.

  6. To-Do 목록 및 추가 버튼 렌더링: To-Do 목록과 추가 버튼을 렌더링합니다. 버튼을 클릭하면 mutation.mutate를 사용하여 새로운 To-Do를 추가합니다.

이 코드는 Tanstack의 React Query를 사용하여 간단한 상태 관리 및 비동기 데이터 처리를 보여주는 예제입니다.

쿼리 작성법

useQuery를 사용해 컴포넌트나 커스텀 훅에 있는 쿼리를 불러온다.
useQuery는 최소한 필요한 것들이 있는 데,
1. unique key: 쿼리를 가져오고, 캐싱하고, 공유하는 데 사용하는 것.
2. 에러 처리: throw new Error()
3. 함수
이 모두를 만족해야 합니다.

예시:

import { useQuery } from '@tanstack/react-query'

function App() {
  const result = useQuery({ queryKey: ['todos'], queryFn: fetchTodoList })
}

useQuery에서 반환 되는 것들:
1. isPending : true 일 경우 요청을 했으나 완료를 하지 못한 경우
2. isSuccess: true 일 경우 완료를 하였고 성공한 경우
3. isError : true 일 경우 완료를 하였지만 에러를 받아 왔을 경우
4. status: 요청을 하고 난 뒤 상태 반환 됨. 주 값은 'pending', 'error', 'success'
5. fetchStatus: 주로 'fetching', 'paused', 'idle'
6. data: 성공했을 경우 받아온 값
7. error: 성공하지 못할 경우 받아온 에러
statusfetchStatus의 다른 점:

  • success in status일 경우 idle in fetchStatus이 될 수 있지만 백그라운드에서 refetch하는 경우 fetching일 상태 일 수 있다.
  • 쿼리가 마운트 되었지만 값이 없으면 pending statusfetching fetchStatus 될 수 있지만, 네트워크 연결이 없을 경우에 paused가 될 수 있다.
  • statusdata에 대한 상태: 값을 가지고 있나?
  • fetchStatusqueryFunction에 대한 상태: 돌아가고 있는 거 맞나?

쿼리 키

가장 간단한 형태는 상수인 배열이다.
주로 일반 리스트, 인덱스 리소스, 비계층적인 리소스에 사용된다.

예시:

// A list of todos
useQuery({ queryKey: ['todos'], ... })

// Something else, whatever!
useQuery({ queryKey: ['something', 'special'], ... })

고유한 키를 표현하기 위해 더 많은 정보가 필요한 경우 문자열과 직렬화된 개체가 포함된 배열을 사용한다.

  • 계층적, 중첩된 리소스
    • 일반적으로 항목을 고유하게 식별하기 위해 ID, 색인 또는 기타 기본 요소를 전달한다.
  • 추가 매개변수가 포함된 쿼리
    - 일반적으로 추가 옵션의 객체를 전달한다.
    예시:
// An individual todo
useQuery({ queryKey: ['todo', 5], ... })

// An individual todo in a "preview" format
useQuery({ queryKey: ['todo', 5, { preview: true }], ...})

// A list of todos that are "done"
useQuery({ queryKey: ['todos', { type: 'done' }], ... })

쿼리 키는 결정적으로 해시된다. 객체 안의 아이템의 순서가 바뀌어도 동일하게 인식된다.

useQuery({ queryKey: ['todos', { status, page }], ... })
useQuery({ queryKey: ['todos', { page, status }], ...})
useQuery({ queryKey: ['todos', { page, status, other: undefined }], ... })

근데 배열일 경우에는 순서가 중요하다.

useQuery({ queryKey: ['todos', status, page], ... })
useQuery({ queryKey: ['todos', page, status], ...})
useQuery({ queryKey: ['todos', undefined, page, status], ...})

만약에 변수가 필요한 경우 쿼리 키에 포함해 사용할 수 있다.

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

쿼리 함수

쿼리 함수는 무슨 함수든 간에 항상 promise를 반환한다.
promise는 데이터를 처리하거나 에러를 던져야한다.

사용가능한 쿼리 예제:

useQuery({ queryKey: ['todos'], queryFn: fetchAllTodos })
useQuery({ queryKey: ['todos', todoId], queryFn: () => fetchTodoById(todoId) })
useQuery({
  queryKey: ['todos', todoId],
  queryFn: async () => {
    const data = await fetchTodoById(todoId)
    return data
  },
})
useQuery({
  queryKey: ['todos', todoId],
  queryFn: ({ queryKey }) => fetchTodoById(queryKey[1]),
})

에러 핸들링 예제:

const { error } = useQuery({
  queryKey: ['todos', todoId],
  queryFn: async () => {
    if (somethingGoesWrong) {
      throw new Error('Oh no!')
    }
    if (somethingElseGoesWrong) {
      return Promise.reject(new Error('Oh no!'))
    }

    return data
  },
})

쿼리 인자 전달 예제:

function Todos({ status, page }) {
  const result = useQuery({
    queryKey: ['todos', { status, page }],
    queryFn: fetchTodoList,
  })
}

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

쿼리 옵션

queryKeyqueryFn을 여러 장소 간에 공유하면서도 서로의 위치를 동일하게 유지하는 가장 좋은 방법 중 하나는queryOptions를 사용하는 것입니다. 런타임에 이 도우미는 전달한 내용을 모두 반환합니다.

예제:

import { queryOptions } from '@tanstack/react-query'

function groupOptions(id) {
  return queryOptions({
    queryKey: ['groups', id],
    queryFn: () => fetchGroups(id),
    staleTime: 5 * 1000,
  })
}

// usage:

useQuery(groupOptions(1))
useSuspenseQuery(groupOptions(5))
useQueries({
  queries: [groupOptions(1), groupOptions(2)],
})
queryClient.prefetchQuery(groupOptions(23))
queryClient.setQueryData(groupOptions(42).queryKey, newGroups)

병렬 쿼리

병렬 쿼리를 사용하는 방법은 아주 간단하다.
하나의 컴포넌트/커스텀 훅에 여러 useQueryuseInfiniteQuery를 생성하면 된다.

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

하지만, 실행해야 하는 쿼리 수가 렌더링마다 변경되는 경우 훅 규칙을 위반하므로 수동 쿼리를 사용할 수 없다.
동적으로 병렬 쿼리를 사용하기 위해서는 useQueries를 사용해 원하는 만큼 여러 쿼리를 사용할 수 있다.

예제:

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

종속에 의한 쿼리 핸들링

만약 이렇게 된 쿼리가 있을 경우,
projectsuserId에 종속되어 undefined를 가져왔을 경우에도 그대로 쿼리를 실행할 것이다.

// Get the user
const { data: user } = useQuery({
  queryKey: ['user', email],
  queryFn: getUserByEmail,
})

const userId = user?.id

// Then get the user's projects
const {
  status,
  fetchStatus,
  data: projects,
} = useQuery({
  queryKey: ['projects', userId],
  queryFn: getProjectsByUser,
  // The query will not execute until the userId exists
  // enabled: !!userId,
})

이를 예방하기 위해서 useQuery에 enabled 속성을 설정해 userId가 undefined 일 경우에는 쿼리를 실행하지 않을 것이다.

useQueries도 마찬가지이다.

// Get the users ids
const { data: userIds } = useQuery({
  queryKey: ['users'],
  queryFn: getUsersData,
  select: (users) => users.map((user) => user.id),
})

// Then get the users messages
const usersMessages = useQueries({
  queries: userIds
    ? userIds.map((id) => {
        return {
          queryKey: ['messages', id],
          queryFn: () => getMessagesByUsers(id),
        }
      })
    : [], // if users is undefined, an empty array will be returned
})

백그라운드 페칭 표시

쿼리에서 status === 'pending'은 초기 하드 로딩 상태를 보여줄 수 있지만, 중간에 쿼리가 백그라운드에서 다시 페칭이라는 표시를 할 수 있다. 이를 위해 isFetching이라는 불리언도 제공한다.

function Todos() {
  const {
    status,
    data: todos,
    error,
    isFetching,
  } = useQuery({
    queryKey: ['todos'],
    queryFn: fetchTodos,
  })

  return status === 'pending' ? (
    <span>Loading...</span>
  ) : status === 'error' ? (
    <span>Error: {error.message}</span>
  ) : (
    <>
      {isFetching ? <div>Refreshing...</div> : null}

      <div>
        {todos.map((todo) => (
          <Todo todo={todo} />
        ))}
      </div>
    </>
  )
}

만약에 해당 컴포넌트 말고 다른 곳에서 페칭 중이라는 것을 표시하고 싶다면 useIsFetching훅을 사용하면 된다.

import { useIsFetching } from '@tanstack/react-query'

function GlobalLoadingIndicator() {
  const isFetching = useIsFetching()

  return isFetching ? (
    <div>Queries are fetching in the background...</div>
  ) : null
}

창의 초첨이 맞춰짐으로 인한 다시 불러오기

창을 떠나려고 하거나 쿼리 값이 탁한 상태가 될 때, TanStack Query는 자동으로 데이터를 새로 요청할 것이다. 원치 않을 경우 refetchOnWindowFocus 옵션을 사용해 글로벌로, 혹은 쿼리당 끌 수 있다.

글로벌로 끄기

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      refetchOnWindowFocus: false, // default: true
    },
  },
})

function App() {
  return <QueryClientProvider client={queryClient}>...</QueryClientProvider>
}

쿼리당 끄기

useQuery({
  queryKey: ['todos'],
  queryFn: fetchTodos,
  refetchOnWindowFocus: false,
})

커스텀 창 맞춤 이벤트

다시 데이터를 검증하기 위해 TanStack Query를 트리거하는 자체 창 맞춤 이벤트를 관리할 수 있다.
이를 위해 focusManager.setEventListener를 사용해 창이 마춰질 때 원하는 함수를 설정할 수 있다.
focusManager.setEventListener를 사용할 경우 기존 핸들러는 제거되고 새로운 핸들러가 사용됩니다.

예제:

focusManager.setEventListener((handleFocus) => {
  // Listen to visibilitychange
  if (typeof window !== 'undefined' && window.addEventListener) {
    window.addEventListener('visibilitychange', () => handleFocus(), false)
    return () => {
      // Be sure to unsubscribe if a new handler is set
      window.removeEventListener('visibilitychange', () => handleFocus())
    }
  }
})

리액트 네이티브에서 창 맞춤을 관리하기 위해서는 window대신에 AppState 를 사용합니다.
AppState를 사용해 앱 상태가 "active"될 때 트리거를 업데이트 할 수 있다.

import { AppState } from 'react-native'
import { focusManager } from '@tanstack/react-query'

function onAppStateChange(status: AppStateStatus) {
  if (Platform.OS !== 'web') {
    focusManager.setFocused(status === 'active')
  }
}

useEffect(() => {
  const subscription = AppState.addEventListener('change', onAppStateChange)

  return () => subscription.remove()
}, [])

맞춤 상태 관리하는 법:

import { focusManager } from '@tanstack/react-query'

// Override the default focus state
focusManager.setFocused(true)

// Fallback to the default focus check
focusManager.setFocused(undefined)

선택적 쿼리

enabled옵션은 껐다 키는 용도로 사용되는 데 예시로 사용자가 필터 값을 입력한 뒤에만 처음 요청을 할 수 있게 만들 수 있게 할 수 있습니다.
예제는 아래와 같습니다:

function Todos() {
  const [filter, setFilter] = React.useState('')

  const { data } = useQuery({
    queryKey: ['todos', filter],
    queryFn: () => fetchTodos(filter),
    // ⬇️ disabled as long as the filter is empty
    enabled: !!filter,
  })

  return (
    <div>
      // 🚀 applying the filter will enable and execute the query
      <FiltersForm onApply={setFilter} />
      {data && <TodosTable data={data} />}
    </div>
  )
}

아니면 상태에 따라서 쿼리 함수의 실행을 막을 수 있습니다.

function Todos() {
  const [filter, setFilter] = React.useState()

  const { data } = useQuery({
    queryKey: ['todos', filter],
    // ⬇️ disabled as long as the filter is undefined or empty
    queryFn: filter ? () => fetchTodos(filter) : skipToken,
  })

  return (
    <div>
      // 🚀 applying the filter will enable and execute the query
      <FiltersForm onApply={setFilter} />
      {data && <TodosTable data={data} />}
    </div>
  )
}

쿼리 재시도 하기

useQuery를 사용할 경우 자동으로 다시 시도됩니다.
그러나 기본으로 3번까지만 시도 되고 더 이상 다시 시도 되지 않습니다.
만약 이를 바꾸거나 다시 시도하는 것을 막고 싶다면 retry를 사용할 수 있습니다.

  • retry = false는 다시 시도하지 않겠다는 의미입니다.
  • retry = 6는 될 때까지 6번 시도 하겠다는 의미입니다.
  • retry = true는 될 때까지 멈추지 않고 다시 시도 하겠다는 의미입니다.
  • retry = failureCount, error) => ...는 요청이 실패한 경우 따른 커스텀 로직을 허용한다는 의미입니다.

예제는 다음과 같습니다:

import { useQuery } from '@tanstack/react-query'

// Make a specific query retry a certain number of times
const result = useQuery({
  queryKey: ['todos', 1],
  queryFn: fetchTodoListPage,
  retry: 10, // Will retry failed requests 10 times before displaying an error
})

재시도하는 지연 시간을 설정하기 원한다면, retryDelay를 사용할 수 있습니다.
기본적으로 1000ms로 시작하여 두 배씩 주기가 길어지지만, 30초를 넘지 않습니다.

이를 예제로 표현하자면 다음과 같습니다.

// Configure for all queries
import {
  QueryCache,
  QueryClient,
  QueryClientProvider,
} from '@tanstack/react-query'

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),
    },
  },
})

function App() {
  return <QueryClientProvider client={queryClient}>...</QueryClientProvider>
}

추천하진 않지만 ProvideruseQuery에서 retryDelay 함수를 재정의 할 수 있습니다.
함수 대신 정수로 설정하면 지연 시간은 항상 동일합니다.

const result = useQuery({
  queryKey: ['todos'],
  queryFn: fetchTodoList,
  retryDelay: 1000, // Will always wait 1000ms to retry, regardless of how many retries
})

Place holder data

쿼리에 대한 pageIndex를 증가시키려고 할 때 useQuery를 사용한다면 기술적으로는 괜찮지만, UI는 각 페이지 또는 pageIndex에 대한 쿼리가 생성되고 사라지기 때문에successpending상태를 들낙한다. 자리 표시자 data를 (previousData) => previousData 함수로 유지하면 새로운 기능을 만들 수 있다.

  • 쿼리 키가 변경되었지만 새로 데이터를 요청하는 동안 마지막으로 성공한 데이터를 사용할 수 있다.
  • 데이터가 새로 도착하면 이전 데이터와 새로운 데이터가 바뀌어진다.
  • isPlaceholderData를 사용하면 쿼리가 현재 제공하고 있는 데이터를 알 수 있다.
    예제:
import { keepPreviousData, useQuery } from '@tanstack/react-query'
import React from 'react'

function Todos() {
  const [page, setPage] = React.useState(0)

  const fetchProjects = (page = 0) =>
    fetch('/api/projects?page=' + page).then((res) => res.json())

  const { isPending, isError, error, data, isFetching, isPlaceholderData } =
    useQuery({
      queryKey: ['projects', page],
      queryFn: () => fetchProjects(page),
      placeholderData: keepPreviousData,
    })

  return (
    <div>
      {isPending ? (
        <div>Loading...</div>
      ) : isError ? (
        <div>Error: {error.message}</div>
      ) : (
        <div>
          {data.projects.map((project) => (
            <p key={project.id}>{project.name}</p>
          ))}
        </div>
      )}
      <span>Current Page: {page + 1}</span>
      <button
        onClick={() => setPage((old) => Math.max(old - 1, 0))}
        disabled={page === 0}
      >
        Previous Page
      </button>{' '}
      <button
        onClick={() => {
          if (!isPlaceholderData && data.hasMore) {
            setPage((old) => old + 1)
          }
        }}
        // Disable the Next Page button until we know a next page is available
        disabled={isPlaceholderData || !data?.hasMore}
      >
        Next Page
      </button>
      {isFetching ? <span> Loading...</span> : null}{' '}
    </div>
  )
}

무한 쿼리

만약 꾸준하게 데이터를 요청해야 할 경우 useInfiniteQuery를 사용할 수 있다.

fetch('/api/projects?cursor=0')
// { data: [...], nextCursor: 3}
fetch('/api/projects?cursor=3')
// { data: [...], nextCursor: 6}
fetch('/api/projects?cursor=6')
// { data: [...], nextCursor: 9}
fetch('/api/projects?cursor=9')
// { data: [...] }

이렇게 계속해서 페칭을 해야 할 경우 아래와 같이 요청할 수 있다.

import { useInfiniteQuery } from '@tanstack/react-query'

function Projects() {
  const fetchProjects = async ({ pageParam }) => {
    const res = await fetch('/api/projects?cursor=' + pageParam)
    return res.json()
  }

  const {
    data,
    error,
    fetchNextPage,
    hasNextPage,
    isFetching,
    isFetchingNextPage,
    status,
  } = useInfiniteQuery({
    queryKey: ['projects'],
    queryFn: fetchProjects,
    initialPageParam: 0,
    getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
  })

  return status === 'pending' ? (
    <p>Loading...</p>
  ) : status === 'error' ? (
    <p>Error: {error.message}</p>
  ) : (
    <>
      {data.pages.map((group, i) => (
        <React.Fragment key={i}>
          {group.data.map((project) => (
            <p key={project.id}>{project.name}</p>
          ))}
        </React.Fragment>
      ))}
      <div>
        <button
          onClick={() => fetchNextPage()}
          disabled={!hasNextPage || isFetchingNextPage}
        >
          {isFetchingNextPage
            ? 'Loading more...'
            : hasNextPage
              ? 'Load More'
              : 'Nothing more to load'}
        </button>
      </div>
      <div>{isFetching && !isFetchingNextPage ? 'Fetching...' : null}</div>
    </>
  )
}

양방향 통신일 경우:

useInfiniteQuery({
  queryKey: ['projects'],
  queryFn: fetchProjects,
  initialPageParam: 0,
  getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
  getPreviousPageParam: (firstPage, pages) => firstPage.prevCursor,
})

거꾸로 페이지를 표시해야 할 경우에는 select옵션을 사용할 수 있다.

useInfiniteQuery({
  queryKey: ['projects'],
  queryFn: fetchProjects,
  select: (data) => ({
    pages: [...data.pages].reverse(),
    pageParams: [...data.pageParams].reverse(),
  }),
})

제한을 하고 싶을 경우에는:

useInfiniteQuery({
  queryKey: ['projects'],
  queryFn: fetchProjects,
  initialPageParam: 0,
  getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
  getPreviousPageParam: (firstPage, pages) => firstPage.prevCursor,
  maxPages: 3,
})

커서를 반환하지 않을 경우에는:

return useInfiniteQuery({
  queryKey: ['projects'],
  queryFn: fetchProjects,
  initialPageParam: 0,
  getNextPageParam: (lastPage, allPages, lastPageParam) => {
    if (lastPage.length === 0) {
      return undefined
    }
    return lastPageParam + 1
  },
  getPreviousPageParam: (firstPage, allPages, firstPageParam) => {
    if (firstPageParam <= 1) {
      return undefined
    }
    return firstPageParam - 1
  },
})

Initial Query Data

요청에 대한 응답이 오기 전에 값은 비어 있다.
비어있는 것을 막기 위해서 보통 isLoading을 사용해서 값이 받아올 때 까지 화면을 보이지 않게 된다.
하지만 값을 받지 못하더라도 useQueryinitialData를 사용해 초기 값을 설정해 화면이 변하지 않는 것처럼 활동할 수 있다.

예제:

const result = useQuery({
  queryKey: ['todos'],
  queryFn: () => fetch('/todos'),
  initialData: initialTodos,
})

Mutations

쿼리와 달리 변화(mutation)는 CRUD에서 CUD를 담당하거나 서버 부작용을 보여준다.

예제:

function App() {
  const mutation = useMutation({
    mutationFn: (newTodo) => {
      return axios.post('/todos', newTodo)
    },
  })

  return (
    <div>
      {mutation.isPending ? (
        'Adding todo...'
      ) : (
        <>
          {mutation.isError ? (
            <div>An error occurred: {mutation.error.message}</div>
          ) : null}

          {mutation.isSuccess ? <div>Todo added!</div> : null}

          <button
            onClick={() => {
              mutation.mutate({ id: new Date(), title: 'Do Laundry' })
            }}
          >
            Create Todo
          </button>
        </>
      )}
    </div>
  )
}

변화는 다음과 같은 상태중 하나만 될 수 있다.
1. isIdle또는 status === 'idle' : 변화가 현재 휴식 되었거나 상태가 리셋되었거나 값을 새로 받았을 때
2. isPending 또는 status === 'pending' : 변화가 진행중
3. isError 또는 status === 'error' : 에러가 감지됨
4. isSuccess또는 status === 'success' : 변화가 성공적으로 진행됨 | 변화된 데이터 사용 가능

변화는 비동기 함수이므로 React 16 버전 이하는 이를 사용할 수 없습니다.
이전 버전에서 사용하기 위해서는 onSubmit안에 mutate를 사용할 수 있습니다.

예제:

// This will not work in React 16 and earlier
const CreateTodo = () => {
  const mutation = useMutation({
    mutationFn: (event) => {
      event.preventDefault()
      return fetch('/api', new FormData(event.target))
    },
  })

  return <form onSubmit={mutation.mutate}>...</form>
}

// This will work
const CreateTodo = () => {
  const mutation = useMutation({
    mutationFn: (formData) => {
      return fetch('/api', formData)
    },
  })
  const onSubmit = (event) => {
    event.preventDefault()
    mutation.mutate(new FormData(event.target))
  }

  return <form onSubmit={onSubmit}>...</form>
}

변화 리셋하기:

const CreateTodo = () => {
  const [title, setTitle] = useState('')
  const mutation = useMutation({ mutationFn: createTodo })

  const onCreateTodo = (e) => {
    e.preventDefault()
    mutation.mutate({ title })
  }

  return (
    <form onSubmit={onCreateTodo}>
      {mutation.error && (
        <h5 onClick={() => mutation.reset()}>{mutation.error}</h5>
      )}
      <input
        type="text"
        value={title}
        onChange={(e) => setTitle(e.target.value)}
      />
      <br />
      <button type="submit">Create Todo</button>
    </form>
  )
}

부작용 대비:

useMutation({
  mutationFn: addTodo,
  onMutate: (variables) => {
    // A mutation is about to happen!

    // Optionally return a context containing data to use when for example rolling back
    return { id: 1 }
  },
  onError: (error, variables, context) => {
    // An error happened!
    console.log(`rolling back optimistic update with id ${context.id}`)
  },
  onSuccess: (data, variables, context) => {
    // Boom baby!
  },
  onSettled: (data, error, variables, context) => {
    // Error or success... doesn't matter!
  },
})

마찬가지로 재시도를 할 수 있고 이를 설정할 수 있습니다.

const mutation = useMutation({
  mutationFn: addTodo,
  retry: 3,
})
profile
(돈과 인맥을 만들어 나가는)학생 개발자

0개의 댓글

관련 채용 정보