react query (공식문서)

박정훈·2023년 3월 26일
5

React

목록 보기
10/10

공식문서 좀 읽어보기. 많은 내용이 빠져있습니다 :)

왜 React Query?

프론트에서 상태를 다루는 일은 매우 흔한일! 그 상태는 크게 두가지로 나눠볼 수 있다.

클라이언트 상태

유저와의 인터렉셕에 의해서 발생하는 상태를 의미한다. 예를들면 모달을 연다던가?

서버 상태

api 통신을 통해서 서버에서 받아온 서버 데이터를 의미한다.

전통전인 상태관리 라이브러리들은 클라이언트 서버를 다루는 것에는 훌륭했지만, 서버 상태나 async와 작업하기에는 충분치 못했다.

This is because server state is totally different

서버 상태는 다음과 같은 특징을 가진다.

  • 우리에게 온전한 소유권이나 통제권이 없는 위치에 원격으로 유지된다.
  • fetching 이나 updating을 위한 비동기 api가 필요하다.
  • 소유권을 암묵적으로 공유하게 되며 사용자 모르게 다른 사람에 의해 변경될 수 있다.
  • 신경쓰지 않으면 데이터는 상해버린다. (경우에 따라 클라이언트에서 서버데이터를 받은 순간부터 데이터는 썩고 있다.)

서버의 특성을 살펴보면 해결해야 할 문제는 산개해 있다.

  • 캐싱...
  • 동일한 데이터에 대한 여러 요청을 단일 요청으로 중복 제거
  • 오래된 데이터를 백그라운드에서 업데이트하기
  • 데이터가 out of date된 시점 알기
  • 데이터 업데이트를 최대한 신속하게 반영
  • 페이징 및 지연 로딩 데이터와 같은 성능 최적화
  • 서버 상태의 메모리 및 가비지 수집 관리
  • 구조적 공유로 쿼리 결과 Memoizing 하기

이런 다양한 문제를 모두 해결했으면 이 라이브러리를 사용하지 않아도 될듯..?

React Query는 서버 상태를 관리하기 위한 최고의 라이브러리 중 하나이다.

React Query를 사용함으로써 클라이언트 상태와 서버 상태를 분리시킬 수 있다.

Quick Start

React Query의 3가지 핵심 컨셉

Devtools

Query Devtools는 process.env.NODE_ENV === 'development'일 때만 번들에 포함되므로 프로덕션 빌드 중에 제외하는 것에 대해 걱정할 필요가 없다.

Devtools in production

Devtools는 프로덕션 빌드에서 제외된다. 그러나 프로덕션에서 devtools를 지연 로드하는 것이 바람직할 수 있다.
code

Comparison

각종 라이브러리 비교링크

Important Defaults

기본적으로 useQuery 또는 useInfiniteQuery를 통한 Query instance는 캐시된 데이터를 오래된 것으로 간주한다.

이러한 동작을 변경하려면 staleTime 옵션을 사용하여 쿼리를 전역적으로 구성하고 쿼리별로 구성할 수 있다. 더 긴 staleTime을 지정하면 쿼리가 데이터를 자주 다시 가져오지 않음을 의미한다.

상한 queries는 다음과 같은 상황에 자동적으로 백그라운드에서 refetch 된다.

  • query mount의 새 인스턴스
  • 윈도우가 refocus 되었을 때
  • netework가 다시 연결되었을 때
  • 쿼리가 refetch interval 설정되었을 때

예상치 못한 refetch가 발생하면, 창에 포커스가 되어있고, 탄스택 쿼리가 refetchOnWindowFocus를 수행하고 있기 때문일 수 있다.

이런 기능을 변경하려면 refetchOnMount, refetchOnWindowFocus, refetchOnReconnect 및 refetchInterval과 같은 옵션을 사용할 수 있다.

  • 더 이상 사용되지 않는 인스턴스인 useQuery, useInfiniteQuery 또는 query observers들의 Query는 비활성 레이블로 지정되며 나중에 다시 사용될 경우에도 캐시에 남아있다.

  • 기본적으로 비활성 쿼리들은 5분 후에 가비지 콜렉터로부터 회수당한다.

이를 바꾸고 싶다면, cacheTime의 값을 변경할 수 있다.

  • 쿼리들이 UI에 error를 capturung하고 displaying하기 전에, 3번의 시도를 한다.

이를 바꾸고 싶다면 retryretryDelay의 값을 변경할 수 있다.

Queries

query는 프로미스 기반의 method(GET, POST)로 서버로부터 data를 fetch할 수 있다. 만약 서버의 데이터를 수정해야 한다면, Mutations를 대신 사용하는 것을 권장한다.

