
RTK Query는 강력한 데이터 fetch 및 cashing 도구입니다.
웹 애플리케이션에서 일반적인 데이터 loadingr과 cashing을 단순하게 작업할 수 있도록 설계하여 데이터 fetch 및 cashing 로직을 직접 작성할 필요가 없습니다.
RTK 쿼리는 Redux 툴킷 패키지에 포함된 선택적 애드온이며 해당 기능은 Redux 툴킷의 다른 API 위에 구축됩니다.
createApi를 구성하는 전체 구조를 알아본다.
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
expot const postApi = createApi({
reducerPath: 'postApi',// 없는 경우는 api가 디폴트로 정해진다.
baseQuery: fetchBaseQuery({ baseUrl: 'http://localhost:3500'}),
// 재 랜드링을 위한 테그 설정. RTK Query의 기본 랜드링은 tag시스템으로 동작한다.
//만약 자료가 수정되었을 때 Tag를 invalides로 설정하면 해당 tag가 설정된
// 캐쉬 자료를 리플레시하는 기본 tag 이름 정의 , 예 ['Users', 'Posts']
tagTypes: ['Post'],
//각각의 query/mutation 동작을 여기서 정의한다.
endpoints: (build) => ({
// build.query는 get에 해당되는 query이다.
getPost: build.query({
// id는 호출에서 받은 값.
query: (id) => ({ url: `post/${id}` }),
// query의 결과로 받은 내용을 변경.
// resposedData는 query의 결과로 받은 값,
// meta는 호출하는 곳에서 세팅한 Object,
// arg는 호출하는 곳에 전달한 기본 parameter 값, 여기서는 id
// 리턴 값 responsedData.data는 Data를 이용하는 곳에서 nest을 줄이기 위함.
transformResponse: (responsedData, meta, arg) => responsedData.data,
// 쿼리 결과에 tag을 주입한다. 나중에 invalidesTags로 재 호출/랜드링한다.
providesTags: (result, error, id) => [{ type: 'Post', id }],
// query결과를 받아서 서버에 요청하여 결과를 받기 전에 캐쉬를 update하거나 다른 처리로 가공할 수 있다
async onQueryStarted(
arg,
{dispatch,getState,extra,requestId,queryFulfilled,getCacheEntry,updateCachedData}
//queryFulfilled는 Promise
) {},
// The 2nd parameter is the destructured `QueryCacheLifecycleApi`
async onCacheEntryAdded(
arg, //endpoint 호출에서 전달된 값 예, { id, post}
{ dispatch, getState, extra, requestId,cacheEntryRemoved, cacheDataLoaded, getCacheEntry, updateCachedData}
) {},
}),
// mutation은 데이터 조작을 정의한다, POST, PUT, DELETE, PATCH
updatePost: build.mutation({
query: ({ id, ...patch }) => ({
url: `post/${id}`,
method: 'PATCH',
body: patch,
}),
// 데이타 변환
//arg : endpoint 호출에서 전달된 값 예, { id, post}
transformResponse: (response, meta, arg) => response.data,
//Post로 등록된 모든 캐쉬 내용을 업데이트, tag에 따라 수정한 곳만 업데이트 가능.
// [{type:'Post', id}]가 등록되어 있고 이 것을 invalidatesTags로 호출하면 행당 id만 재 호출/랜드링.
invalidatesTags: ['Post'],
// onQueryStarted는 optimistic 업데이트에 유용합니다.
// 이때 queryFulfilled(Promise) 을 실행하여 쿼리 확정한 후 에러면 1)redo 2) 전체 refetching
async onQueryStarted(
arg,
{ dispatch, getState, queryFulfilled, requestId, extra, getCacheEntry }
) {},
async onCacheEntryAdded(
arg,
{ dispatch, getState, extra, requestId, cacheEntryRemoved, cacheDataLoaded, getCacheEntry}
) {},
}),
}),
})
//정의된 endpoints을 기반으로 자동 생성됨. getPost=>useGetPostQuery
export const { useGetPostQuery, useUpdatePostMutation } = postApi
data/db.json 파일에 다음과 같이 데이타를 정리한다.
https://jsonplaceholder.typicode.com/의 내용을 copy하는 것도 한 방법이다.
{
"posts":[
{ "id":1, "userId": 1, "title": "this is title", "body": "this is body"},
{...
],
"users": [
{ "id": 1, "name": "Brian Song", .... }
{ ...
]
}
json-server --watch data/db.json --port 3500 으로 서버 구동
"API Slice"에는 자동 생성된 Redux Slice reducer와 subscription lifetimes을 관리하는 사용자 지정 미들웨어도 포함되어 있습니다. 둘 다 Redux store에 추가해야 한다
import { configureStore } from '@reduxjs/toolkit'
// Or from '@reduxjs/toolkit/query/react'
import { setupListeners } from '@reduxjs/toolkit/query'
import { postApi } from './services/posts'
export const store = configureStore({
reducer: {
// Add the generated reducer as a specific top-level slice
[postApi.reducerPath]: postApi.reducer,
},
// API 미들웨어를 추가하면 cahsing, invalidation, polling, 기타 유용한 기능 사용 가능.
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(postApi.middleware),
//getDefaultMiddleware().concat(tokenListsApi.middleware)
})
// 선택적, "Refetch On Focus/refetch Reconnect" 동작에 필수 - 사용자 지정을 위한 두 번째 인수로 선택적 콜백을 사용합니다.
setupListeners(store.dispatch)
optimistic 업데이트: 보동 업데이트는 쿼리를 실행하면 서버에 내용을 요청/수정한 다음 데이트를 받아와서 캐쉬를 업데이트한다. optimistic은 낙관적으로 어차피 캐쉬가 나중에 업데이트 될 것을 가정하여 캐쉬를 업데이트와 쿼리를 실행을 같이 하고 나중에 서버에서 받은 결과로 검증하여 UI의 속도감을 높히는 방식의 업데이트.
마지막으로 API 슬라이스에서 자동 생성된 hook를 component 파일로 가져와서(hooks 이용) 필요한 매개변수를 사용하여 hook에서 제공된 endpoint의 query/mutation을 호출합니다.
RTK 쿼리는 마운트시 자동으로 데이터를 가져오고, parameter가 변경되면 다시 가져온다. 결과 {data, isFetching, isSucess, ...} 값을 받는다.
해당 값이 변경되면 component를 다시 렌더링합니다.
export const PostDetail = ({ id }) => {
const { data: post, isFetching, isLoading } =
useGetPostQuery(id, {
pollingInterval: 3000,
refetchOnMountOrArgChange: true,
skip: false,
}) // 두번째 인자는 옵션
if (isLoading) return <div>Loading...</div>
if (!post) return <div>Missing post!</div>
return (
<div>
{post.name} {isFetching ? '...refetching' : ''}
</div>
)
}
쿼리 작업은 선택한 모든 데이터 가져오기 라이브러리로 수행할 수 있지만 일반적으로 데이터를 검색하는 요청에 대해서만 쿼리를 사용하는 것이 좋습니다. 서버의 데이터를 변경하거나 캐시를 invalidation할 가능성이 있는 모든 경우에는 Mutation을 사용해야 합니다.
기본적으로 RTK 쿼리는 axios와 같은 일반 라이브러리와 유사한 방식으로 request headers 및 response parsing을 자동으로 처리하는 lightweight fetch wrapper인 fetchBaseQuery와 함께 제공됩니다. fetchBaseQuery가 요구 사항을 만족시킬 수 없는 경우 useQuery를 이용하여 사용자 정의할 수 있다.
Query endpoints는 createApi 안의 endpoints섹션에서 개체를 반환으로 정의 되고 필드들은 builder.query() 메서드에 정의 된다.
Query endpoints은 URL을 만드는 쿼리 콜백(쿼리 매개변수 포함) 또는 임의의 비동기 논리를 수행하고 결과를 반환할 수 있는 queryFn 콜백을 정의해야 합니다.
쿼리 콜백이 URL을 생성하기 위해 추가 데이터가 필요한 경우 단일 인수를 사용하도록 작성해야 합니다. 여러 매개변수를 전달해야 하는 경우 단일 "옵션 Object" 형식으로 전달하십시오.
Query endpoints은 결과가 캐시되기 전에 응답 내용을 수정하고, 캐시 invalidation를 식별하기 위한 "tag"를 정의하여, 캐시 항목의 추가 및 제거 같은 추가 논리를 실행하기 위해 캐시 항목 수명주기 콜백(cache entry lifecycle callbacks)을 제공할 수 있습니다.
React Hooks를 사용하는 경우 RTK 쿼리가 몇 가지 추가 작업을 수행합니다. 주요 이점은 편의를 위해 쿼리 상태의 Boolean 값 뿐만 아니라 'background fetching'를 가능케하는 렌더링 최적화 후크을 얻을 수 있다는 것입니다.
후크는 api endpoints 이름에 따라 자동으로 생성됩니다. getPost: builder.query()가 있는 endpoint 필드는 useGetPostQuery라는 후크를 생성합니다.
쿼리 후크는 두 개의 매개변수(queryArg?, queryOptions?)를 필요로 합니다.
queryArg 매개변수는 기본 쿼리 콜백으로 전달되어 URL을 생성합니다.
queryOptions 개체는 데이터 가져오기 동작을 제어하는 데 사용할 수 있는 몇 가지 추가 매개변수를 허용합니다.
쿼리 후크는 쿼리 요청에 대한 최신 데이터, 현재 request lifecycle 상태가 포함된 Object를 반환합니다. 다음은 가장 자주 사용되는 속성 중 일부입니다.
대부분의 경우 UI를 렌더링하기 위해 데이터와 isLoading 또는 isFetching을 체크함.
isLoading과 isFetching은 기존 query로 가져온 데이터가 있는냐에 따라 다름. 예를 들어 Pagination 경우 기존 페이지에서 다른 page로 이동할 때 isFetching 사용
isLoading은 첫 request에만 isFetching 모두 사용
createApi의 자동 생성 React 후크는 주어진 쿼리의 현재 상태를 반영하는 생태값들을 제공합니다. 상태값들은 상태 플래그와 반대로 생성된 React 후크에 대해 선호됩니다. 상태값들은 주어진 시간에 여러 상태가 참일 수 있으므로 단일 상태 플래그로는 불가능한 더 많은 양의 세부 정보를 제공할 수 있기 때문입니다. (isFetching 및 isSuccess와 같은).
이러한 구분을 통해 UI 동작을 처리할 때 더 확실한 제어가 가능합니다. 예를 들어, isLoading은 처음 로드하는 동안 skeleton을 표시하는 데 사용할 수 있으며 isFetching은 1페이지에서 2페이지로 변경하거나 데이터가 무효화되고 다시 가져올 때 오래된 데이터를 회색으로 표시하는 데 사용할 수 있습니다.
/* ---------------- data -------------------- */
import { Skeleton } from './Skeleton'
import { useGetPostsQuery } from './api'
function App() {
const { data = [], isLoading, isFetching, isError } = useGetPostsQuery()
if (isError) return <div>An error has occurred!</div>
if (isLoading) return <Skeleton />
return (
<div className={isFetching ? 'posts--disabled' : ''}>
{data.map((post) => (
<Post key={post.id} id={post.id} name={post.name} disabled={isFetching} />
))}
</div>
)
}
/* ----------------currentData-------------------- */
import { Skeleton } from './Skeleton'
import { useGetPostsByUserQuery } from './api'
function PostsList({ userName }: { userName: string }) {
const { currentData, isFetching, isError } = useGetPostsByUserQuery(userName)
if (isError) return <div>An error has occurred!</div>
if (isFetching && !currentData) return <Skeleton />
return (
<div className={isFetching ? 'posts--disabled' : ''}>
{currentData
? currentData.map((post) => (
<Post key={post.id} id={post.id} name={post.name} disabled={isFetching} />
))
: 'No data available'}
</div>
)
}
useQuery hook를 이용한 endpoint query의 다른 인스턴스들은 각 독립적으로 작용한다.
쿼리를 수행하면 RTK 쿼리가 자동으로 request parameters를 직렬화하고, 각 request에 대한 내부 queryCacheKey를 생성합니다. 그러나 동일 queryCacheKey를 생성하는 향후 요청은 원본에 대한 중복이 제한되며, 등록된 어떤한 componet에서 쿼리에대한 refetch가 호출되는 경우 업데이트를 결과를 공유합니다.
때때로 쿼리에 등록된 상위 구성 요소가 있고 그 하위 구성 요소에서 해당 쿼리에서 한가지 항목을 선택하려고 할 수 있습니다. 대부분의 경우 이미 결과가 있다는 것을 알고 있으면 getItemById 유형의 쿼리로 추가 request을 수행하고 싶지 않습니다.
selectFromResult를 사용하면 성능적으로 쿼리 결과에서 특정 세그먼트를 구할 수 있다. 이 기능을 사용하면 선택한 항목의 기본 데이터가 변경되지 않는 한 구성 요소가 다시 렌더링되지 않습니다. 선택한 항목이 더 큰 컬렉션의 한 요소인 경우 동일한 컬렉션 안의 요소들에 대한 변경 사항을 무시합니다.
function PostsList() {
const { data: posts } = api.useGetPostsQuery()
return (
<ul>
{posts?.data?.map((post) => (
<PostById key={post.id} id={post.id} />
))}
</ul>
)
}
function PostById({ id }: { id: number }) {
// Will select the post with the given id,
// and will only rerender if the given posts data changes
const { post } = api.useGetPostsQuery(undefined, {
selectFromResult: ({ data }) => ({
post: data?.find((post) => post.id === id),
}),
})
return <li>{post?.name}</li>
}
강제로 다시 렌더링할지 여부를 결정하기 위해 selectFromResult의 전체 반환 값에 대해 얕은 동등성 검사가 수행됩니다. 즉, 반환된 개체 값 중 하나라도 참조를 변경하면 다시 렌더링을 트리거합니다. 새로운 배열/객체를 생성하여 콜백 내에서 반환 값으로 사용하면 콜백이 실행될 때마다 새 항목으로 식별되기 때문에 성능상의 이점을 방해합니다. 의도적으로 빈 배열/객체를 제공할 때 콜백이 실행될 때마다 다시 생성되는 것을 방지하기 위해 안정적인 참조를 유지하기 위해 구성 요소 외부에 빈 배열/객체를 선언할 수 있습니다.
/* -----------------*/
// An array declared here will maintain a stable
// reference rather than be re-created again
const emptyArray: Post[] = []
function PostsList() {
// This call will result in an initial render returning an empty array for `posts`,
// and a second render when the data is received.
// It will trigger additional rerenders only if the `posts` data changes
const { posts } = api.useGetPostsQuery(undefined, {
selectFromResult: ({ data }) => ({
posts: data ?? emptyArray,
}),
})
return (
<ul>
{posts.map((post) => (
<PostById key={post.id} id={post.id} />
))}
</ul>
)
}
기본적으로 기존 쿼리와 동일한 쿼리를 만드는 구성 요소를 추가하면 요청이 수행되지 않습니다.
어떤 경우에는 이 동작을 건너뛰고 강제로 다시 가져오기를 원할 수 있습니다. 이 경우 후크에서 반환되는 refetch를 호출할 수 있습니다.
const { status, data, error, refetch } = dispatch(
pokemonApi.endpoints.getPokemon.initiate('bulbasaur')
)
mutation는 데이터 업데이트를 서버로 보내고 변경 사항을 로컬 캐시에 적용하는 데 사용됩니다. mutation는 캐시된 데이터를 무효화하고 강제로 다시 가져올 수도 있습니다.
mutation endpoint는 createApi의 endpoint 섹션 내부에 Object를 반환하고 build.mutation() 메서드를 사용하여 필드를 정의하여 정의됩니다.
mutation endpoint는 URL을 구성하는 query 콜백(모든 URL 쿼리 매개변수 포함) 또는 임의의 비동기 논리를 수행하고 결과를 반환할 수 있는 queryFn 콜백을 정의해야 합니다. 쿼리 콜백은 URL, 사용할 HTTP method, request, body을 포함하는 Object를 반환할 수도 있습니다.
쿼리 콜백이 URL을 생성하기 위해 추가 데이터가 필요한 경우 단일 인수를 사용하도록 작성해야 합니다. 여러 매개변수를 전달해야 하는 경우 단일 "옵션 Object" 형식으로 전달하십시오.
mutation endpoint는 결과가 캐시되기 전에 응답 내용을 수정하고, 캐시 invalidation를 식별을 위해 "tags"를 정의하여, 캐시 항목이 추가 및 제거될 때 추가 논리를 실행하기 위해 캐시 항목 lifecycle 콜백을 제공할 수 있습니다.
endpoints: (build) => ({
updatePost: build.mutation({
// note: an optional `queryFn` may be used in place of `query`
query: ({ id, ...patch }) => ({
url: `post/${id}`,
method: 'PATCH',
body: patch,
}),
transformResponse: (response, meta, arg) => response.data,
invalidatesTags: ['Post'],
// Optimistic 업데이트를 위해
async onQueryStarted(
arg,
{ dispatch, getState, queryFulfilled, requestId, extra, getCacheEntry }
) {},
async onCacheEntryAdded(
arg,
{ dispatch, getState, extra, requestId, cacheEntryRemoved, cacheDataLoaded,getCacheEntry }
) {},
}
useQuery와 달리 useMutation은 튜플을 반환합니다. 튜플의 첫 번째 항목은 trigger 함수, Object{state, error, data}를 포함합니다.
useQuery 후크와 달리 useMutation 후크는 자동으로 실행되지 않습니다. mutation를 실행하려면 후크에서 첫 번째 튜플 값으로 반환된 trigger 함수를 호출해야 합니다.
const [addNewPost, { isLoading, isError } ] = useAddNewPostMutation()
useMutation 후크는 "mutation trig" 기능을 포함하는 튜플과 "mutation 결과"에 대한 속성을 포함하는 객체를 반환합니다.
"mutation trigger"는 호출은 해당 endpoint에 대한 mutation request을 시작하는 기능입니다. "mutation trigger"를 호출하면, unwrap 프로퍼티를 가진 Promise을 리턴합니다.프로미스에소 unwrap을 호출하면 바로 raw response/error 제공한다. 이는 호출 사이트에서 인라인으로 mutation의 성공/실패 여부를 결정하려는 경우에 유용할 수 있습니다.
"mutation 결과"는 최신 데이터와 현재 request lifecycle 상태 속성 등을 포함하는 Object입니다.
다음은 "mutation 결과" 개체에서 가장 자주 사용되는 속성 중 일부입니다.
mutation query는 후속 requst가 존재하지 않는 고로 isFatching이 없다
기본적으로 useMutation 후크의 개별 인스턴스는 본질적으로 서로 관련되지 않습니다. 한 인스턴스를 트리거해도 별도의 인스턴스에 대한 결과에는 영향을 미치지 않습니다.
RTK 쿼리는 fixedCacheKey 옵션을 사용하여 mutation 후크 인스턴스 간에 결과를 공유하는 옵션을 제공합니다. 동일한 fixedCacheKey 문자열을 사용하는 모든 useMutation 후크는 트리거 함수가 호출될 때 서로 결과를 공유합니다. 결과를 공유하려는 각 mutation 후크 인스턴스 간에 공유되는 고유한 문자열이어야 합니다.
export const ComponentOne = () => {
// Triggering `updatePostOne` will affect the result in both this component,
// but as well as the result in `ComponentTwo`, and vice-versa
const [updatePost, result] = useUpdatePostMutation({
fixedCacheKey: 'shared-update-post',//고유 문자열
})
return <div>...</div>
}
export const ComponentTwo = () => {
const [updatePost, result] = useUpdatePostMutation({
fixedCacheKey: 'shared-update-post',//고유 문자열
})
return <div>...</div>
}
이 시나리오에서는 useQuery를 사용하여 게시물을 가져온 다음 게시물 이름을 편집할 수 있는 EditablePostName 구성 요소가 렌더링됩니다.
export const PostDetail = () => {
const { id } = useParams<{ id: any }>()
const { data: post } = useGetPostQuery(id)
const [
updatePost, // This is the mutation trigger
{ isLoading: isUpdating }, // This is the destructured mutation result
] = useUpdatePostMutation()
return (
<Box p={4}>
<EditablePostName
name={post.name}
onUpdate={(name) => {
// If you want to immediately access the result of a mutation, you need to chain `.unwrap()`
// if you actually want the payload or to catch the error.
// Example: `updatePost().unwrap().then(fulfilled => console.log(fulfilled)).catch(rejected => console.error(rejected))
return (
// Execute the trigger with the `id` and updated `name`
updatePost({ id, name })
)
}}
isLoading={isUpdating}
/>
</Box>
)
}
현실 세계에서는 개발자가 mutation를 수행한 후 로컬 데이터 캐시를 서버와 재동기화(일명 "revalidation")하려고 하는 것이 매우 일반적입니다. RTK 쿼리는 이에 대한 보다 중앙 집중식 접근 방식을 취하며 API 서비스 정의에서 invalidateTags 동작을 구성해야 합니다.
// Or from '@reduxjs/toolkit/query' if not using the auto-generated hooks
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
export const postApi = createApi({
reducerPath: 'postsApi',
baseQuery: fetchBaseQuery({ baseUrl: '/' }),
tagTypes: ['Posts'],
endpoints: (build) => ({
getPosts: build.query({
query: () => 'posts',
// Provides a list of `Posts` by `id`.
// If any mutation is executed that `invalidate`s any of these tags, this query will re-run to be always up-to-date.
// The `LIST` id is a "virtual id" we just made up to be able to invalidate this query specifically if a new `Posts` element was added.
providesTags: (result) =>
// is result available?
result
? [...result.map(({ id }) => ({ type: 'Posts', id })),
{ type: 'Posts', id: 'LIST' },
]
: // an error occurred, but we still want to refetch this query when `{ type: 'Posts', id: 'LIST' }` is invalidated
[{ type: 'Posts', id: 'LIST' }],
}),
addPost: build.mutation({
query(body) {
return {
url: `post`,
method: 'POST',
body,
}
},
// Invalidates all Post-type queries providing the `LIST` id - after all, depending of the sort order,
// that newly created post could show up in any lists.
invalidatesTags: [{ type: 'Posts', id: 'LIST' }],
}),
getPost: build.query({
query: (id) => `post/${id}`,
providesTags: (result, error, id) => [{ type: 'Posts', id }],
}),
updatePost: build.mutation({
query(data) {
const { id, ...body } = data
return {
url: `post/${id}`,
method: 'PUT',
body,
}
},
// Invalidates all queries that subscribe to this Post `id` only.
// In this case, `getPost` will be re-run. `getPosts` *might* rerun, if this id was under its results.
invalidatesTags: (result, error, { id }) => [{ type: 'Posts', id }],
}),
deletePost: build.mutation({
query(id) {
return {
url: `post/${id}`,
method: 'DELETE',
}
},
// Invalidates all queries that subscribe to this Post `id` only.
invalidatesTags: (result, error, id) => [{ type: 'Posts', id }],
}),
}),
})
export const {
useGetPostsQuery,
useAddPostMutation,
useGetPostQuery,
useUpdatePostMutation,
useDeletePostMutation,
} = postApi
RTK 쿼리의 주요 기능은 캐시된 데이터의 관리입니다. 서버에서 데이터를 가져오면 RTK 쿼리는 Redux store에 데이터를 '캐시'로 저장합니다. 동일한 데이터에 대해 추가 요청이 수행되면 RTK 쿼리는 서버에 추가 요청을 보내는 대신 기존에 캐시된 데이터를 제공합니다.
RTK 쿼리는 캐시 동작을 조작하고 필요에 맞게 조정할 수 있는 다양한 개념과 도구를 제공합니다.
RTK 쿼리에서 캐싱은 다음을 기반으로 합니다.
등록(subscription)이 시작되면 endpoint와 parameters가 함께 직렬화되고, request를 위해 내부에 queryCacheKey로 저장됩니다. 그러나 동일 queryCacheKey를 생성하는 향후 request은 원본에 대한 중복이 제한되며, 같은 data와 update을 공유한다.
request가 시도될 때 데이터가 이미 캐시에 있는 경우 해당 데이터가 제공되고 새 요청이 서버로 전송되지 않습니다. 데이터가 캐시에 없으면 새 요청이 전송되고 반환된 응답은 캐시에 저장됩니다.
Subscriptions은 참조수로 계산됩니다. 동일한 endpoint+매개변수를 요청하는 추가 Subscriptions은 참조 횟수를 증가시킵니다. 데이터에 대한 활성 'Subscription'이 있는 한(예: 엔드포인트에 대한 useQuery 후크를 호출하는 구성 요소가 마운트된 경우) 데이터는 캐시에 남아 있습니다. Subscription이 제거되면(예: 데이터를 subscribe한 마지막 구성 요소가 마운트 해제될 때) 일정 시간(기본값 60초) 후에 데이터가 캐시에서 제거됩니다. 만료 시간은 API 정의 전체에 대한 keepUnusedDataFor 속성을 사용하여 구성할 수 있을 뿐만 아니라 endpoint별로 구성할 수 있습니다.
기본 동작 외에도 RTK 쿼리는 데이터가 유효하지 않은 것으로 간주되어야 하거나 그렇지 않으면 'refreshed'하기에 적합한 것으로 간주되는 시나리오에서 데이터를 더 일찍 다시 가져오는 여러 방법을 제공합니다.
기본 캐시 동작 및 캐시 수명 및 subscribe는 예제에서 위에서 언급했듯이 기본적으로 데이터는 sbusciption 참조 수가 0에 도달한 후 60초 동안 캐시에 남아 있습니다.
이 값은 API 정의 부분과 각 endpoint에서 keepUnusedDataFor 옵션을 사용하여 구성할 수 있습니다. endpoint의 경우가 API 정의의 설정보다 우선 적용됩니다.
keepUnusedDataFor에 값을 초 단위로 제공하면 subscriber 참조 횟수가 0에 도달한 후 데이터를 캐시에 보관해야 하는 기간을 지정합니다.
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
export const api = createApi({
baseQuery: fetchBaseQuery({ baseUrl: '/' }),
// global configuration for the api
keepUnusedDataFor: 30,//모든 참조가 없어진 후 30초간 Data유지
endpoints: (builder) => ({
getPosts: builder.query({
query: () => `posts`,
// configuration for an individual endpoint, overriding the api setting
keepUnusedDataFor: 5,//모든 참조가 없어진 후 5초간 Data유지
}),
}),
})
데이터 다시 가져오기를 완벽하게 세부적으로 제어하기 위해 useQuery 또는 useQuerySubscription 후크에서 결과 속성으로 반환된 다시 가져오기 기능을 사용할 수 있습니다.
refetch 함수를 호출하면 연결된 쿼리를 강제로 다시 가져옵니다.
또는 동일한 효과를 위해 forceRefetch:true 옵션으로 endpoint에 대한 initiate thunk action을 dispatch할 수 있습니다.
import { useDispatch } from 'react-redux'
import { useGetPostsQuery } from './api'
const Component = () => {
const dispatch = useDispatch()
const { data, refetch } = useGetPostsQuery({ count: 5 })
function handleRefetchOne() {
// force re-fetches the data
refetch()
}
function handleRefetchTwo() {
// has the same effect as `refetch` for the associated query
dispatch(
api.endpoints.getPosts.initiate(
{ count: 5 },
{ subscribe: false, forceRefetch: true }
)
)
}
return (
<div>
<button onClick={handleRefetchOne}>Force re-fetch 1</button>
<button onClick={handleRefetchTwo}>Force re-fetch 2</button>
</div>
)
}
쿼리는 refetchOnMountOrArgChange 속성을 통해 평소보다 더 자주 re-fetch할 수 있습니다. 이는 전체적으로 endpoint에 전달로, 개별 후크 호출로 또는 initiate action을 dispatch로 가능합니다. (action creator's option의 이름은 forceRefetch임).
refetchOnMountOrArgChange는 기본 동작이 캐시된 데이터를 제공하는 대신 re-fetching울 해야하는 추가적인 상황에 사용됩니다.
refetchOnMountOrArgChange는 부울 값이나 숫자를 초 단위의 시간으로 허용합니다.
이 속성에 대해 false(기본값)를 전달하면 위에서 설명한 기본 동작이 사용됩니다.
이 속성에 대해 true를 전달하면 쿼리에 대한 새 subscriber가 추가될 때 endpoint가 데이타를 다시 가져옵니다. API 정의 자체가 아닌 개별 후크 호출에 전달된 경우 이는 해당 후크 호출에만 적용됩니다. 즉, 후크를 호출하는 구성 요소가 마운트되거나 인수가 변경되면 endpoint + 인수 조합에 대해 캐시된 데이터가 이미 존재하는지 여부에 관계없이 항상 다시 가져옵니다.
숫자를 초 단위 값으로 전달하면 다음 동작이 사용됩니다.
/* Configuring re-fetching on subscription if data exceeds a given time */
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
export const api = createApi({
baseQuery: fetchBaseQuery({ baseUrl: '/' }),
// global configuration for the api
refetchOnMountOrArgChange: 30,
endpoints: (builder) => ({
getPosts: builder.query({
query: () => `posts`,
}),
}),
})
/* Forcing refetch on component mount */
import { useGetPostsQuery } from './api'
const Component = () => {
const { data } = useGetPostsQuery(
{ count: 5 },
// this overrules the api definition setting,
// forcing the query to always fetch when this component is mounted
{ refetchOnMountOrArgChange: true }
)
return <div>...</div>
}
refetchOnFocus 옵션을 사용하면 응용 프로그램 창이 다시 포커스를 얻은 후 RTK 쿼리가 모든 sbusciption 쿼리를 다시 가져오려고 할 것인지 여부를 제어할 수 있습니다.
skip: true와 함께 이 옵션을 지정하면 skip이 false가 될 때까지 평가되지 않습니다.
이를 위해서는 setupListeners가 호출되어야 합니다.
이 옵션은 createApi를 사용한 API 정의와 useQuery, useQuerySubscription, useLazyQuery 및 useLazyQuerySubscription 후크에서 모두 사용할 수 있습니다.
/* api */
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
export const api = createApi({
baseQuery: fetchBaseQuery({ baseUrl: '/' }),
// global configuration for the api
refetchOnFocus: true,
endpoints: (builder) => ({
getPosts: builder.query({
query: () => `posts`,
}),
}),
})
/* store */
import { configureStore } from '@reduxjs/toolkit'
import { setupListeners } from '@reduxjs/toolkit/query'
import { api } from './services/api'
export const store = configureStore({
reducer: {
[api.reducerPath]: api.reducer,
},
middleware: (gDM) => gDM().concat(api.middleware),
})
// enable listener behavior for the store
setupListeners(store.dispatch)
createApi의 refetchOnReconnect 옵션을 사용하면 네트워크 연결을 다시 얻은 후 RTK 쿼리가 subscribed된 모든 쿼리를 다시 가져오려고 할 것인지 여부를 제어할 수 있습니다.
skip: true와 함께 이 옵션을 지정하면 skip이 false가 될 때까지 평가되지 않습니다.
이렇게 하려면 setupListeners가 호출되어야 합니다.
이 옵션은 createApi를 사용한 API 정의와 useQuery, useQuerySubscription, useLazyQuery 및 useLazyQuerySubscription 후크에서 모두 사용할 수 있습니다.
/* api */
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
export const api = createApi({
baseQuery: fetchBaseQuery({ baseUrl: '/' }),
// global configuration for the api
refetchOnReconnect: true,
endpoints: (builder) => ({
getPosts: builder.query({
query: () => `posts`,
}),
}),
})
/* store */
import { configureStore } from '@reduxjs/toolkit'
import { setupListeners } from '@reduxjs/toolkit/query'
import { api } from './services/api'
export const store = configureStore({
reducer: {
[api.reducerPath]: api.reducer,
},
middleware: (gDM) => gDM().concat(api.middleware),
})
// enable listener behavior for the store
setupListeners(store.dispatch)
RTK 쿼리는 캐시 태그 시스템을 사용하여 mutation endpoint의 영향을 받는 데이터가 있는 쿼리 endpoint에 대한 자동 re-fetching를 합니다.
기본 캐시 동작에서 볼 수 있듯이 쿼리 endpoint에 대한 subscription이 추가되면 캐쉬 데이터가 아직 존재하지 않는 경우에만 요청이 전송됩니다. 존재하는 경우 기존 캐쉬되어 있는 데이터가 대신 제공됩니다.
RTK 쿼리는 "cache tag" 시스템을 사용하여 mutation-endpoint(수정)의 영향을 받는 데이터가 있는 query-endpoint(읽기)에 대한 re-fetching을 자동화합니다. 이를 통해 특정 mutation을 실행하면 특정 query-endpoint가 캐쉬된 데이터를 유효하지 않은 것으로 간주하고 활성 subscription이 있는 경우 데이터를 다시 가져오도록 API를 설계할 수 있습니다.
각 endpoint + 매개변수 조합은 고유한 queryCacheKey를 제공합니다. 캐시 태그 시스템은 개별 쿼리 캐시가 특정 태그를 제공했음을 RTK 쿼리에 알릴 수 있습니다. 만약 쿼리 캐시가 invalidate tag 제공한 것을 알리는 mutation이 발생하면 캐시된 데이터는 무효화된 것으로 간주되고, 캐시된 데이터에 대한 활성 subscription이 있는 경우 re-fetchg한다.
RTK 쿼리의 경우 태그는 re-fetching를 위해 캐싱 및 invalidation 동작을 제어하기 위해 특정 데이터 컬렉션에 지정할 수 있는 이름일 뿐입니다. mutation에 영향을 받았는 지를 결정하기 위해 캐쉬된 데이터에 붙은 (mutation에 의해 읽혔지를 나타내는) 라벨로 간주된다.
태그는 API를 정의할 때 tagTypes 인수에 정의됩니다. 예를 들어 Posts와 Users가 모두 있는 애플리케이션에서 createApi를 정의할 때 tagTypes: ['Post', 'User']를 정의할 수 있습니다.
개별 태그에는 문자열 이름으로 표시되는 유형과 문자열 또는 숫자로 표시되는 선택적 id가 있습니다. [ {type: 'Post', id: 1}].
쿼리는 캐시된 데이터가 태그를 제공하도록 할 수 있습니다. 이렇게 하면 쿼리에서 반환된 캐시된 데이터에 어떤 '태그'가 첨부될지 결정됩니다.
ProvideTags 인수는 ['Post'], [{type: 'Post', id: 1}] 또는 그러한 배열을 반환하는 콜백. 그 함수는 결과를 첫 번째 인수로, 응답 오류를 두 번째 인수로, 원래 쿼리 메서드에 전달된 인수를 세 번째 인수로 전달합니다. 쿼리의 성공 여부에 따라 결과 또는 오류 인수가 정의되지 않을 수 있습니다.
mutation는 태그를 기반으로 캐시된 특정 데이터를 무효화할 수 있습니다. 이렇게 하면 캐시에서 다시 가져오거나 제거할 캐시된 데이터가 결정됩니다.
invalidatesTags 인수는 ['Post']),[{type: 'Post', id: 1}], 또는 그러한 배열을 반환하는 콜백. 그 함수는 결과를 첫 번째 인수로, 응답 오류를 두 번째 인수로, 원래 쿼리 메서드에 전달된 인수를 세 번째 인수로 전달합니다. 결과 또는 오류 인수는 mutate의 성공 여부에 따라 정의되지 않을 수 있습니다.
RTK 쿼리는 'tags' 개념을 사용하여 한 endpoint에 대한 mutate가 다른 endpoint의 쿼리에서 제공한 일부 데이터를 invalidate 하려는 것인지 여부를 결정합니다.
캐시 데이터가 invalidation되면 제공 쿼리를 다시 가져오거나(구성 요소가 해당 데이터를 계속 사용하는 경우) 캐시에서 데이터를 제거합니다.
API 슬라이스를 정의할 때 createApi는 tagTypes 속성을 위해 태그-타입-이름 배열을 받습는다. 이 것은 쿼리가 제공할 수 있게 하는 가능한 태그-이름-옵션 목록입니다.
const api = createApi({
baseQuery: fetchBaseQuery({
baseUrl: '/',
}),
tagTypes: ['Post', 'User'],
각 개별 쿼리 endpoint는 캐시된 데이터가 특정 태그를 제공하도록 할 수 있습니다. 이렇게 하면 하나 이상의 query endpoint에서 캐시된 데이터와 하나 이상의 mutation endpoint 동작 간의 관계가 활성화됩니다.
쿼리 endpoint의 provideTags 속성이 이 용도로 사용됩니다.
endpoints: (build) => ({
getPosts: build.query({
query: () => '/posts',
providesTags: ['Post'],
}),
// 혹은
providesTags: (result, error, arg) =>
result
? [...result.map(({ id }) => ({ type: 'Post', id })), 'Post']
: ['Post'],
각 개별 mutation endpoint는 기존 캐시 데이터에 대한 특정 태그를 무효화할 수 있습니다. 이렇게 하면 하나 이상의 query endpoint에서 캐시된 데이터와 하나 이상의 mutation endpoint 동작 간의 관계가 활성화됩니다.
이를 위해 mutation endpoint의 invalidatesTags 속성이 사용됩니다.
addPost: build.mutation({
query: (body) => ({
url: 'post',
method: 'POST',
body,
}),
// General tag
invalidatesTags: ['Post'],
}),
editPost: build.mutation({
query: (body) => ({
url: `post/${body.id}`,
method: 'POST',
body,
}),
invalidatesTags: (result, error, arg) => [{ type: 'Post', id: arg.id }],
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
export const api = createApi({
baseQuery: fetchBaseQuery({ baseUrl: '/' }),
tagTypes: ['Posts'],
endpoints: (build) => ({
getPosts: build.query({
query: () => 'posts',
providesTags: (result) =>
result
? [ { type: 'Posts', id: 'LIST' },
...result.map(({ id }) => ({ type: 'Posts', id }))]
: [{ type: 'Posts', id: 'LIST' }]
}),
addPost: build.mutation({
query(body) {
return {
url: `post`,
method: 'POST',
body,
}
},
invalidatesTags: [{ type: 'Posts', id: 'LIST' }],
}),
getPost: build.query({
query: (id) => `post/${id}`,
providesTags: (result, error, id) => [{ type: 'Posts', id }],
}),
}),
})
export const { useGetPostsQuery, useAddPostMutation, useGetPostQuery } = api
/* App */
function App() {
const { data: posts } = useGetPostsQuery()
const [addPost] = useAddPostMutation()
return (
<div>
<AddPost onAdd={addPost} />
<PostsList />
{/* Assume each PostDetail is subscribed via `const {data} = useGetPostQuery(id)` */}
<PostDetail id={1} />
<PostDetail id={2} />
<PostDetail id={3} />
</div>
)
}
addPost가 실행되면 addPost가 'LIST' ID만 무효화하여 getPosts가 다시 실행되도록 하기 때문에 PostsList가 isFetching 상태가 되도록 합니다(특정 ID를 제공하기 때문). 따라서 네트워크 탭(dev tool)에서 GET /posts에 대해 1개의 새로운 요청 실행만 볼 수 있습니다. 단일 getPost 쿼리는 무효화되지 않았으므로 addPost의 결과로 다시 실행되지 않습니다.
addPost 변형이 개별 PostDetail 구성 요소를 포함한 모든 게시물을 새로 고치고 여전히 1개의 새로운 GET /posts 요청을 만들려는 경우 selectFromResult를 사용하여 데이터의 일부를 선택하여 수행할 수 있습니다.
2. Providing errors to the cache
캐시에 제공되는 정보는 성공적인 데이터 fetche에만 국한되지 않습니다. 이 개념은 특정 오류가 발생했을 때 실패한 캐시 데이터에 대한 특정 태그를 제공하기 위해 RTK 쿼리에 알리는 데 사용할 수 있습니다. 그런 다음 별도의 endpoint은 해당 태그의 데이터를 무효화하여 구성 요소가 여전히 실패한 데이터에 가입되어 있는 경우 이전에 실패한 endpoint을 다시 시도하도록 RTK 쿼리에 지시할 수 있습니다.
- postById에 대한 마지막 호출에서 승인되지 않은 오류가 발생했으며
- 구성 요소가 여전히 캐시된 데이터에 가입되어 있습니다.import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query'
const api = createApi({
baseQuery: fetchBaseQuery({ baseUrl: 'https://example.com' }),
tagTypes: ['Post', 'UNAUTHORIZED', 'UNKNOWN_ERROR'],
endpoints: (build) => ({
postById: build.query({
query: (id) => `post/${id}`,
providesTags: (result, error, id) =>
result
? [{ type: 'Post', id }]
: error?.status === 401
? ['UNAUTHORIZED']
: ['UNKNOWN_ERROR'],
}),
login: build.mutation({
query: () => '/login',
// on successful login, will refetch all currently
// 'UNAUTHORIZED' queries
invalidatesTags: (result) => (result ? ['UNAUTHORIZED'] : []),
}),
refetchErroredQueries: build.mutation({
queryFn: () => ({ data: null }),
invalidatesTags: ['UNKNOWN_ERROR'],
}),
}),
})
주어진 API 슬라이스에 대한 태그를 제공 및 무효화하기 위해 작성된 코드는 다음을 포함한 여러 요인에 따라 달라집니다.
API 슬라이스를 선언할 때 코드를 복제하는 것처럼 느껴질 수 있습니다. 예를 들어, 둘 다 특정 엔터티의 목록을 제공하는 두 개의 개별 끝점에 대해 제공하는 tagType에서만 제공하는 태그 선언이 다를 수 있습니다.
function providesList(resultsWithIds, tagType) {
return resultsWithIds
? [
{ type: tagType, id: 'LIST' },
...resultsWithIds.map(({ id }) => ({ type: tagType, id })),
]
: [{ type: tagType, id: 'LIST' }]
}
const api = createApi({
baseQuery: fetchBaseQuery({ baseUrl: 'https://example.com' }),
tagTypes: ['Post', 'User'],
endpoints: (build) => ({
getPosts: build.query({
query: () => `posts`,
providesTags: (result) => providesList(result, 'Post'),
}),
getUsers: build.query({
query: () => `users`,
providesTags: (result) => providesList(result, 'User'),
}),
}),
})
대부분의 경우 백엔드에서 변경을 트리거한 후 최신 데이터를 수신하기 위해 캐시 태그 무효화를 활용하여 자동화된 다시 가져오기를 수행할 수 있습니다.
이렇게 하면 데이터가 오래되게 되는 mutation이 발생했을 때 쿼리가 데이터를 다시 가져옵니다. 대부분의 경우 필요한 경우가 아니면 자동 다시 가져오기를 수동 캐시 업데이트보다 기본 설정으로 사용하는 것이 좋습니다.
그러나 경우에 따라 캐시를 수동으로 업데이트해야 할 수도 있습니다. query-endpoint에 대해 이미 존재하는 캐시 데이터를 업데이트하려는 경우 생성된 API의 util 개체에서 사용할 수 있는 updateQueryData 썽크 작업을 사용하여 업데이트할 수 있습니다.
store 인스턴스에 대한 dispatch 메소드에 액세스할 수 있는 모든 곳에서 해당 캐시 항목이 있는 경우 query-endpoint에 대한 캐시 데이터를 업데이트하기 위해 updateQueryData 호출 결과를 디스패치할 수 있습니다.
수동 캐시 업데이트의 사용 사례는 다음과 같습니다.
mutate가 트리거된 직후 캐시 데이터에 대한 업데이트를 수행하려는 경우 Optimistic 업데이트를 적용할 수 있습니다. 이것은 사용자에게 변경 요청이 아직 진행 중인 동안에도 변경 사항이 즉각적이라는 인상을 주고 싶을 때 유용한 패턴이 될 수 있습니다.
Optimistic 업데이트의 핵심 개념은 다음과 같습니다.
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query'
const api = createApi({
baseQuery: fetchBaseQuery({
baseUrl: '/',
}),
tagTypes: ['Post'],
endpoints: (build) => ({
getPost: build.query({
query: (id) => `post/${id}`,
providesTags: ['Post'],
}),
updatePost: build.mutation({
query: ({ id, ...patch }) => ({
url: `post/${id}`,
method: 'PATCH',
body: patch,
}),
async onQueryStarted({ id, ...patch }, {dispatch, queryFulfilled}) {
const patchResult = dispatch(
api.util.updateQueryData('getPost', id, (draft) => {
Object.assign(draft, patch)}))
try {
await queryFulfilled
} catch {
patchResult.undo()
/**
* Alternatively, on failure you can invalidate the corresponding cache tags
* to trigger a re-fetch:
* dispatch(api.util.invalidateTags(['Post']))
*/
}
},
}),
}),
})
mutatation이 발생한 후 서버로부터 받은 응답을 기반으로 캐시 데이터에 대한 업데이트를 수행하려는 경우 Pessimistic 업데이트를 적용할 수 있습니다. Pessimistic 업데이트와 Optimistic 업데이트의 차이점은 Pessimistic 업데이트가 캐시된 데이터를 업데이트하기 전에 서버의 응답을 대신 대기한다는 것입니다.
Pessimistic 업데이트의 핵심 개념은 다음과 같습니다.
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query'
const api = createApi({
baseQuery: fetchBaseQuery({
baseUrl: '/',
}),
tagTypes: ['Post'],
endpoints: (build) => ({
getPost: build.query({
query: (id) => `post/${id}`,
providesTags: ['Post'],
}),
updatePost: build.mutation({
query: ({ id, ...patch }) => ({
url: `post/${id}`,
method: 'PATCH',
body: patch,
}),
async onQueryStarted({ id, ...patch },{dispatch, queryFulfilled }) {
try {
const { data: updatedPost } = await queryFulfilled
const patchResult = dispatch(
api.util.updateQueryData('getPost', id, (draft) => {
Object.assign(draft, updatedPost)
})
)
} catch {}
},
}),
}),
})
애플리케이션의 다른 곳에서 캐시 데이터를 업데이트하려는 경우 useDispatch 후크를 통해 React 구성 요소 내를 포함하여 store.dispatch 메서드에 액세스할 수 있는 모든 곳에서 업데이트할 수 있습니다.
import { api } from './api'
import { useAppDispatch } from './store/hooks'
function App() {
const dispatch = useAppDispatch()
function handleClick() {
/**
* This will update the cache data for the query corresponding to the `getPosts` endpoint,
* when that endpoint is used with no argument (undefined).
*/
const patchCollection = dispatch(
api.util.updateQueryData('getPosts', undefined, (draftPosts) => {
draftPosts.push({ id: 1, name: 'Teddy' })
})
)
}
return <button onClick={handleClick}>Add post to cache</button>
}
쿼리 후크는 구성 요소가 마운트되자마자 자동으로 데이터 가져오기를 시작합니다. 그러나 어떤 조건이 참이 될 때까지 데이터 가져오기를 지연해야 하는 사용 사례가 있습니다. RTK 쿼리는 해당 동작을 활성화하기 위해 조건부 가져오기를 지원합니다.
쿼리가 자동으로 실행되지 않도록 하려면 후크에서 skip 매개변수를 사용할 수 있습니다.
const Pokemon = ({ name, skip }: { name: string; skip: boolean }) => {
const { data, error, status }
= useGetPokemonByNameQuery(name, {skip });
return (
<div>
{name} - {status}
</div>
);
};
skip이 true인 경우(또는 skipToken이 arg로 전달된 경우):
쿼리에 캐시된 데이터가 있는 경우:
쿼리에 캐시된 데이터가 없는 경우:
import { useGetPokemonByNameQuery } from './services/pokemon'
import type { PokemonName } from './pokemon.data'
export const Pokemon = ({ name }) => {
const [skip, setSkip] = React.useState(true)
const { data, error, isLoading, isUninitialized } = useGetPokemonByNameQuery( name, { skip })
const SkipToggle = () => (
<button onClick={() => setSkip((prev) => !prev)}>
Toggle Skip ({String(skip)})
</button>
)
return (
<>
{error ? (
<>Oh no, there was an error</>
) : isUninitialized ? (
<div>
{name} - Currently skipped - <SkipToggle />
</div>
) : isLoading ? (
<>loading...</>
) : data ? (
<>
<div>
<h3>{data.species.name}</h3>
<img src={data.sprites.front_shiny} alt={data.species.name} />
</div>
<SkipToggle />
</>
) : null}
</>
)
}
fetchBaseQuery를 사용할 때 query/mutation에서 오류가 발생하면 해당 후크의 error 속성에 반환됩니다. 이 경우 구성 요소가 다시 렌더링되며 원하는 경우 오류 데이터를 기반으로 적절한 UI를 표시할 수 있습니다.
function PostsList() {
const { data, error } = useGetPostsQuery()
return (
<div>
{error.status} {JSON.stringify(error.data)}
</div>
)
}
function AddPost() {
const [addPost, { error }] = useAddPostMutation()
return (
<div>
{error.status} {JSON.stringify(error.data)}
</div>
)
}
// 직접 실행하는 경우
addPost({ id: 1, name: 'Example' })
.unwrap()
.then((payload) => console.log('fulfilled', payload))
.catch((error) => console.error('rejected', error))
응답이 데이터로 반환되는지 또는 오류로 반환되는지는 제공된 baseQuery에 의해 결정됩니다.
궁극적으로 baseQuery와 함께 사용하려는 라이브러리를 선택할 수 있지만 올바른 응답 형식을 반환하는 것이 중요합니다. 아직 fetchBaseQuery를 시도하지 않았다면 기회를 주세요! 그렇지 않으면 반환된 오류를 변경하는 방법에 대한 정보는 쿼리 사용자 정의를 참조하십시오.
오류를 관리할 수 있는 몇 가지 방법이 있으며 어떤 경우에는 비동기 오류에 대한 일반 알림을 표시할 수 있습니다. RTK 쿼리는 Redux 및 Redux-Toolkit을 기반으로 하기 때문에 스토어에 미들웨어를 쉽게 추가할 수 있습니다.
import { isRejectedWithValue } from '@reduxjs/toolkit'
import { toast } from 'your-cool-library'
/**
* Log a warning and show a toast!
*/
export const rtkQueryErrorLogger = (api) => (next) => (action) => {
// RTK Query uses `createAsyncThunk` from redux-toolkit
// under the hood, so we're able to utilize these matchers!
if (isRejectedWithValue(action)) {
console.warn('We got a rejected action!')
toast.warn({ title: 'Async error!', message: action.error.data.message })
}
return next(action)
}
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
export const postApi = createApi({
reducerPath: 'postsApi',
baseQuery: fetchBaseQuery({ baseUrl: '/' }),
tagTypes: ['Posts'],
endpoints: (build) => ({
listPosts: build.query({
query: (page = 1) => `posts?page=${page}`,
providesTags: (result, error, page) =>
result
? [
// Provides a tag for each post in the current page,
// as well as the 'PARTIAL-LIST' tag.
...result.data.map(({ id }) => ({ type: 'Posts', id })),
{ type: 'Posts', id: 'PARTIAL-LIST' },
]
: [{ type: 'Posts', id: 'PARTIAL-LIST' }],
}),
deletePost: build.mutation({
query(id) {
return {
url: `post/${id}`,
method: 'DELETE',
}
},
// Invalidates the tag for this Post `id`, as well as the `PARTIAL-LIST` tag,
// causing the `listPosts` query to re-fetch if a component is subscribed to the query.
invalidatesTags: (result, error, id) => [
{ type: 'Posts', id },
{ type: 'Posts', id: 'PARTIAL-LIST' },
],
}),
}),
})
import * as React from 'react'
import {Badge, Box,Button,Divider,Flex,Heading,HStack,Icon,
List,ListIcon,ListItem,Spacer,Stat,StatLabel,
StatNumber} from '@chakra-ui/react'
import { MdArrowBack, MdArrowForward, MdBook } from 'react-icons/md'
import { Post, useListPostsQuery } from '../../app/services/posts'
const getColorForStatus = (status: Post['status']) => {
return status === 'draft'
? 'gray'
: status === 'pending_review'
? 'orange'
: 'green'
}
const PostList = () => {
const [page, setPage] = React.useState(1)
const { data: posts, isLoading, isFetching } = useListPostsQuery(page)
if (isLoading) {
return <div>Loading</div>
}
if (!posts?.data) {
return <div>No posts :(</div>
}
return (
<Box>
<HStack spacing="14px">
<Button
onClick={() => setPage((prev) => prev - 1)}
isLoading={isFetching}
disabled={page === 1}
>
<Icon as={MdArrowBack} />
</Button>
<Button
onClick={() => setPage((prev) => prev + 1)}
isLoading={isFetching}
disabled={page === posts.total_pages}
>
<Icon as={MdArrowForward} />
</Button>
<Box>{`${page} / ${posts.total_pages}`}</Box>
</HStack>
<List spacing={3} mt={6}>
{posts?.data.map(({ id, title, status }) => (
<ListItem key={id}>
<ListIcon as={MdBook} color="green.500" /> {title}{' '}
<Badge
ml="1"
fontSize="0.8em"
colorScheme={getColorForStatus(status)}
>
{status}
</Badge>
</ListItem>
))}
</List>
</Box>
)
}
export const PostsCountStat = () => {
const { data: posts } = useListPostsQuery()
return (
<Stat>
<StatLabel>Total Posts</StatLabel>
<StatNumber>{`${posts?.total || 'NA'}`}</StatNumber>
</Stat>
)
}
export const PostsManager = () => {
return (
<Box>
<Flex wrap="wrap" bg="#011627" p={4} color="white">
<Box>
<Heading size="xl">Manage Posts</Heading>
</Box>
<Spacer />
<Box>
<PostsCountStat />
</Box>
</Flex>
<Divider />
<Box p={4}>
<PostList />
</Box>
</Box>
)
}
export default PostsManager
Prefetching의 목표는 사용자가 페이지로 이동하거나 알려진 콘텐츠를 로드하려고 시도하기 전에 데이터를 가져오는 것입니다.
이 작업을 수행할 수 있는 몇 가지 상황이 있지만 몇 가지 매우 일반적인 사용 사례는 다음과 같습니다.
useMutation 후크와 유사하게 usePrefetch 후크는 자동으로 실행되지 않습니다. 동작을 시작하는 데 사용할 수 있는 "트리거 기능"을 반환합니다.
두 개의 인수를 허용합니다. 첫 번째는 API 서비스에서 정의한 쿼리 작업의 키이고 두 번째는 두 개의 선택적 매개변수의 객체입니다.
usePrefetch( endpointName, options)
후크를 선언할 때나 호출 사이트에서 이러한 Prefetch 옵션을 지정할 수 있습니다. 호출 사이트는 기본값보다 우선합니다.
트리거 함수는 항상 void를 반환합니다.
force: true가 선언 중이거나 호출 사이트에서 설정되면 쿼리는 상관없이 실행됩니다. 한 가지 예외는 동일한 쿼리가 이미 진행 중인 경우입니다.
옵션이 지정되지 않고 쿼리가 캐시에 있는 경우 쿼리가 수행되지 않습니다.
옵션이 지정되지 않고 쿼리가 캐시에 없으면 쿼리가 수행됩니다.
ifOlderThan이 지정되었지만 false로 평가되고 쿼리가 캐시에 있으면 쿼리가 수행되지 않습니다.
ifOlderThan이 지정되고 true로 평가되면 기존 캐시 항목이 있더라도 쿼리가 수행됩니다.
function User() {
const prefetchUser = usePrefetch('getUser')
// Low priority hover will not fire unless the last request happened more than 35s ago
// High priority hover will _always_ fire
return (
<div>
<button onMouseEnter={() => prefetchUser(4, { ifOlderThan: 35 })}>
Low priority
</button>
<button onMouseEnter={() => prefetchUser(4, { force: true })}>
High priority
</button>
</div>
)
}
export function usePrefetchImmediately(
endpoint, arg, options: PrefetchOptions = {}
) {
const dispatch = useAppDispatch()
useEffect(() => {
dispatch(api.util.prefetch(endpoint, arg, options))
}, [])
}
// In a component
usePrefetchImmediately('getUser', 5)
usePrefetch 후크를 사용하지 않는 경우 모든 프레임워크에서 동일한 동작을 직접 다시 만들 수 있습니다.
아래와 같이 Prefetching thunk를 디dispatch할 때 여기에 설명된 것과 동일한 정확한 동작을 볼 수 있습니다.
store.dispatch(
api.util.prefetch(endpointName, arg, { force: false, ifOlderThan: 10 })
)
쿼리 작업을 디스패치할 수도 있지만 추가 논리를 구현해야 합니다.
dispatch(api.endpoints[endpointName].initiate(arg, { forceRefetch: true }))
이것은 사용자가 다음 화살표 위로 마우스를 가져갈 때 미리 가져오는 방법을 보여주는 매우 기본적인 예입니다. 이것은 최적의 솔루션이 아닐 수 있습니다. 마우스를 가리키고 클릭한 다음 마우스를 움직이지 않고 페이지를 변경하면 다음 onMouseEnter 이벤트를 볼 수 없기 때문에 다음 페이지를 미리 가져오는지 알 수 없기 때문입니다. 이 경우 직접 처리해야 합니다. 다음 페이지를 자동으로 미리 가져오는 것을 고려할 수도 있습니다...
export const api = createApi({
baseQuery: fetchBaseQuery({ baseUrl: '/' }),
endpoints: (build) => ({
listPosts: build.query({
query: (page = 1) => `posts?page=${page}`,
}),
}),
})
export const { useListPostsQuery, usePrefetch } = api
//
import { useCallback, useState } from 'react'
import {Badge, Box,Button,Divider,Flex,Heading,HStack,Icon,
List,ListIcon,ListItem,Spacer,Stat,StatLabel,
StatNumber} from '@chakra-ui/react'
import { MdArrowBack, MdArrowForward, MdBook } from 'react-icons/md'
import { Post, useListPostsQuery } from '../../app/services/posts'
const getColorForStatus = (status: Post['status']) => {
return status === 'draft'
? 'gray'
: status === 'pending_review'
? 'orange'
: 'green'
}
const PostList = () => {
const [page, setPage] = useState(1)
const { data: posts, isLoading, isFetching } = useListPostsQuery(page)
const prefetchPage = usePrefetch('listPosts')
const prefetchNext = useCallback(() => {
prefetchPage(page + 1)
}, [prefetchPage, page])
if (isLoading) {
return <div>Loading</div>
}
if (!posts?.data) {
return <div>No posts :(</div>
}
return (
<Box>
<HStack spacing="14px">
<Button
onClick={() => setPage((prev) => prev - 1)}
isLoading={isFetching}
disabled={page === 1}
>
<Icon as={MdArrowBack} />
</Button>
<Button
onClick={() => setPage((prev) => prev + 1)}
isLoading={isFetching}
disabled={page === posts.total_pages}
onMouseEnter={prefetchNext}
>
<Icon as={MdArrowForward} />
</Button>
<Box>{`${page} / ${posts.total_pages}`}</Box>
</HStack>
<List spacing={3} mt={6}>
{posts?.data.map(({ id, title, status }) => (
<ListItem key={id}>
<ListIcon as={MdBook} color="green.500" /> {title}{' '}
<Badge
ml="1"
fontSize="0.8em"
colorScheme={getColorForStatus(status)}
>
{status}
</Badge>
</ListItem>
))}
</List>
</Box>
)
}
export const PostsCountStat = () => {
const { data: posts } = useListPostsQuery()
return (
<Stat>
<StatLabel>Total Posts</StatLabel>
<StatNumber>{`${posts?.total || 'NA'}`}</StatNumber>
</Stat>
)
}
export const PostsManager = () => {
return (
<Box>
<Flex wrap="wrap" bg="#011627" p={4} color="white">
<Box>
<Heading size="xl">Manage Posts</Heading>
</Box>
<Spacer />
<Box>
<PostsCountStat />
</Box>
</Flex>
<Divider />
<Box p={4}>
<PostList />
</Box>
</Box>
)
}
export default PostsManager
마지막 예제를 선택하여 네트워크 지연이 없는 것처럼 보이도록 다음 페이지를 자동으로 미리 가져옵니다.
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
export const postStatuses = ['draft', 'published', 'pending_review']
export const api = createApi({
baseQuery: fetchBaseQuery({ baseUrl: '/' }),
endpoints: (build) => ({
listPosts: build.query({
query: (page = 1) => `posts?page=${page}`,
}),
}),
})
export const { useListPostsQuery, usePrefetch } = api
//
import { useCallback, useState, useEffect } from 'react'
import { Badge, ... /위와 동일} from '@chakra-ui/react'
import { MdArrowBack, MdArrowForward, MdBook } from 'react-icons/md'
import { Post, usePrefetch, useListPostsQuery } from '../../app/services/posts'
const getColorForStatus = (status: Post['status']) => {
return status === 'draft'
? 'gray'
: status === 'pending_review'
? 'orange'
: 'green'
}
const PostList = () => {
const [page, setPage] = useState(1)
const { data: posts, isLoading, isFetching } = useListPostsQuery(page)
const prefetchPage = usePrefetch('listPosts')
const prefetchNext = useCallback(() => {
prefetchPage(page + 1)
}, [prefetchPage, page])
useEffect(() => {
if (page !== posts?.total_pages) {
prefetchNext()
}
}, [posts, page, prefetchNext])
// 생략
useQuery로 초기화된 첫 번째 쿼리가 실행된 후 나머지 페이지를 모두 자동으로 가져옵니다.
// 위와 같음
const PostList = () => {
const [page, setPage] = useState(1)
const [hasPrefetchedAll, setHasPrefetchedAll] = useState(false)
const { data: posts, isLoading, isFetching } = useListPostsQuery(page)
const prefetchPage = usePrefetch('listPosts')
const prefetchNext = useCallback(() => {
prefetchPage(page + 1)
}, [prefetchPage, page])
useEffect(() => {
if (!hasPrefetchedAll) {
if (posts && posts.total_pages > 1) {
;[...new Array(posts.total_pages)].forEach((page, index) => {
if (index >= posts.total_pages) return
prefetchPage(index + 1, { force: true })
})
setHasPrefetchedAll(true)
}
}
}, [posts, page, prefetchPage, hasPrefetchedAll])
//생략
폴링은 쿼리가 지정된 간격으로 실행되도록 하여 '실시간' 효과를 낼 수 있는 기능을 제공합니다. 쿼리에 대한 폴링을 활성화하려면 밀리초 단위의 간격으로 pollingInterval을 useQuery 후크 또는 action creator에게 전달합니다.
import * as React from 'react'
import { useGetPokemonByNameQuery } from './services/pokemon'
export const Pokemon = ({ name }: { name: string }) => {
// Automatically refetch every 3s
const { data, status, error, refetch } = useGetPokemonByNameQuery(name, {
pollingInterval: 3000,
})
return <div>{data}</div>
}
React Hooks가 없는 ction creator에서:
const { data, status, error, refetch } = store.dispatch(
endpoints.getCountById.initiate(id, {
subscriptionOptions: { pollingInterval: 3000 },
})
)
React Hooks의 편리함 없이 폴링을 사용하는 경우 약속 참조에서 수동으로 updateSubscriptionOptions를 호출하여 간격을 업데이트해야 합니다. 이 접근 방식은 프레임워크에 따라 다르지만 어디에서나 가능합니다.
queryRef.updateSubscriptionOptions({ pollingInterval: 0 })
// services/pokemon
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
import type { PokemonName } from '../pokemon.data'
export const pokemonApi = createApi({
reducerPath: 'pokemonApi',
baseQuery: fetchBaseQuery({ baseUrl: 'https://pokeapi.co/api/v2/' }),
endpoints: (builder) => ({
getPokemonByName: builder.query({
query: (name: PokemonName) => `pokemon/${name}`,
}),
}),
})
// Export hooks for usage in functional components
export const { useGetPokemonByNameQuery } = pokemonApi
// Pokemon.js
import * as React from 'react'
import { useGetPokemonByNameQuery } from './services/pokemon'
const intervalOptions = [
{ label: 'Off', value: 0 },
{ label: '3s', value: 3000 },
{ label: '5s', value: 5000 },
{ label: '10s', value: 10000 },
{ label: '1m', value: 60000 },
]
const getRandomIntervalValue = () =>
intervalOptions[Math.floor(Math.random() * intervalOptions.length)].value
export const Pokemon = ({ name }) => {
const [pollingInterval, setPollingInterval] = React.useState(
getRandomIntervalValue()
)
const {
data,error,isLoading,isFetching,refetch} =
useGetPokemonByNameQuery(name, { pollingInterval})
return (
<div
style={{
float: 'left',
textAlign: 'center',
...(isFetching ? { background: '#e6ffe8' } : {}),
}}
>
{error ? (
<>Oh no, there was an error loading {name}</>
) : isLoading ? (
<>Loading...</>
) : data ? (
<>
<h3>{data.species.name}</h3>
<div style={{ minWidth: 96, minHeight: 96 }}>
<img
src={data.sprites.front_shiny}
alt={data.species.name}
style={{ ...(isFetching ? { opacity: 0.3 } : {}) }}
/>
</div>
<div>
<label style={{ display: 'block' }}>Polling interval</label>
<select
value={pollingInterval}
onChange={({ target: { value } }) =>
setPollingInterval(Number(value))
}
>
{intervalOptions.map(({ label, value }) => (
<option key={value} value={value}>
{label}
</option>
))}
</select>
</div>
<div>
<button onClick={refetch} disabled={isFetching}>
{isFetching ? 'Loading' : 'Manually refetch'}
</button>
</div>
</>
) : (
'No Data'
)}
</div>
)
}
//App.js
import { Pokemon } from './Pokemon'
import { PokemonName, POKEMON_NAMES } from './pokemon.data'
import './styles.css'
const getRandomPokemonName = () =>
POKEMON_NAMES[Math.floor(Math.random() * POKEMON_NAMES.length)]
export default function App() {
const [pokemon, setPokemon] = React.useState<PokemonName[]>(['bulbasaur'])
return (
<div className="App">
<div>
<button
onClick={() =>
setPokemon((prev) => [...prev, getRandomPokemonName()])
}
>
Add random pokemon
</button>
<button onClick={() => setPokemon((prev) => [...prev, 'bulbasaur'])}>
Add bulbasaur
</button>
</div>
{pokemon.map((name, index) => (
<Pokemon key={index} name={name} />
))}
</div>
)
}
RTK 쿼리는 영구 쿼리에 대한 스트리밍 업데이트를 수신할 수 있는 기능을 제공합니다. 이를 통해 쿼리는 서버에 대한 지속적인 연결(일반적으로 WebSocket 사용)을 설정하고 서버에서 추가 정보를 수신할 때 캐시된 데이터에 업데이트를 적용할 수 있습니다.
스트리밍 업데이트를 사용하여 API가 생성되는 새 항목 또는 업데이트되는 중요한 속성과 같은 백엔드 데이터에 대한 실시간 업데이트를 수신할 수 있습니다.
쿼리에 대한 스트리밍 업데이트를 활성화하려면 스트리밍된 데이터가 수신될 때 쿼리를 업데이트하는 방법에 대한 논리를 포함하여 비동기 onCacheEntryAdded 함수를 쿼리에 전달합니다. 자세한 내용은 onCacheEntryAdded API 참조를 참조하세요.
기본적으로 쿼리 데이터에 대한 업데이트는 query/mutaiton과 관련된 태그를 기반으로 데이터를 무효화하기 위해 캐시 무효화를 사용하거나 데이터를 사용하는 구성 요소가 마운트될 때 새로운 데이터를 가져오기 위해 refetchOnMountOrArgChange를 사용하여 간격에 따라 간헐적으로 폴링을 통해 수행해야 합니다.
그러나 스트리밍 업데이트는 다음과 관련된 시나리오에 특히 유용합니다.
스트리밍 업데이트의 이점을 얻을 수 있는 사용 사례의 예는 다음과 같습니다.
onCacheEntryAdded 수명 주기 콜백을 사용하면 새 캐시 항목이 RTK 쿼리 캐시에 추가된 후(즉, 구성 요소가 지정된 endpoint+매개변수 조합에 대한 새 sbusciption을 생성한 후) 실행될 임의의 비동기 논리를 작성할 수 있습니다.
onCacheEntryAdded는 sbusciption에 전달된 인수와 "수명 주기 약속" 및 유틸리티 기능을 포함하는 옵션 개체의 두 가지 인수로 호출됩니다. 이를 사용하여 데이터 추가를 기다리고, 서버 연결을 시작하고, 부분 업데이트를 적용하고, 쿼리 subscription이 제거될 때 연결을 정리하는 순차 논리를 작성할 수 있습니다.
일반적으로 첫 번째 데이터를 가져온 시기를 결정하기 위해 cacheDataLoaded를 기다린 다음 updateCacheData 유틸리티를 사용하여 메시지가 수신될 때 스트리밍 업데이트를 적용합니다. updateCacheData는 현재 캐시 값의 초안을 수신하는 Immer 기반 콜백입니다. 초안 값을 "변경"하여 수신된 값을 기반으로 필요에 따라 업데이트할 수 있습니다. 그런 다음 RTK 쿼리는 이러한 변경 사항을 기반으로 diffed 패치를 적용하는 작업을 전달합니다.
마지막으로 cacheEntryRemoved가 언제 서버 연결을 정리할지 알 때까지 기다릴 수 있습니다.
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
import { isMessage } from './schemaValidators'
export const api = createApi({
baseQuery: fetchBaseQuery({ baseUrl: '/' }),
endpoints: (build) => ({
getMessages: build.query({
query: (channel) => `messages/${channel}`,
async onCacheEntryAdded(
arg,
{ updateCachedData, cacheDataLoaded, cacheEntryRemoved }
) {
// create a websocket connection when the cache subscription starts
const ws = new WebSocket('ws://localhost:8080')
try {
// wait for the initial query to resolve before proceeding
await cacheDataLoaded
// when data is received from the socket connection to the server,
// if it is a message and for the appropriate channel,
// update our query result with the received message
const listener = (event) => {
const data = JSON.parse(event.data)
if (!isMessage(data) || data.channel !== arg) return
updateCachedData((draft) => {
draft.push(data)
})
}
ws.addEventListener('message', listener)
} catch {
// no-op in case `cacheEntryRemoved` resolves before `cacheDataLoaded`,
// in which case `cacheDataLoaded` will throw
}
// cacheEntryRemoved will resolve when the cache subscription is no longer active
await cacheEntryRemoved
// perform cleanup steps once the `cacheEntryRemoved` promise resolves
ws.close()
},
}),
}),
})
export const { useGetMessagesQuery } = api
getMessages 쿼리가 트리거되면(예: useGetMessagesQuery() 후크로 마운트하는 구성 요소를 통해) 엔드포인트에 대해 직렬화된 인수를 기반으로 캐시 항목이 추가됩니다. 캐시의 초기 데이터를 가져오기 위해 쿼리 속성을 기반으로 연결된 쿼리가 시작됩니다. 한편 비동기 onCacheEntryAdded 콜백이 시작되고 새 WebSocket 연결이 생성됩니다. 초기 쿼리에 대한 응답이 수신되면 캐시가 응답 데이터로 채워지고 cacheDataLoaded 약속이 해결됩니다. cacheDataLoaded 약속을 기다린 후 메시지 이벤트 리스너가 WebSocket 연결에 추가되어 연결된 메시지가 수신될 때 캐시 데이터를 업데이트합니다.
데이터에 대한 활성 subscription이 더 이상 없을 때(예: subscrived 구성 요소가 충분한 시간 동안 마운트 해제된 상태로 유지되는 경우) cacheEntryRemoved 약속이 해결되어 나머지 코드가 실행되고 웹 소켓 연결을 닫을 수 있습니다. RTK 쿼리는 캐시에서 관련 데이터도 제거합니다.
해당 캐시 항목에 대한 쿼리가 나중에 실행되면 전체 캐시 항목을 덮어쓰고 스트리밍 업데이트 수신기는 업데이트된 데이터에서 계속 작업합니다.
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
import { createEntityAdapter } from '@reduxjs/toolkit'
import { isMessage } from './schemaValidators'
const messagesAdapter = createEntityAdapter()
export const api = createApi({
baseQuery: fetchBaseQuery({ baseUrl: '/' }),
endpoints: (build) => ({
getMessages: build.query({
query: (channel) => `messages/${channel}`,
transformResponse(response) {
return messagesAdapter.addMany(
messagesAdapter.getInitialState(),
response
)
},
async onCacheEntryAdded(
arg,
{ updateCachedData, cacheDataLoaded, cacheEntryRemoved }
) {
const ws = new WebSocket('ws://localhost:8080')
try {
await cacheDataLoaded
const listener = (event) => {
const data = JSON.parse(event.data)
if (!isMessage(data) || data.channel !== arg) return
updateCachedData((draft) => {
messagesAdapter.upsertOne(draft, data)
})
}
ws.addEventListener('message', listener)
} catch {}
await cacheEntryRemoved
ws.close()
},
}),
}),
})
export const { useGetMessagesQuery } = api
이 예는 캐시에 데이터를 추가할 때 응답 형태를 변환할 수 있도록 이전 nomalizing 방법을 보여줍니다.
명심해야 할 핵심 사항은 onCacheEntryAdded 콜백 내에서 캐시된 데이터에 대한 업데이트가 캐시된 데이터에 표시될 변환된 데이터 형태를 존중해야 한다는 것입니다. 이 예는 초기 transformResponse에 대해 createEntityAdapter를 사용할 수 있는 방법과 정규화된 상태 구조를 유지하면서 수신된 항목을 캐시된 데이터에 upsert하기 위해 스트리밍된 업데이트가 수신되는 경우 다시 보여줍니다.
RTK 쿼리를 사용하면 초기 서비스 정의를 설정한 후 추가 endpoint를 삽입할 수 있으므로 초기 번들 크기를 줄일 수 있습니다. 이는 endpoint가 많을 수 있는 대규모 애플리케이션에 매우 유용할 수 있습니다.
injectionEndpoints는 endpoint 컬렉션과 선택적인 overrideExisting 매개변수를 허용합니다.
injectionEndpoints를 호출하면 endpoint을 원래 API에 주입하지만 이러한 endpoint에 대해 올바른 유형이 있는 동일한 API도 제공합니다. ( 원래 정의의 유형을 수정할 수 없습니다.)
일반적인 접근 방식은 하나의 빈 중앙 API 슬라이스 정의를 갖는 것입니다.
// Or from '@reduxjs/toolkit/query' if not using the auto-generated hooks
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
// initialize an empty api service that we'll inject endpoints into later as needed
export const emptySplitApi = createApi({
baseQuery: fetchBaseQuery({ baseUrl: '/' }),
endpoints: () => ({}),
})
그런 다음 다른 파일에 api endpoint을 주입하고 거기에서 내보내십시오. 그렇게 하면 확실히 주입되는 방식으로 endpoint을 항상 가져올 수 있습니다.
import { emptySplitApi } from './emptySplitApi'
const extendedApi = emptySplitApi.injectEndpoints({
endpoints: (build) => ({
example: build.query({
query: () => 'test',
}),
}),
overrideExisting: false,
})
export const { useExampleQuery } = extendedApi
RTK 쿼리의 API 및 아키텍처는 API 끝점을 미리 선언하는 데 중점을 둡니다. 이는 OpenAPI 및 GraphQL과 같은 외부 API 스키마 정의에서 API 슬라이스 정의를 자동으로 생성하는 데 적합합니다.
별도의 도구로 사용할 수 있는 코드 생성 기능의 초기 미리 보기가 있습니다.
https://www.graphql-code-generator.com/plugins/typescript-rtk-query
OpenAPI 스키마에서 RTK 쿼리 코드 생성을 위한 패키지를 제공합니다. @rtk-query/codegen-openapi로 게시되며 packages/rtk-query-codegen-openapi에서 소스 코드를 찾을 수 있습니다.
Create an empty api using createApi like
// src/store/emptyApi.ts
// Or from '@reduxjs/toolkit/query' if not using the auto-generated hooks
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
// initialize an empty api service that we'll inject endpoints into later as needed
export const emptySplitApi = createApi({
baseQuery: fetchBaseQuery({ baseUrl: '/' }),
endpoints: () => ({}),
})
다음과 같은 내용으로 구성 파일(json, js 또는 ts)을 생성합니다.
//openapi-config.ts
import type { ConfigFile } from '@rtk-query/codegen-openapi'
const config: ConfigFile = {
schemaFile: 'https://petstore3.swagger.io/api/v3/openapi.json',
apiFile: './src/store/emptyApi.ts',
apiImport: 'emptySplitApi',
outputFile: './src/store/petApi.ts',
exportName: 'petApi',
hooks: true,
}
export default config
그런 다음 코드 생성기를 호출합니다.
npx @rtk-query/codegen-openapi openapi-config.ts
// src/store/petApi.ts
import { generateEndpoints } from '@rtk-query/codegen-openapi'
const api = await generateEndpoints({
apiFile: './fixtures/emptyApi.ts',
schemaFile: resolve(__dirname, 'fixtures/petstore.json'),
filterEndpoints: ['getPetById', 'addPet'],
hooks: true,
})
interface SimpleUsage {
apiFile: string
schemaFile: string
apiImport?: string
exportName?: string
argSuffix?: string
responseSuffix?: string
hooks?: boolean
outputFile: string
filterEndpoints?:
| string
| RegExp
| EndpointMatcherFunction
| Array<string | RegExp | EndpointMatcherFunction>
endpointOverrides?: EndpointOverrides[]
}
export type EndpointMatcherFunction = (
operationName: string,
operationDefinition: OperationDefinition
) => boolean
몇 개의 endpoint만 포함하려는 경우 filterEndpoints 구성 옵션을 사용하여 endpoint을 필터링할 수 있습니다.
// openapi-config.ts
const filteredConfig: ConfigFile = {
// ...
// should only have endpoints loginUser, placeOrder, getOrderById, deleteOrder
filterEndpoints: ['loginUser', /Order/],
}
endpoint가 query 대신 mutation으로 생성되거나 다른 방식으로 생성되는 경우 이를 재정의할 수 있습니다.
// openapi-config.ts
const withOverride: ConfigFile = {
// ...
endpointOverrides: [
{
pattern: 'loginUser',
type: 'mutation',
},
],
}
const config: ConfigFile = {
schemaFile: 'https://petstore3.swagger.io/api/v3/openapi.json',
apiFile: './src/store/emptyApi.ts',
outputFiles: {
'./src/store/user.ts': {
filterEndpoints: [/user/i],
},
'./src/store/order.ts': {
filterEndpoints: [/order/i],
},
'./src/store/pet.ts': {
filterEndpoints: [/pet/i],
},
},
}
RTK 쿼리는 next-redux-wrapper와 함께 rehydration를 통해 Next.js를 사용하여 SSR(Server Side Rendering)을 지원합니다.
hydration or rehydration 는 클라이언트 측 JavaScript가 정적 호스팅 또는 서버 측 렌더링을 통해 전달되는 정적 HTML 웹 페이지를 HTML 요소에 이벤트 핸들러를 첨부하여 동적 웹 페이지로 변환하는 기술입니다. HTML은 서버에서 미리 렌더링되기 때문에 "첫 번째 콘텐츠가 포함된 페인트"(유용한 데이터가 사용자에게 처음으로 표시될 때)를 빠르게 할 수 있지만 그 이후에는 페이지가 완전히 로드되고 대화형이지만 클라이언트 측 JavaScript가 실행되고 이벤트 핸들러가 연결될 때까지는 아닙니다.
워크플로는 다음과 같습니다.
1. next-redux-wrapper 설정
2. getStaticProps 또는 getServerSideProps에서:
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
import { HYDRATE } from 'next-redux-wrapper'
export const api = createApi({
baseQuery: fetchBaseQuery({ baseUrl: '/' }),
extractRehydrationInfo(action, { reducerPath }) {
if (action.type === HYDRATE) {
return action.payload[reducerPath]
}
},
endpoints: (build) => ({
// omitted
}),
})
next.js를 사용하는 예제 저장소는 여기에서 사용할 수 있습니다.
https://github.com/phryneas/ssr-experiments/tree/main/nextjs-blog
메모리 누수가 예상되지는 않지만 렌더가 클라이언트로 전송되고 저장소가 메모리에서 제거되면 store.dispatch(api.util.resetApiState())를 호출하여 불량 타이머가 동작이 남지 않도록 할 수도 있습니다.
SSG(정적 사이트 생성)에서 오래된 데이터를 제공하지 않으려면 액세스할 때 데이터를 다시 가져올 수 있도록 refetchOnMountOrArgChange를 900(초)과 같은 합리적인 값으로 설정할 수 있습니다.
next.js를 사용하지 않고 위의 예를 SSR 프레임워크에 적용할 수 없는 경우 unstable__ 마크 접근 방식을 사용하여 렌더링 중에 비동기 코드를 실행해야 하고 효과에서는 안전하지 않은 SSR 시나리오를 지원할 수 있습니다. 이것은 Apollo와 함께 getDataFromTree를 사용하는 것과 유사한 접근 방식입니다.
워크플로는 다음과 같습니다.
1. 렌더링 중에 비동기 작업을 수행하는 createApi 버전을 만듭니다.
import {
buildCreateApi,
coreModule,
reactHooksModule,
} from '@reduxjs/toolkit/query/react'
const createApi = buildCreateApi(
coreModule(),
reactHooksModule({ unstable__sideEffectsInRender: true })
)
RTK 쿼리는 createApi의 extractRehydrationInfo 옵션을 통해 rehydration를 지원합니다. 이 함수는 디스패치된 모든 작업에 전달되며 정의되지 않은 값이 아닌 다른 값을 반환하는 경우 해당 값은 이행 및 오류 쿼리에 대한 API 상태를 rehydration하는 데 사용됩니다.
일반적으로 API 슬라이스를 유지하는 것은 권장되지 않으며 대신 캐시 동작을 정의하기 위해 브라우저에서 Cache-Control Header와 같은 메커니즘을 사용해야 합니다. 사용자가 일정 시간 동안 페이지를 방문하지 않은 경우 api 슬라이스를 유지하고 rehydration하면 항상 사용자에게 매우 오래된 데이터를 남길 수 있습니다. 그럼에도 불구하고 이를 처리할 브라우저 캐시가 없는 기본 앱과 같은 환경에서는 지속성이 여전히 실행 가능한 옵션일 수 있습니다.
API 상태 복원은 redux-persist에서 가져온 REHYDRATE 작업 유형을 활용하여 Redux Persist와 함께 사용할 수 있습니다. 이것은 루트 리듀서를 유지할 때 autoMergeLevel1 또는 autoMergeLevel2 상태 조정자와 함께 사용하거나 api 리듀서를 유지할 때 autoMergeLevel1 조정자와 함께 즉시 사용할 수 있습니다.
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
import { REHYDRATE } from 'redux-persist'
export const api = createApi({
baseQuery: fetchBaseQuery({ baseUrl: '/' }),
extractRehydrationInfo(action, { reducerPath }) {
if (action.type === REHYDRATE) {
return action.payload[reducerPath]
}
},
endpoints: (build) => ({
// omitted
}),
})
현재 RTK 쿼리에는 createApi의 두 가지 변형이 포함되어 있습니다.
모듈에 대해 기본이 아닌 옵션을 지정하거나 고유한 모듈을 추가하여 고유한 버전의 createApi를 작성할 수 있습니다.
사용자 정의 컨텍스트를 사용하는 경우와 같이 후크가 다른 버전의 useSelector 또는 useDispatch를 사용하도록 하려면 모듈 생성 시 다음을 전달할 수 있습니다.
import * as React from 'react'
import { createDispatchHook } from 'react-redux'
import {
buildCreateApi,
coreModule,
reactHooksModule,
} from '@reduxjs/toolkit/query/react'
const MyContext = React.createContext(null)
const customCreateApi = buildCreateApi(
coreModule(),
reactHooksModule({ useDispatch: createDispatchHook(MyContext) })
)
자신만의 모듈을 만들고 싶다면 react-hooks 모듈을 검토하여 구현이 어떻게 생겼는지 확인해야 합니다.
다음은 매우 간소화된 버전입니다.
import { CoreModule } from '@internal/core/module'
import {
BaseQueryFn,
EndpointDefinitions,
Api,
Module,
buildCreateApi,
coreModule,
} from '@reduxjs/toolkit/query'
export const customModuleName = Symbol()
export type CustomModule = typeof customModuleName
declare module '../apiTypes' {
export interface ApiModules<
BaseQuery extends BaseQueryFn,
Definitions extends EndpointDefinitions,
ReducerPath extends string,
TagTypes extends string
> {
[customModuleName]: {
endpoints: {
[K in keyof Definitions]: {
myEndpointProperty: string
}
}
}
}
}
export const myModule = (): Module<CustomModule> => ({
name: customModuleName,
init(api, options, context) {
// initialize stuff here if you need to
return {
injectEndpoint(endpoint, definition) {
const anyApi = (api as any) as Api<
any,
Record<string, any>,
string,
string,
CustomModule | CoreModule
>
anyApi.endpoints[endpoint].myEndpointProperty = 'test'
},
}
},
})
export const myCreateApi = buildCreateApi(coreModule(), myModule())
RTK 쿼리는 요청이 해결되는 방식에 대해 불가지론적입니다. 요청을 처리하기 위해 원하는 라이브러리를 사용하거나 전혀 라이브러리를 사용하지 않을 수 있습니다. RTK 쿼리는 대부분의 사용 사례를 포함할 것으로 예상되는 합리적인 기본값을 제공하는 동시에 특정 요구에 맞게 쿼리 처리를 변경할 수 있는 사용자 지정 공간도 허용합니다.
기존의 BaseQuery를 Default함수인 fetchBaseQuery를 사용자 정의로 대체할 수 있다.
const customBaseQuery = (
args,
//The return value of the query function for a given endpoint
// baseQuery의 baseUrl의 input value와
// baseQuery: fetchBaseQuery({ baseUrl: '/'}),
//endpoits 안의 query, mutation의 input value
//query: build.query({
// query: () => ({ url: '/query', method: 'get' })
// }),
{ signal,
// An AbortSignal object that may be used to abort DOM requests and/or read whether the request is aborted.
dispatch,
//The store.dispatch method for the corresponding Redux store
getState,
// A function that may be called to access the current store state
extra
//Provided as thunk. extraArgument to the configureStore getDefaultMiddleware option
},
extraOptions
//The value of the optional extraOptions property provided for a given endpoint
) => {
if ( 사용자정의 condition) {
return { data: YourData }
} else {
return{ error: { status: number, data: YourErrorData } }
}
}
import { createApi } from '@reduxjs/toolkit/query'
import axios from 'axios'
const axiosBaseQuery = ({ baseUrl } = { baseUrl: '' }) =>
async ({ url, method, data, params }) => {
try {
const result = await axios({ url: baseUrl + url, method, data, params })
return { data: result.data }
} catch (axiosError) {
let err = axiosError
return {
error: {
status: err.response?.status,
data: err.response?.data || err.message,
},
}
}
}
const api = createApi({
baseQuery: axiosBaseQuery({
baseUrl: 'https://example.com',
}),
endpoints(build) {
return {
query: build.query({
query: () => ({
url: '/query',
method: 'get'
})
}),
mutation: build.mutation({
query: () => ({
url: '/mutation',
method: 'post'
}),
}),
}
},
})
import { createApi } from '@reduxjs/toolkit/query'
import { request, gql, ClientError } from 'graphql-request'
const graphqlBaseQuery =
({ baseUrl }) =>
async ({ body }) => {
try {
const result = await request(baseUrl, body)
return { data: result }
} catch (error) {
if (error instanceof ClientError) {
return { error: { status: error.response.status, data: error } }
}
return { error: { status: 500, data: error } }
}
}
export const api = createApi({
baseQuery: graphqlBaseQuery({
baseUrl: 'https://graphqlzero.almansi.me/api',
}),
endpoints: (builder) => ({
getPosts: builder.query({
query: () => ({
body: gql`
query {
posts {
data {
id
title
}
}
}
`,
}),
transformResponse: (response) => response.posts.data,
}),
getPost: builder.query({
query: (id) => ({
body: gql`
query {
post(id: ${id}) {
id
title
body
}
}
`,
}),
transformResponse: (response) => response.post,
}),
}),
})
이 예는 401 Unauthorized 오류가 발생하면 추가 요청이 전송되어 인증 토큰을 새로 고치고 재인증 후 초기 쿼리를 다시 시도하도록 fetchBaseQuery를 래핑합니다.
import { fetchBaseQuery } from '@reduxjs/toolkit/query'
import { tokenReceived, loggedOut } from './authSlice'
const baseQuery = fetchBaseQuery({ baseUrl: '/' })
const baseQueryWithReauth = async (args, api, extraOptions) => {
let result = await baseQuery(args, api, extraOptions)
if (result.error && result.error.status === 401) {
// try to get a new token
const refreshResult = await baseQuery('/refreshToken', api, extraOptions)
if (refreshResult.data) {
// store the new token
api.dispatch(tokenReceived(refreshResult.data))
// retry the initial query
result = await baseQuery(args, api, extraOptions)
} else {
api.dispatch(loggedOut())
}
}
return result
}
401 Unauthorized 오류로 호출이 실패할 때 async-mutex를 사용하여 '/refreshToken'에 대한 반복적인 호출을 방지합니다.
import { fetchBaseQuery } from '@reduxjs/toolkit/query'
import { tokenReceived, loggedOut } from './authSlice'
> import { Mutex } from 'async-mutex'
// create a new mutex
> const mutex = new Mutex()
const baseQuery = fetchBaseQuery({ baseUrl: '/' })
const baseQueryWithReauth = async (args, api, extraOptions) => {
// wait until the mutex is available without locking it
> await mutex.waitForUnlock()
let result = await baseQuery(args, api, extraOptions)
if (result.error && result.error.status === 401) {
// checking whether the mutex is locked
> if (!mutex.isLocked()) {
> const release = await mutex.acquire()
try {
const refreshResult = await baseQuery(
'/refreshToken',
api,
extraOptions
)
if (refreshResult.data) {
api.dispatch(tokenReceived(refreshResult.data))
// retry the initial query
result = await baseQuery(args, api, extraOptions)
} else {
api.dispatch(loggedOut())
}
} finally {
// release must be called once the mutex should be released again.
> release()
}
} else {
// wait until the mutex is available without locking it
> await mutex.waitForUnlock()
result = await baseQuery(args, api, extraOptions)
}
}
return result
}
RTK 쿼리는 API 정의에서 baseQuery를 래핑할 수 있는 retry라는 유틸리티를 내보냅니다. basic exponential backoff를 사용하여 기본적으로 5회 시도합니다.
600ms random(0.4, 1.4)
1200ms random(0.4, 1.4)
2400ms random(0.4, 1.4)
4800ms random(0.4, 1.4)
9600ms * random(0.4, 1.4)
import { createApi, fetchBaseQuery, retry } from '@reduxjs/toolkit/query/react'
// maxRetries: 5 is the default, and can be omitted. Shown for documentation purposes.
const staggeredBaseQuery = retry(fetchBaseQuery({ baseUrl: '/' }), {
maxRetries: 5,
})
export const api = createApi({
baseQuery: staggeredBaseQuery,
endpoints: (build) => ({
getPosts: build.query({
query: () => ({ url: 'posts' }),
}),
getPost: build.query({
query: (id) => ({ url: `post/${id}` }),
> extraOptions: { maxRetries: 8 }, // You can override the retry behavior on each endpoint
}),
}),
})
export const { useGetPostsQuery, useGetPostQuery } = api
재시도 유틸리티에는 재시도를 즉시 중단하는 데 사용할 수 있는 fail 메서드 속성이 연결되어 있습니다. 이는 추가 재시도가 모두 실패하고 중복되는 것으로 알려진 상황에 사용할 수 있습니다.
import { createApi, fetchBaseQuery, retry } from '@reduxjs/toolkit/query/react'
const staggeredBaseQueryWithBailOut = retry(
async (args, api, extraOptions) => {
const result = await fetchBaseQuery({ baseUrl: '/api/' }) (args, api, extraOptions)
// unauthorized인 경우 즉시 중단.
if (result.error?.status === 401) {
retry.fail(result.error)
}
return result
},
{ maxRetries: 5 }
)
export const api = createApi({
baseQuery: staggeredBaseQueryWithBailOut,
endpoints: (build) => ({
getPosts: build.query({
query: () => ({ url: 'posts' }),
}),
getPost: build.query({
query: (id) => ({ url: `post/${id}` }),
extraOptions: { maxRetries: 8 }, // 회수 재정의
}),
}),
})
export const { useGetPostsQuery, useGetPostQuery } = api
baseQuery는 반환 값에 메타 속성을 포함할 수도 있습니다. 이는 요청 ID 또는 타임스탬프와 같은 요청과 관련된 추가 정보를 포함하려는 경우에 유용할 수 있습니다.
import { fetchBaseQuery, createApi } from '@reduxjs/toolkit/query'
import { uuid } from './idGenerator'
const metaBaseQuery = async (args, api, extraOptions) => {
const requestId = uuid()
const timestamp = Date.now()
const baseResult = await fetchBaseQuery({ baseUrl: '/' })( args, api, extraOptions )
return {
...baseResult,
> meta: baseResult.meta && { ...baseResult.meta, requestId, timestamp },
}
}
const DAY_MS = 24 * 60 * 60 * 1000
const api = createApi({
baseQuery: metaBaseQuery,
endpoints: (build) => ({
// 특정 날짜 이후에 요청이 수행된 경우에만 데이터를 반환하려는 theoretical endpoint
getRecentPosts: build.query({
query: () => 'posts',
transformResponse: (returnValue, meta) => {
// 여기의 `meta`에는 추가된 `requestId` 및 `timestamp`와 fetchBaseQuery의 메타 개체에서 가져온 `request` 및 `response`가 포함됩니다. 이러한 속성을 사용하여 원하는 대로 응답을 변환할 수 있습니다.
if (!meta) return []
return returnValue.filter(
(post) => post.timestamp >= meta.timestamp - DAY_MS
)
},
}),
}),
})
어떤 경우에는 Redux state의 속성에 따라 결정된 동적으로 변경된 기본 URL을 원할 수 있습니다. baseQuery는 호출 당시의 현재 store 상태를 제공하는 getState 메서드에 액세스할 수 있습니다. 이것은 부분 URL 문자열을 사용하여 원하는 URL을 구성하는 데 사용할 수 있으며 store state의 적절한 데이터를 사용할 수 있습니다.
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
import { selectProjectId } from './projectSlice'
const rawBaseQuery = fetchBaseQuery({
baseUrl: 'www.my-cool-site.com/',
})
const dynamicBaseQuery = async (args, api, extraOptions) => {
const projectId = selectProjectId(api.getState())
// gracefully handle scenarios where data to generate the URL is missing
if (!projectId) {
return {
error: {
status: 400,
statusText: 'Bad Request',
data: 'No project ID received',
},
}
}
const urlEnd = typeof args === 'string' ? args : args.url
// construct a dynamically generated portion of the url
const adjustedUrl = `project/${projectId}/${urlEnd}`
const adjustedArgs =
typeof args === 'string' ? adjustedUrl : { ...args, url: adjustedUrl }
// provide the amended url and other params to the raw base query
return rawBaseQuery(adjustedArgs, api, extraOptions)
}
export const api = createApi({
baseQuery: dynamicBaseQuery,
endpoints: (builder) => ({
getPosts: builder.query({
query: () => 'posts',
}),
}),
})
export const { useGetPostsQuery } = api
/*
Using `useGetPostsQuery()` where a `projectId` of 500 is in the redux state will result in
a request being sent to www.my-cool-site.com/project/500/posts
*/
query나 mutation에서 data를 받아 Cache에 넣기 전에 조작할 수 있다.
function defaultTransformResponse(baseQueryReturnValue, meta, arg) {
return baseQueryReturnValue
}
hook이나 selector(useselector) 사용하기 편하도록 data로 바로 접근할 수 있다
transformResponse: (response, meta, arg) =>
response.some.deeply.nested.collection
baseQuery는 반환 값에 메타 속성을 포함할 수도 있습니다. 이는 요청 ID 또는 타임스탬프와 같은 요청과 관련된 추가 정보를 포함하려는 경우에 유용할 수 있습니다.
transformResponse: (response: { sideA, sideB }, meta, arg) => {
if (meta?.coinFlip === 'heads') {
return response.sideA
}
return response.sideB
}
transformResponse: (response: Posts, meta, arg) => {
return {
originalArg: arg,
data: response,
}
}
필요한 경우 data를 nomalizing 할 수 있고, nomalizing은 createEntityAdapter 사용하고, createEntityAdapter안에서 sortComparer를 사용하여 정렬할 수도 있다.
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
import { createEntityAdapter } from '@reduxjs/toolkit'
const postsAdapter = createEntityAdapter({
sortComparer: (a, b) => a.name.localeCompare(b.name),
})
export const api = createApi({
baseQuery: fetchBaseQuery({ baseUrl: '/' }),
endpoints: (build) => ({
getPosts: build.query({
query: () => `posts`,
transformResponse(response) {
return postsAdapter.addMany(postsAdapter.getInitialState(), response)
},
}),
}),
})
createEntityAdapter의 사용예 추가
createApi의 각 endpoints은 쿼리 해결 방법을 결정하는 inline function, queryFn를 제공하여 주어진 endpoints이 baseQuery가 전체적으로 적용한 것을 무시할 수 있게 한다.
이는 single endpoint에 대해 특히 다른 동작을 원하거나 쿼리 자체가 관련이 없는 시나리오에 유용할 수 있습니다.
이러한 상황에는 다음이 포함될 수 있습니다.
다른 기본 URL을 사용하는 일회성 쿼리
자동 재시도와 같이 다른 요청 처리를 사용하는 일회성 쿼리
다른 오류 처리 동작을 사용하는 일회성 쿼리
단일 쿼리로 여러 요청 수행(예시)
관련 쿼리 없이 무효화 동작 활용(예시)
const queryFn = (
args,
{ signal, dispatch, getState },
extraOptions,
baseQuery
) => {
if (Math.random() > 0.5) return { error: 'Too high!' }
return { data: 'All good!' }
}
특정 시나리오에서는 요청을 보내거나 데이터를 반환하는 것과 같은 일반적인 상황과 관련이 없는 query/mutation 만을 원할 수 있습니다. 이러한 시나리오는 invalidatesTags 속성을 활용하여 캐시에 제공된 특정 태그를 강제로 다시 가져오는 것입니다.
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query'
const api = createApi({
baseQuery: fetchBaseQuery({ baseUrl: '/' }),
tagTypes: ['Post', 'User'],
endpoints: (build) => ({
getPosts: build.query({
query: () => 'posts',
providesTags: ['Post'],
}),
getUsers: build.query({
query: () => 'users',
providesTags: ['User'],
}),
refetchPostsAndUsers: build.mutation({
queryFn: () => ({ data: null }),
// 이 mutation은 invalidatesTags 동작을 활용하여 현재 캐시된 데이터를 re-fetch 하여 캐시를 update한다.
invalidatesTags: ['Post', 'User'],
}),
}),
})
RTK Query는 endpoints가 데이터에 대한 초기 요청을 보내고 업데이트가 발생할 때 캐시된 데이터에 대한 추가 업데이트를 수행하는 반복 스트리밍 업데이트로 이어지는 기능을 제공합니다. 그러나 초기 요청은 선택 사항이며 초기 요청을 시작하지 않고 스트리밍 업데이트를 사용할 수 있습니다.
아래 예에서 queryFn은 초기 요청을 보내지 않고 빈 배열로 캐시 데이터를 채우는 데 사용됩니다. 배열은 나중에 onCacheEntryAdded endpoints 옵션을 통해 스트리밍 업데이트를 사용하여 채워지며 캐시된 데이터가 수신될 때 업데이트됩니다.
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query'
const api = createApi({
baseQuery: fetchBaseQuery({ baseUrl: '/' }),
tagTypes: ['Message'],
endpoints: (build) => ({
streamMessages: build.query({
// 스트리밍 업데이트를 통해 데이터가 제공되므로 쿼리는 여기에서 관련이 없습니다. 빈 배열을 반환하는 queryFn이 사용되며 콘텐츠가 수신될 때 아래 스트리밍 업데이트를 통해 채워진다.
queryFn: () => ({ data: [] }),
async onCacheEntryAdded(arg, { updateCachedData, cacheEntryRemoved }) {
const ws = new WebSocket('ws://localhost:8080')
// websocket에서 받은 메시지로 배열을 채운다.
ws.addEventListener('message', (event) => {
updateCachedData((draft) => {
draft.push(JSON.parse(event.data))
})
})
await cacheEntryRemoved
ws.close()
},
}),
}),
})
아래 예에서는 임의의 사용자에 대한 모든 게시물을 가져오는 쿼리가 작성되었습니다. 이것은 임의의 사용자에 대한 첫 번째 요청을 사용하여 수행된 다음 해당 사용자에 대한 모든 게시물을 가져옵니다. queryFn을 사용하면 단일 쿼리에 두 요청을 포함할 수 있으므로 구성 요소 코드 내에서 해당 논리를 연결하지 않아도 됩니다.
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query'
const api = createApi({
baseQuery: fetchBaseQuery({ baseUrl: '/ ' }),
endpoints: (build) => ({
getRandomUserPosts: build.query({
/* args, { signal, dispatch, getState }, extraOptions, baseQuery */
async queryFn(_arg, _queryApi, _extraOptions, fetchWithBQ) {
// 받은 값 user을 이용하여 연속으로 user의 posts를 가져온다.
const randomResult = await fetchWithBQ('users/random')
if (randomResult.error) throw randomResult.error
const user = randomResult.data
const result = await fetchWithBQ(`user/${user.id}/posts`)
return result.data ? { data: result.data } : { error: result.error }
},
}),
}),
})
Redux 코어 및 Redux 툴킷과 마찬가지로 RTK 쿼리의 기본 기능은 UI에 구애받지 않으며 모든 UI 계층에서 사용할 수 있습니다. RTK 쿼리에는 React 후크를 자동으로 생성하는 React와 함께 사용하도록 특별히 설계된 createApi 버전도 포함되어 있습니다.
React 후크는 대부분의 사용자가 RTK 쿼리를 사용할 것으로 예상되는 주요 방법이지만 라이브러리 자체는 일반 JS 로직을 사용하며 React 클래스 구성 요소와 함께 사용하거나 React 자체와 독립적으로 사용할 수 있습니다.
이 페이지에서는 RTK 쿼리 캐시 동작을 적절하게 사용하기 위해 React Hooks 없이 사용할 때 RTK 쿼리와 상호 작용하는 방법을 설명합니다.
캐시 구독은 엔드포인트에 대한 데이터를 가져와야 한다고 RTK 쿼리에 알리는 데 사용됩니다. 쿼리 끝점에 연결된 시작 썽크 작업 생성자의 결과를 디스패치하여 끝점에 대한 구독을 추가할 수 있습니다.
React 후크를 사용하면 이 동작이 대신 useQuery, useQuerySubscription, useLazyQuery 및 useLazyQuerySubscription 내에서 처리됩니다.
dispatch(api.endpoints.getPosts.initiate())
RTK 쿼리가 캐시된 데이터가 더 이상 필요하지 않음을 식별하려면 캐시 구독을 제거해야 합니다. 이를 통해 RTK 쿼리는 오래된 캐시 데이터를 정리하고 제거할 수 있습니다.
쿼리 끝점의 시작 썽크 작업 생성자를 디스패치한 결과는 구독 취소 속성이 있는 개체입니다. 이 속성은 호출될 때 해당 캐시 구독을 제거하는 함수입니다.
React 후크를 사용하면 이 동작이 대신 useQuery, useQuerySubscription, useLazyQuery 및 useLazyQuerySubscription 내에서 처리됩니다.
// Adding a cache subscription
const result = dispatch(api.endpoints.getPosts.initiate())
// Removing the corresponding cache subscription
result.unsubscribe()
쿼리 엔드포인트의 select 함수 속성을 사용하여 캐시 데이터 및 요청 상태 정보에 액세스하여 reducer를 만들고 Redux 상태로 호출할 수 있습니다. 이것은 캐시 데이터의 스냅샷을 제공하고 호출 시 상태 정보를 요청합니다.
주의
endpoint.select() 함수는 새로운 reducer 인스턴스를 생성합니다 - 실제 reducer 함수 자체가 아닙니다!
React 후크를 사용하면 이 동작이 대신 useQuery, useQueryState 및 useLazyQuery 내에서 처리됩니다.
const result = api.endpoints.getPosts.select()(state)
const { data, status, error } = result
돌연변이는 서버의 데이터를 업데이트하기 위해 사용됩니다. 돌연변이 끝점에 연결된 시작 썽크 작업 생성자의 결과를 전달하여 돌연변이를 수행할 수 있습니다.
React 후크를 사용하면 이 동작이 대신 useMutation 내에서 처리됩니다.
dispatch(api.endpoints.addPost.initiate({ name: 'foo' }))
RTK 쿼리 사용의 다양한 측면을 보여주는 다양한 예가 있습니다.
이러한 예는 애플리케이션의 기반이 되기 위한 것이 아니라 애플리케이션에서 실제로 원하지 않거나 필요하지 않을 수 있는 매우 구체적인 동작을 보여주기 위해 존재합니다. 대부분의 사용자는 쿼리 및 변형 섹션의 기본 예제에서 대부분의 요구 사항을 다룹니다.
CodeSandbox에서 예제를 사용할 때 특히 예제를 분기하고 파일 편집을 시작하는 경우 이상한 동작을 경험할 수 있습니다. 핫 리로딩, CSB 서비스 작업자 및 msw는 때때로 올바른 페이지로 이동하는 데 문제가 있습니다. 이 경우 CSB 브라우저 창에서 새로 고침하세요.
- ID가 있는 태그를 사용하여 캐시 invalidation 및 refetching를 관리하는 방법
- React 외부에서 RTK 쿼리 캐시로 작업하는 방법
- response 데이터 조작 기술
- optimistic 업데이트 및 스트리밍 업데이트 구현