React query (Tanstack query) 를 다시 사용하게 될 미래를 위해, 제대로 사용하기 위한 개념과 사용 패턴 정리하기! - 2탄
이전 포스트 보러가기 - React query 파보기
여러개의 쿼리를 promise.all
처럼 병렬적으로 실행하기
const result = useQueries({ queries: …, queryClient: …, combine: … })
useQueries 옵션들
queries
- useQuery 사용 시 넣어주는 쿼리 객체 배열
queryClient
- 쿼리들에게 따로 구성한 커스텀 QueryClient 를 제공하기 위한 옵션 (optional)
combine
- 쿼리 결과들을 단일 값으로 결합할 때 사용 (optional)
- useQuery 의 select 옵션과 유사
1. useQueries 를 활용하여 한번에 데이터 불러오기
const results = useQueries({
queries: [
{
queryKey: ["users"],
queryFn: () => getUsers(),
},
{
queryKey: ["todos"],
queryFn: () => getTodos(),
staleTime: 1000,
},
],
});
위의 예시에서 getUsers 가 1000ms 걸리고 getTodos 가 2000ms 가 걸린다고 가정했을 때, 초기 데이터 로딩에 걸리는 시간은 약 2000ms 이다.
쿼리에서 반환하는 객체들로 이루어진 배열을 반환한다. (result)
result[0].isPending
이런식으로 써야하는 불편함이 있을 수 있다.2. Combine 으로 단일 데이터 반환하기
const ids = [1, 2, 3];
const result = useQueries({
queries: ids.map((id) => (
{ queryKey: ['post', id], queryFn: () => fetchPost(id) },
)),
combine: (results) => {
return ({
data: results.map((result) => result.data),
pending: results.some((result) => result.isPending),
});
},
});
기존 반환값
result = [
{data: …, isPending: … },
{data: …, isPending: … }
]
Combine 옵션으로 결합한 반환값
(pending 과 같은 쿼리 실행 상태를 다루기 훨씬 편해졌다)
result = {
data: [...],
pending: true // or false
}
특정 쿼리에 의존할 경우, 해당 쿼리가 완료될 때 까지 실행 멈추기
1. useQuery + enabled 를 사용한 종속적인 쿼리
enabled
옵션을 사용해서 특정 값이 존재할 때 까지 쿼리 실행을 막을 수 있다.
// id 값을 받아와서 요청에 포함시켜야 하는 경우
const { data: user } = useQuery({
queryKey: ['user', email],
queryFn: getUserByEmail,
})
const userId = user?.id
const { data: projects } = useQuery({
queryKey: ['projects', userId],
queryFn: getProjectsByUser,
// userId가 존재할 때까지 쿼리를 실행하지 않음
// Boolean(userId)
enabled: !!userId,
})
2. useQueries + 삼항연산자 를 사용한 종속적인 쿼리
특정 값이 없을 때, queries 배열로 아예 빈 배열을 넣어버리는 방식이다.
queries: []
는 실행해야 하는 쿼리들이 없다는 뜻
// [id1, id2, id3, ...]
const { data: userIds } = useQuery({
queryKey: ['users'],
queryFn: getUsersData,
select: (users) => users.map((user) => user.id),
})
const usersMessages = useQueries({
queries: userIds
? userIds.map((id) => {
return {
queryKey: ['messages', id],
queryFn: () => getMessagesByUsers(id),
}
})
: []
// userIds 가 없다면 빈 배열 넣어줌
})
💡 주의
종속적인 쿼리는 요청 waterfall 을 형성하기 때문에, 성능에 영향을 줄 수 있다. 따라서 API 구조 자체를 바꾸는 것을 권장한다고 한다.
결국 직렬 수행은 병렬 수행보다 오래걸리기 때문이다!
mutation 이 완료되기 전에 UI 를 미리 업데이트하기
1. 캐시 직접 업데이트
old
를 new
로 수정하는 mutation 의 경우,
old
데이터 저장new
로 업데이트old
데이터로 돌아가기useMutation({
mutationFn: updateTodo,
onMutate: async (newTodo) => {
// 진행 중인 쿼리 업데이트 취소
await queryClient.cancelQueries({ queryKey: ['todos'] });
// 이전 쿼리값 저장
const previousTodos = queryClient.getQueryData(['todos']);
// mutation 으로 바뀔 새 값으로 업데이트
queryClient.setQueryData(['todos'], (old) => [...old, newTodo]);
// 이전 쿼리값이 들어있는 객체 반환 (**context**)
return { previousTodos };
},
onError: (err, newTodo, context) => {
// mutation 이 실패하면 이전 쿼리값으로 롤백
// 위에서 반환한 context 사용
queryClient.setQueryData(['todos'], context.previousTodos);
},
onSettled: () => {
// mutation 이 끝나고, 에러/성공에 상관없이 실제 쿼리 업데이트!
queryClient.invalidateQueries({ queryKey: ['todos'] });
},
})
2. UI 만 업데이트하기
useMutation 의 반환값인 variables 을 사용해서 UI 를 미리 바꿔줄 수 있다.
const { isPending, variables, mutate, isError } = useMutation({
mutationFn: (newTodo: string) => axios.post('/api/data', { text: newTodo }),
// 쿼리 업데이트가 완료될 때까지 보류시키기 위해 Promise 반환
onSettled: async () => {
return await queryClient.invalidateQueries({ queryKey: ['todos'] })
},
})
(mutation → 쿼리 업데이트) 가 진행되는 동안, isPending = true
이다.
isPending
일 시에만 variable 을 UI 에 보여주면 된다.variables 은 mutationFn 에서 매개변수로 받은 값이다.
variables = newTodo
<ul>
{todoQuery.items.map((todo) => (
<li key={todo.id}>{todo.text}</li>
))}
{isPending && <li>{variables}</li>}
</ul>
다시 시도하는 버튼을 구성할 수도 있다.
{
isError && (
<li>
{variables}
<button onClick={() => mutate(variables)}>다시 시도하기</button>
</li>
)
}
데이터 미리 불러오기 (fetch)
1. queryClient.prefetchQuery
미리 가져와서 캐시에 쌓아놓는 방법
const prefetchNextPosts = async (nextPage: number) => {
const queryClient = useQueryClient();
await queryClient.prefetchQuery({
queryKey: ["posts", nextPage],
queryFn: () => fetchPosts(nextPage),
});
};
useEffect(() => {
const nextPage = currentPage + 1;
if (nextPage < maxPage) {
prefetchNextPosts(nextPage);
}
}, [currentPage]);
2. 이벤트 핸들러에서 prefetch
버튼 focus, hover 와 같은 이벤트에 prefetch 연결하기
const ShowDetailsButton = () => {
const queryClient = useQueryClient()
const prefetch = () => {
queryClient.prefetchQuery({
queryKey: ['details'],
queryFn: getDetailsData
})
}
return (
<button onMouseEnter={prefetch} onFocus={prefetch} onClick={...}>
상세정보 표시
</button>
)
}
3. 컴포넌트 내에서 데이터 prefetch 하기
컴포넌트 라이프사이클로 인해 쿼리가 종속적으로 작동하게 될 경우,
부모 컴포넌트에서 prefetch 하는 방법을 통해 해결할 수 있다고 한다.
아래 예시의 경우 article
과 article-comments
가 종속적인 쿼리가 아님에도 불구하고, Comments 가 Article 의 자식 컴포넌트이기 때문에 종속적으로 실행된다. (article 쿼리 실행이 끝나야 article-comments 쿼리 실행)
request waterfall 이 발생한다고 할 수 있다.
// Article 컴포넌트
function Article({ id }) {
const { data: articleData, isPending } = useQuery({
queryKey: ['article', id],
queryFn: getArticleById,
})
if (isPending) {
return 'Loading article...'
}
return (
<>
<ArticleHeader articleData={articleData} />
<ArticleBody articleData={articleData} />
<Comments id={id} />
</>
)
}
// Comments 컴포넌트
function Comments({ id }) {
const { data, isPending } = useQuery({
queryKey: ['article-comments', id],
queryFn: getArticleCommentsById,
})
// ...
}
이를 해결하기 위해 article-comment
쿼리를 부모컴포넌트인 Article 로 올린 후, Comment 컴포넌트의 prop 으로 쿼리 결과를 전달해줄 수도 있다.
하지만, prop drilling 의 가능성 + 컴포넌트 역할 뒤죽박죽 + prop 타입 선언 어려움... 등등의 이유로 인해 좋지 않을 수 있다.
따라서 이런 방법이 있다고 한다!
function Article({ id }) {
const { data: articleData, isPending } = useQuery({
queryKey: ['article', id],
queryFn: getArticleById,
})
// Prefetch : 미리 comments 쿼리 실행해서 캐시 업데이트
useQuery({
queryKey: ['article-comments', id],
queryFn: getArticleCommentsById,
notifyOnChangeProps: [],
// 쿼리 결과에 따른 리렌더링을 막기 위한 옵션
})
if (isPending) {
return 'Loading article...'
}
return (
<>
<ArticleHeader articleData={articleData} />
<ArticleBody articleData={articleData} />
<Comments id={id} />
</>
)
}
function Comments({ id }) {
// stale time 이 지나지 않았다면 캐싱된 데이터만 가져옴
const { data, isPending } = useQuery({
queryKey: ['article-comments', id],
queryFn: getArticleCommentsById,
})
...
}
글이 또 길어졌다!
다음에는 Query Observer 와 같은 친구들이 어떤 일을 하는 지 간단한 동작 원리와, invalidate Query 에 대해 정리할 계획이다.