당신의 컴포넌트나 커스텀 훅에서 query를 구독하기 위해서는 최소한 다음과 같이 useQuery를 호출해라.

  • query를 위한 unique keu
  • 다음과 같은 promise를 반환하는 함수
    • resolves the data, or
    • throws an error

당신이 제공하는 unique key는 당신의 쿼리들을 app 전체에서 refetching, caching, sharing하기 위해 내부적으로 사용된다.

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

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

result객체에는 몇가지 생산성을 위한 아주 중요한 sates가 포함되어 있다. 쿼리는 주어진 순간(given moment)에 다음 중 하나의 상태만 가질 수있다.

  • isLoading or status === 'loading' - The query has no data yet
  • isError or status === 'error' - The query encountered an error
  • isSuccess or status === 'success' - The query was successful and data is available

이런 기본 상태 말고도, 쿼리 상태에 따라 더 많은 정보를 사용할 수 있다.

  • error - If the query is in an isError state, the error is available via the error property.
  • data - If the query is in an isSuccess state, the data is available via the data property.
function Todos() {
  //const { isLoading, isError, data, error } = useQuery({
  //  queryKey: ['todos'],
  //  queryFn: fetchTodoList,
  //})
  const { status, data, error } = useQuery({
    queryKey: ['todos'],
    queryFn: 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>
  )
}

FetchStatus

추가적으로 status나 result object 말고도 다음 옵션이 있는 fetchStatus 프로퍼티를 얻을 수 있다.

  • fetchStatus === 'fetching' - The query is currently fetching.
  • fetchStatus === 'paused' - The query wanted to fetch, but it is paused. Read more about this in the Network Mode guide.
  • fetchStatus === 'idle' - The query is not doing anything at the moment.

why two different states?

백그라운드 refetches와 오래된 검증 로직은 모든 statusfetchStatus조합을 만들 수 있다.

  • success 상태의 쿼리는 일반적으론 idle fetchStatus이지만, 백그라운드 refetch가 발생했을때 fetching에도 있을 수 있다.
  • 마운트되고 데이터가 없는 쿼리는 일반적으로 loadingfetching fetchStatus 상태지만 네트워크 연결이 없는 경우 pause될 수도 있습니다.

따라서 실제로 데이터를 가져오지 않고도 쿼리가 로드 상태에 있을 수 있음을 명심하자!

  • statusdata에 대한 정보를 제공한다. 데이터가 있는지 없는지?
  • fetchStatusqueryFn의 정보를 제공한다. 지금 실행되고 있는지 아닌지?

Query Keys

본질적으로 TanStack Query는 쿼리 키를 기반으로 쿼리 캐싱을 관리한다. 쿼리 키는 top level의 배열이어야 하며 단일 문자열이 포함된 배열처럼 단순하거나 많은 문자열 및 중첩 개체의 배열처럼 복잡할 수 있다. 쿼리 키가 직렬화 가능하고 쿼리 데이터에 고유한 한 사용할 수 있다!!

Simple Query keys

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

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

Array Keys with variables

  • 계층적 또는 중첩된 자원
    • 항목을 고유하게 식별하기 위해 ID, 인덱스 또는 기타 기본값을 전달하는 것이 일반적이다.
  • 추가적인 parameters와 함께인 Queries
    • 추가 옵션 객체를 전달하는것이 일반적이다.
// 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' }], ... 

Query Keys are hashed deterministically!

객체의 키 순서에 관계없이 다음 쿼리는 모두 동일하게 간주된다.

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], ...})

만약 당신의 쿼리 함수가 변수에 의존하고 있다면, 이를 쿼리키에 포함시켜라

쿼리 키는 가져오는 데이터를 uniquely하게 설명하므로 변경되는 쿼리 기능에 사용하는 변수를 포함해야 한다.

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

Query Functions

쿼리 함수는 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]),
})

Parallel Queries

parallel 쿼리는 병렬로 실행되거나, 동시에 maximize fetching concurrency 하는 쿼리이다.

Manual Parallel Queries

병렬 쿼리수가 변경되지 않는다면(?), parallel 쿼리를 사용할 필요가 없다.

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 })
  ...
}

Dynamic Parallel Queries with 'useQueries'

  const results = useQueries({
    queries: [
      { queryKey: ['movieList', 1], queryFn: () => getMovieList({}) },
      { queryKey: ['movieList', 2], queryFn: () => getMovieList({ limit: 20, sort_by: 'peers'}) },
      { queryKey: ['movieList', 1], queryFn: () => getMovieList({ limit: 15, sort_by: 'rating', order_by: 'asc'}) }
    ]
  })
  
  // results는 결과 배열을 반환 받는다. 

병렬 처리 문서를 읽다 문득 의문점이 생겼다. 이 동작은 병렬이라 그랬으니까... 누가 먼저 올지는 모르겠네?

  const { movieList, isLoading } = useGetMoviList({
    limit: 10,
    sort_by: 'rating',
    order_by: 'asc'
  })

  const { movieList: movieList2, isLoading: isLoading2 } = useGetMoviList({
    limit: 20,
    sort_by: 'peers',
    order_by: 'asc'
  })

  const { movieList: movieList3, isLoading: isLoading3 } = useGetMoviList({
    limit: 15,
    sort_by: 'title',
    order_by: 'desc'
  })

  const { movieList: movieList4, isLoading: isLoading4 } = useGetMoviList({
    limit: 35,
    sort_by: 'year',
    order_by: 'desc'
  })
  
  // 누가 먼저 결과를 받아올지 모름
  console.log(movieList, movieList2, movieList3, movieList4);

// [결과1], undefined, undefined, undefined
// [결과1], undefined, [결과2], undefined
// 뭐 이런식으로 뒤죽박죽

위 같은 경우 누가 먼저 올지 알 수 없었다. 말 그대로 병렬 처리되면서, 먼저 오는대로 새롭게 앱을 렌더링하고 있었다.자, 그렇다면 useQueries를 보자.

  const results = useQueries({
    queries: [
      { queryKey: ['movieList', 1], queryFn: () => getMovieList({}) },
      { queryKey: ['movieList', 2], queryFn: () => getMovieList({ limit: 20, sort_by: 'peers' }) },
      { queryKey: ['movieList', 1], queryFn: () => getMovieList({ limit: 15, sort_by: 'rating', order_by: 'asc' }) }
      ]
  })

역시 누가 먼저 올지 알 수 없다.

enabled

찾아보니 enabled 옵션이 있다.

  const { movieList, isLoading } = useGetMoviList({
    limit: 10,
    sort_by: 'rating',
    order_by: 'asc',    
  })

  const { movieList: movieList2, isLoading: isLoading2 } = useGetMoviList({
    limit: 15,
    sort_by: 'peers',
    order_by: 'asc',
    option: {
      enabled: !!movieList
    }
  })

  const { movieList: movieList3, isLoading: isLoading3 } = useGetMoviList({
    limit: 20,
    sort_by: 'title',
    order_by: 'desc',
    option: {
      enabled: !!movieList2
    }
  })

  const { movieList: movieList4, isLoading: isLoading4 } = useGetMoviList({
    limit: 35,
    sort_by: 'year',
    order_by: 'desc',
    option: {
      enabled: !!movieList3
    }
  })
// [결과1], undefined, undefined, undefined
// [결과1], [결과2], undefined, undefined
// [결과1], [결과2], [결과3], undefined
// [결과1], [결과2], [결과3], [결과4]

확실히 받아오는 속도가 느려졌지만, 앞의 데이터를 의존함으로써, 앞의 데이터가 있어야만 다음 호출이 일어났다.

그럼 useQueries를 사용해서 위와 같은 결과를 얻고 시퍼! 어떡해?

  const results = useQueries({
    queries: [
      { queryKey: ['movieList', 1], queryFn: () => getMovieList({}), enabled: true },
      { queryKey: ['movieList', 2], queryFn: () => getMovieList({ limit: 20, sort_by: 'peers' }), enabled: true },
      { queryKey: ['movieList', 3], queryFn: () => getMovieList({ limit: 15, sort_by: 'rating', order_by: 'asc' }), enabled: true }
      ]
  })
  
  const loading = results.some(result => result.isLoading)
  
  
  if(loading) {
    return <>Loading...</>
  }

  console.log(results); // loading을 통과해야만 log가 찍힌다!

some통해 loading이 하나라도 있는지 체크하다가 loading이 없어지면, 즉, 모두 호출되면 log가 찍히는 것이다.

잠깐 다른 길로 샜다. 다시 돌아가자.

Dependent Queries

아니! 바로 다음장에 enabled가 나왔다. 히히

Background Fetching Indicators

쿼리의 status === loading 상태는 쿼리의 초기 하드로딩 상태를 표시하기에 충분하지만, 쿼리가 백그라운드에서 refetch 중임을 나타내는 추가 표시기를 표시할 수도 있다. 이를 위해 쿼리는 상태 변수의 상태에 관계없이 fetching 상태임을 표시하는 데 사용할 수 있는 isFetching 부울을 제공한다.

Displaying Global Background Fetching Loading State

개별적인 쿼리 로드 상태 외에도, 어떠한 query든지 fetching될 때 전역적으로 loading 표시기를 보여주고 싶다면, useIsFetching hook을 사용할 수 있다.

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

  const isFetching = useIsFetching()

  if(isFetching) {
  return <h1>Queries are fetching in the background...</h1>
}

Window Focus Refetching

유저가 앱에서 벗어났다가 돌아왔고, 쿼리 데이터가 stale이라면, 백그라운드에서 자동으로 fresh data를 request한다. 이를 글로벌적으로, 혹은 개별적 쿼리에서 disable 할 수 있다.

위의 예제를 다시 재활용 해보자.

  const { movieList, isLoading } = useGetMoviList({
    limit: 10,
    sort_by: 'rating',
    order_by: 'asc',    
  })

  const { movieList: movieList2, isLoading: isLoading2 } = useGetMoviList({
    limit: 15,
    sort_by: 'peers',
    order_by: 'asc',
    option: {
      enabled: !!movieList
    }
  })

  const { movieList: movieList3, isLoading: isLoading3 } = useGetMoviList({
    limit: 20,
    sort_by: 'title',
    order_by: 'desc',
    option: {
      enabled: !!movieList2,
      refetchOnWindowFocus: false
    }
  })

  const { movieList: movieList4, isLoading: isLoading4 } = useGetMoviList({
    limit: 35,
    sort_by: 'year',
    order_by: 'desc',
    option: {
      enabled: !!movieList3,
      refetchOnWindowFocus: false
    }
  })

window를 focus 했을 때 네트워크 탭에는 두개만 호출되는 것을 확인할 수 있다.

Custom Window Focus Event

커스텀 하고 싶다면 focusManager를 사용하면 된다.

Disabling/Pausing Queries

쿼리가 자동으로 실행되지 않도록 하려면 enabled = false 옵션을 사용할 수 있다.
마찬가지로 위 예제에서 option으로 enabled = false를 부여하면, 첫 fetch조차 일어나지 않는것을 확인할 수 있었다.

enabled가 false인 경우에는 다음과 같다.

  • 쿼리가 데이터를 캐시한 경우 쿼리가 status === success 또는 isSuccess 상태로 초기화된다.
  • 쿼리에 캐시된 데이터가 없는 경우 쿼리는 status === loadingfetchStatus === loading 상태에서 시작된다.
  • 쿼리는 마운트시에 자동으로 fetch되지 않는다.
  • 쿼리는 백그라운드에서 자동으로 refetch되지 않는다.
  • 쿼리는 쿼리 클라이언트의 invalidateQuerys 및 refetchQuerys 호출을 무시하며 일반적으로 쿼리를 다시 fetch한다.(?)
  • useQuery에서 반환된 refetch는 가져올 쿼리를 수동으로 트리거하는 데 사용할 수 있다.(?)

Lazy Queries

활성화 옵션은 쿼리를 영구적으로 비활성화할 뿐만 아니라 나중에 활성화/비활성화할 수도 있다. 사용자가 필터 값을 입력한 후 첫 번째 요청만 실행하려는 필터 양식이 좋은 예이다.

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

isInitialLoading

Lazy queries는 loading이 의미하는 바가 데이터가 아직 없다는 뜻이니까 status: 'loading'이 된다. 기술적으로는 맞는 말이지만, 현재 데이터를 가져오지 않았기 때문에(비활성화 이기 때문에) 이 flag로는 로딩 스피너 여부를 표시할 수 없다.

만약 disabled나 lazy queries를 사용하는 경우에는 isInitialLoading flag를 대신 사용할 수 있다. 이는 isLoading && isFetching 이다.

따라서 쿼리가 현재 처음으로 가져오는 경우에만 해당된다.

Query Retries

제목처럼 쿼리를 재시도 하는 것이다. 당연~히 실패했을때 재시도 여부를 설정할 수 있다.

Retry Delay

제목만 봐도 바로 알 수 있을것이다.
기본 retryDelay는 시도할 때마다 두 배(1000ms부터 시작)로 설정되지만 30초를 초과할 수 없다.

Paginated / Lagged Queries

일반적으로 useQuery를 사용해서 페이지네이트를 하게되면 각각의 새로운 페이지가 새로운 쿼리처럼 생성되기 때문에 UI 는 successloading상태에서 점프한다(?)

아, 영어 못하니까 역시 직접 써봤다.

const Paginate = () => {
    const [page, setPage] = useState(1)

    const { data, isLoading} = useMovieListQuery({
        limit: 10,
        page,
        option: {
            // keepPreviousData: true,
            staleTime: 1 * 6000
        }
    })

    const clickPrevious = () => {
        setPage(cur => {
            if(cur === 1) return 1
            return cur - 1
        })
    }

    const clickNext = () => {
        setPage(cur => cur + 1)
    }

    if(isLoading) return <h1>Loading...</h1>

  return (
    <div style={{ textAlign: 'center'}}>
      <ol style={{ padding: '100px'}}>
              {data?.data.data.movies?.map(movie => {
              return (
                  <li style={{ display: 'inline-block'}} key={movie.id}>
                      <Image src={movie.large_cover_image} alt={movie.description_full} width={200} height={350} priority />
                  </li>
              )
          })}
      </ol>
      <span>Current Page: { page }</span>
      <button onClick={clickPrevious}>이전 페이지</button>
      <button onClick={clickNext}>다음 페이지</button>
    </div>
  )
}

export default Paginate

keepPreviousData를 적용하면 페이지를 넘길때, 로딩대신 이전 데이터를 유지시키기 때문에 이전 페이지 결과값이 화면에 남아있다.
keepPreviousData를 빼버리면 페이지를 넘길때<h1>Loading...</h1>이 출력된다.

Infinite Queries

스크롤을 내릴때, 특정 지점에서 query로 nextFetch를 한다.

// useMovieListInfiniteQuery.ts
import { getMovieList } from '@/api/movie'
import { useInfiniteQuery } from '@tanstack/react-query'
import { getMovieParmI } from './useMovieListQuery'

const useMovieListInfiniteQuery = (param: getMovieParmI) => {
    const result = useInfiniteQuery({
        queryKey: ['movieListInf'],
        queryFn: ({ pageParam = 1}) => getMovieList({
            page: pageParam
        }),
        getNextPageParam: (lastPage) => lastPage.data.data.page_number + 1,
        ...param.option
    })
    return result
}

export default useMovieListInfiniteQuery
// useIntersectionObserver.ts
import { useEffect, useState } from 'react';

interface useIntersectionObserverProps {
    onIntersect: IntersectionObserverCallback;
}

const useIntersectionObserver = ({
    onIntersect,
}: useIntersectionObserverProps) => {
    const [target, setTarget] = useState<HTMLElement | null>(null);

    useEffect(() => {
        if (!target) return;

        const observer: IntersectionObserver = new IntersectionObserver(onIntersect);
        observer.observe(target);

        return () => observer.unobserve(target);
        
    }, [onIntersect, target]);

    return { setTarget };
};

export default useIntersectionObserver;
import useIntersectionObserver from '@/hooks/intersectionObserver/useIntersectionObserver'
import useMovieListInfiniteQuery from '@/hooks/query/useMovieListInfiniteQuery'
import Image from 'next/image'
import React from 'react'

const InfiniteScroll = () => {
  const { data, isLoading, isError, fetchNextPage } = useMovieListInfiniteQuery({
    page: 0
  })

  const onIntersect: IntersectionObserverCallback = ([{ isIntersecting }]) => {
    if(isIntersecting) {
      fetchNextPage()
    }
  }

  const { setTarget } = useIntersectionObserver({onIntersect})

  if (isLoading) return <h1>Loading...</h1>
  if (isError) return <h1>Error...</h1>


  return (
    <main style={{ width: '100%',  border: '1px solid white', minHeight: '700px', position: 'relative'}}>
      {data.pages.map((item, i) => (
        <React.Fragment key={i}>
          {item.data.data.movies.map(movie => (
            <Image key={movie.id} src={movie.large_cover_image} alt={movie.description_full} width={200} height={350} priority />
          ))}
        </React.Fragment>
      ))}
      <button onClick={() => fetchNextPage()}>버튼</button>
      <div ref={setTarget} style={{ border: '1px solid green', width: '100%', height: '100px', bottom: '100px', position: 'absolute' }}></div>          
    </main>
  )
}

export default InfiniteScroll

잘 된다!

What happens when an infinite query needs to be refetched?

무한 쿼리가 오래되어 다시 페치해야 할 경우 각 그룹은 첫 번째 그룹부터 순차적으로 페치된다. 무한 쿼리의 결과가 queryCache에서 제거되면 초기 그룹만 요청된 상태에서 pagination이 초기 상태에서 다시 시작된다.

Initial Query Data

캐시에 쿼리에 대한 초기 데이터를 제공하는 방법은 여러가지가 있다.

  • 선언적인 방법
    • 쿼리에 initialData를 제공하여 비어 있는 경우 캐시를 미리 채운다.
  • 명령적인 방법
    • Prefetch the data using queryClient.prefetchQuery
    • queryClient.setQueryData를 사용해서 데이터를 수동으로 캐시에 배치한다.

initialData는 캐시에 남는다. 그렇기에 placeholder, 부분적 또는 불완전한 데이터에 이 옵션을 부여하지 말고, 대신 placeholderData를 사용해라!!

공식문서를 봐도 뽝! 느낌이 안와서 걍 사용해봤다.

const InitialData = () => {

    const result = useQuery({
        queryKey: ["movieList"],
        queryFn: () => getMovieList({}),
        initialData: {
            config: {},
            data: {
                '@meta': {},
                data: {
                    limit: 0,
                    movie_count: 0,
                    movies: [{
                        id: 1,
                        url: '',
                        large_cover_image: 'https://yts.torrentbay.to/assets/images/movies/fours_a_crowd_2022/large-cover.jpg',
                        title: '',
                        description_full: ''
                    }],
                    page_number: 1,
                },
                status: '',
                status_message: ''
            },
            headers: {},
            request: {},
            status: 1,
            statusText: ''
        }
    })
    
    if(result.isLoading) return <h1>Loading...</h1>
    
  return (
    <ol>
          {result.data?.data.data.movies.map(result => {
              return (
                  <li key={result.id} style={{display: 'inline-block'}}>
                      <Image src={result.large_cover_image} alt={result.description_full} width={200} height={350} priority></Image>
                  </li>
              )
          })}
    </ol>
  )
}

export default InitialData

로딩을 거치지 않는것을 확인했다.
기존에 초기값, 그러니까 내가 억지로 넣은 initialData값으로 사진한장이 덜렁 나오다가 data를 fetch 해 오면, 해당 결과 바뀌는 것을 확인했다. 말 그대로 initialData였다.

그 이후 나오는 여러 예제가 있는데 이중 initialDataUpdatedAt은 솔직히 이해가 안간다. 관련해서 tkdodo 씨의 블로그 글을 봐야겠다.

Placeholder Query Data

initialData와 비슷하지만 캐시에 남지 않는다. 이 기능은 실제 데이터가 백그라운드에서 가져와지는 동안 쿼리를 성공적으로 렌더링하기에 충분한 부분(또는 가짜) 데이터가 있는 상황에서 유용하다.

위 initialData예제에서 placeholderData로 바꾸고 해봐도 결과가 같다.

이하, chat gpt의 답변이다.

React Query의 initial query data와 placeholder query data는 비슷해 보일 수 있지만 목적이 다릅니다.
initial query data는 쿼리가 처음 실행될 때 UI에 표시될 데이터입니다. 이 데이터는 일반적으로 캐시에서 가져온 데이터이거나, 서버에서 가져온 데이터가 없을 때 사용되는 기본값입니다.
반면, placeholder query data는 새로운 데이터를 가져오기 전에 UI에 표시할 데이터입니다. 이 데이터는 일반적으로 실제 서버에서 가져올 데이터와 유사한 형식으로 구성됩니다. placeholder query data는 쿼리가 해결되기 전에 사용자에게 로딩 중임을 알리기 위해 UI에 사용됩니다.
따라서, initial query data와 placeholder query data는 모두 UI에 표시할 데이터이지만, initial query data는 쿼리 결과를 받기 전에 캐시나 기본값으로 사용되고, placeholder query data는 로딩 중인 상태에서 사용되는 일시적인 데이터입니다.

알거 같으면서 모르겠다~

Prefetching

유저가 어떤 데이터를 필요로 하기전에 해당 데이터를 미리 받아놨다면, 데이터를 가져오는데 걸리는 시간을 줄일 수 있다. Prefetching으로 데이터를 캐싱하는것이 그 방법이 될 수 있다.

  • 이 쿼리에 대한 데이터가 이미 캐시에 있고 유효하지 않은 경우 데이터를 가져오지 않는다.
  • staleTime을 부여했고, 데이터가 특정 staleTime보다 오래되었으면, 쿼리는 fetched한다.
  • 사전 추출된 쿼리에 대해 useQuery 인스턴스가 나타나지 않으면 이 쿼리는 삭제되고 cacheTime에 지정된 시간이 지나면 가비지에 의해 수집된다.

Mutations

쿼리와 달리 mutations는 create/update/delete data 또는 server side-effects를 수행한다.

mutaion은 주어진 순간에 다음 중 하나의 상태에 있다.

  • isIdle or status === 'idle' - mutation은 idle 또는 fresh/reset 상태이다.
  • isLoading or status === 'loading' - mutation은 running 상태이다.
  • isError or status === 'error' - mutation은 error를 맞닥뜨렸다.
  • isSuccess or status ==='success' - mutation은 성공했고 mutation data는 사용 가능하다.

외에도 많은 정보를 사용 가능하다.

  • error - mutation은 error 상태이고, error 프로퍼티를 통해 사용 가능하다.
  • data - mutation은 success 상태이고, data 프로퍼티를 통해 사용 가능하다.

Resetting Mutation State

가끔 mutation 요청의 errordata를 비워줘야 할 때가 있다. reset 함수를 사용하자.

mutation.reset()

Query Invalidation

현재 캐시된 쿼리를 무효화하며, 해당 쿼리를 다시 가져오게 한다. 해당 함수는 주로! 데이터 업데이트 이후에 사용하게 된다. 사용자가 어떠한 작업을 해서, 데이터를 업데이트 시켰다면, 해당 데이터에 관한 모든 쿼리를 무효화한 다음, 업데이트된 데이터를 다시 가져와야 할 것이다. 쿼리 무효화를 하는 방법은 매우 다양하다.

쿼리 무효화

Updates from Mutation Responses

앞선 invalidation과 유사해 보이는 setQueryData가 소개되고 있다.
서버에게 객체를 업데이트하는 mutation을 다룰때, 새로운 객체는 mutation의 응답으로 자동으로 반환되는게 일반적이다. 해당 항목에 대한 쿼리를 다시 가져와서 이미 가지고 있는 데이터에 대한 네트워크 호출을 낭비하는 대신에 mutation 함수에 의해 반환된 객체를 활용하고 Query Client's setQueryData 메서드를 사용해서 즉시 기존 쿼리를 새 데이터로 업데이트 할 수 있다.

Immutability

setQueryData를 통한 업데이트는 immutable 방식으로 수행되어야 한다.

queryClient.setQueryData(
  ['posts', { id }],
  (oldData) => {
    if (oldData) {
      // ❌ do not try this
      oldData.title = 'my new post title'
    }
    return oldData
  })

queryClient.setQueryData(
  ['posts', { id }],
  // ✅ this is the way
  (oldData) => oldData ? {
    ...oldData,
    title: 'my new post title'
  } : oldData
)

이후 공식문서는 봐도 무슨 의미인지 솔직히? 모르겠다. 실전에서 써먹다가 의문이 생기면 그때 다시 와서 읽어야지.

SSR & Next.js

React Query는 서버에서 데이터를 미리 가져와 queryClient에 전달하는 두 가지 방법을 지원합니다.

  1. 데이터를 직접 prefetch하고, initial data로 전달한다.
    • 간단한 경우를 위한 빠른 셋업
    • 몇가지 주의사항이 있음
  2. 서버에서 query를 prefetch하고, cache를 dehydrate하고 client에 rehydrate한다.
    • front에 약간의 더 많은 설정이 필요함

Using Next.js

두가지 형태의 pre-rendering을 지원하는 Next.js와 시작해보자!

  • Static Generation
  • Server-side Rendering

React Query는 플랫폼에 관계없이 이러한 형태의 사전 렌더링을 모두 지원한다.

Using initialData

Next.js의 getStaticProps 또는 getServerSideProps에서, 둘 중 하나의 메서드에서 가져오는 data를 useQueryinitialData 옵션에 전달할 수 있다. React Query의 관점에서 이들은 동일한 방식으로 integrate(통합)된다.

export async function getStaticProps() {
  const posts = await getPosts()
  return { props: { posts } }
}

function Posts(props) {
  const { data } = useQuery({
    queryKey: ['posts'],
    queryFn: getPosts,
    initialData: props.posts,
  })

  // ...
}

설정이 쉽고, 경우에 따라 빠른 솔루션이 될 수 있지만 전체 접근 방식과 비교할때 고려해야 할 몇가지 장단점이 있다.

  • 만약 컴포넌트 딮한곳에서 useQuery를 call 한다면, initialData를 그 지점까지 전달해줘야 한다.
  • 만약 useQuery를 동일한 query로 여러 지역에서 call 한다면, initialData를 그들 모두에게 전달해줘야 한다.
  • 서버에서 쿼리를 가져온 시간을 알 수 있는 방법은 없으므로, dataUpdatedAt 및 쿼리를 다시 가져와야 하는지 여부를 결정하는 것은 페이지가 로드된 시간에 따라 결정된다.

Using Hydration

React query는 Next.js 서버에서 prefetching multiple queries를 지원한다. 그리고 해당 쿼리를 queryClient로 dehydrating한다. 이것은 서버가 페이지 로드 시 즉시 사용할 수 있는 마크업을 미리 렌더링할 수 있다는 것을 의미하며, js가 사용 가능한 즉시 리액트 쿼리는 라이브러리의 전체 기능으로 이러한 쿼리를 업그레이드하거나 hydrate시킬 수 있다. 이 작업은 서버에서 렌더링된 이후로 쿼리가 오래된 경우 클라이언트에서 쿼리를 다시 페치하는 작업이 포함된다.

서버에서 캐싱 쿼리를 지원하고 hydration을 set up 하려면

  • 새로운 QueryClient instance를 당신의 app과 instance ref에 만든다. 이렇게 하면 컴포넌트 생명주기당 한 번만 QueryClient를 생성하면서 서로 다른 유저와 요청 사이의 data가 절대로 공유되지 않음을 보장한다.
  • 당신의 app 컴포넌트를 QueryClientProvider로 감싸서 클라이언트 instance로 전달한다.
  • 당신의 app 컴포넌트를 Hydrate로 감싸서 pageProps로 부터의 dehydratedState를 전달한다.
// _app.jsx
import {
  Hydrate,
  QueryClient,
  QueryClientProvider,
} from '@tanstack/react-query'

export default function MyApp({ Component, pageProps }) {
  const [queryClient] = React.useState(() => new QueryClient())

  return (
    <QueryClientProvider client={queryClient}>
      <<Hydrate state={pageProps.dehydratedState}>
        <Component {...pageProps} />
      </Hydrate>
    </QueryClientProvider>
  )
}

이제 우린 getStaticProps 또는 getServerSideProps로 페이지의 일부 데이터를 prefetch 할 준비가 되었다.
React Query의 관점에서 이들은 동일한 방식으로 integrate(통합)된다.

  • 새로운 QueryClient instance를 당신의 app과 instance ref에 만든다. 이렇게 하면 컴포넌트 생명주기당 한 번만 QueryClient를 생성하면서 서로 다른 유저와 요청 사이의 data가 절대로 공유되지 않음을 보장한다.
  • 클라이언트 prefetchQuery 메서드를 사용하여 데이터를 미리 가져오고 완료될 때까지 기다린다.
  • query cache를 dehydrate 하기위해서 dehydrate를 사용하고, dehydratedState prop을 통해 페이지로 전달한다.
// pages/posts.jsx
import { dehydrate, QueryClient, useQuery } from '@tanstack/react-query'

export async function getStaticProps() {
  const queryClient = new QueryClient()

  await queryClient.prefetchQuery(['posts'], getPosts)

  return {
    props: {
      dehydratedState: dehydrate(queryClient),
    },
  }
}

function Posts() {
  // 이러한 useQuery는 게시물 페이지의 더욱 깊은 하위 자식에서 발생할 수 있으며,
  // 데이터는 어느 쪽이든지 즉시 사용이 가능하다.
  const { data } = useQuery({ queryKey: ['posts'], queryFn: getPosts })

  // 이 쿼리는 서버에서 prefetch 되지 않았으며, 클라이언트에서 불러올때까지 시작하지 않는다.
  // 이렇게 두 패턴을 혼합해도 괜찮다.
  const { data: otherData } = useQuery({
    queryKey: ['posts-2'],
    queryFn: getPosts,
  })

  // ...
}

Caveat for Next.js rewrites

Next.js의 rewrites 기능을 Automatic Static Optimization과 함께 사용하거나 StaticProps와 함께 사용하면 다음과 같은 단점이 있습니다.

바로 React Query에 의한 두번째 hydration이 발생한다.

그 이유는 Next.js가 router.query에서 제공될 수 있도록 클라이언트에서 다시 쓰기를 구문 분석하고 hydration 후 매개 변수를 수집해야 하기 때문이다.

그 결과, 모든 hydration data의 참조 동일성이 누락된다. 이러한 예로는 컴포넌트의 props로 사용되는 data, useEffect/useMemo의 의존성 배열에 사용되는 data가 있다.

끝으로 chatGPT의 대답

React Query v3과 v4의 주요 차이점은 다음과 같습니다.

QueryKey

React Query v4에서는 QueryKey를 사용하여 쿼리의 종류와 관련된 데이터를 쉽게 식별할 수 있습니다. 이전 버전의 React Query에서는 각 쿼리에 고유한 문자열 식별자를 제공해야 했습니다. 그러나 v4에서는 객체, 배열 또는 함수를 사용하여 QueryKey를 구성할 수 있으므로 쿼리 구성이 훨씬 유연해졌습니다.

새로운 쿼리 컴포넌트

React Query v4에서는 새로운 Query 및 Mutation 컴포넌트가 추가되었습니다. 이 컴포넌트들은 useQuery 및 useMutation 훅을 래핑하며, 컴포넌트를 사용하여 리액트 애플리케이션의 상태를 더욱 쉽게 관리할 수 있습니다.

타입스크립트 지원

React Query v4에서는 타입스크립트를 지원하기 위해 많은 개선이 이루어졌습니다. 예를 들어, QueryKey 및 MutationOptions에 대한 타입 지원이 추가되었습니다.

멀티캐시

React Query v4에서는 멀티캐시라는 개념이 도입되었습니다. 이는 캐시된 데이터를 여러 위치에서 사용할 수 있도록 허용하며, 이전 버전에서는 캐시 데이터를 변경하기 위해서는 쿼리를 다시 실행해야 했습니다.

성능 개선

React Query v4에서는 쿼리 결과 데이터의 크기를 줄이고 캐시 불일치를 방지하는 등의 성능 개선이 이루어졌습니다. 또한 v4에서는 쿼리 결과 데이터를 직렬화하여 네트워크 전송 속도를 높이는 기능도 추가되었습니다.

이러한 변경 사항을 통해 React Query v4는 더욱 유연하고 강력한 캐싱 및 데이터 관리 라이브러리가 되었습니다.

profile
그냥 개인적으로 공부한 글들에 불과

2개의 댓글

comment-user-thumbnail
2023년 3월 26일

와 리액트 쿼리!

1개의 답글