react query
작게 보면 서버 상태 도구, 넓게 보면 비동기 상태 도구이다. 서버를 대상으로하는 data fetching뿐만 아니라 비동기적으로 이루어지는 외부 시스템과의 통신 및 처리를 관리하는데 사용할 수 있다.
react query를 사용하는 이유
서버와 통신할 때 endpoint마다 반복적으로 이루어지는 loading, error 등 상태 관리를 하기가 귀찮을 때 사용할 수 있다. 게시물 리스트를 서버로부터 받아오는 상황을 예로 들어 보자.
AS-IS
async function getPosts() {
const response = await fetch(`${BASE_URL}/api/posts`);
const data = await response.json();
return data;
};
function usePosts() {
const [posts, setPosts] = useState<Post[]>([]);
const [isLoading, setIsLoading] = useState<boolean>(true);
const [error, setError] = useState();
useEffect(() => {
try {
getPosts.then((posts) => {
setPosts(posts);
});
} catch(e) {
setError(e);
} finally {
setLoading(false);
}
});
return { data, isLoading, error };
}
isLoading, error 와 같은 상태는 스켈레톤 UI와 에러 UI를 보여주기 위해서 대부분의 data fetching 로직에서 반복적으로 관리해주어야하는 상태이다. useQuery를 사용하면 다음과 같이 상태를 반복적으로 정의하지 않고 재사용할 수 있다.
TO-BE-1
async function getPosts() {
const response = await fetch(`${BASE_URL}/api/posts`);
const data = await response.json();
return data;
};
async function usePosts() {
return useQuery({
queryKey: ['posts'],
queryFn: getPosts,
});
}
function PostListSection(){
const { data: posts, isLoading, error } = usePosts();
if (isLoading) {
return <ListSection.Skeleton />;
}
if (error != null) {
return <ListSection.Error />;
}
return (
<>
{posts.map(post => <Post {...post} />)}
</>
);
}
- loading ui는 어디서 비동기 작업이 트리거되는지 알 필요없다.
- 비동기 작업은 어떤 loading ui가 비동기 작업을 캐치했는지 알 필요가 없다.
- component tree의 어딘가에 있는 다른 컴포넌트들도 비동기 작업을 수행할 때, 하나의 Suspense로 잡아서 Loader UI를 보여줄 수 있다.
Cache Invalidation
- data stale하다 && 새로운 query instance가 mount되었다
- data가 stale하다 && 네트워크 재연결이 발생하다. 네트워크 연결이 불안정안 환경에 있는 유저에게 best effort로 data fetching 결과를 제공해주어야 할 때 사용할 수 있다.
- data가 stale하다 && window가 refocus되었다.
-
queryClient.invalidateQueries()를 통해 query invalidation이 발생하였다.
Background Refetching
- background에서 refethcin
react query의 active / inactive
- 더이상 mount된 컴포넌트 중에 react query hook을 사용하고 있지 않은 경우 inactive 상태로 전환된다.
- inactice된지 gcTime이 지났을 때 캐시는 제거된다.
새로운 query instance가 mount되었다
- 새로운 query key를 가지는 react query hook (useQuery, useSuspenseQuery) 가 mount되었다
useSuspenseQueries를 사용하는 이유
- Suspense Boundary로 인하여 request waterfall이 발생할 때 사용한다.
<>
<Suspense clientOnly>
</Suspense>
</>
Structural Sharing and Referential Stability
- 레퍼런스를 유지하여 virtual dom에서 리렌더링이 필요한 부분만 리랜더링(재실행) 할 수 있게 만들어준다.
- deeply nested object의 depth별로, 데이터가 변하지 않았다면 동일한 레퍼런스를 유지한다.
async function usePosts() {
return useSuspenseQuery({
queryKey: ['posts'],
queryFn: getPosts,
});
}
async function usePostCount() {
return useSuspenseQuery({
queryKey: ['posts'],
queryFn: getPosts,
select: ({ data: posts }) => posts.count
});
};
throwing Error on onSuccess callback
-
onSuccess와onError는 모두
useSuspenseQueries
Suspensive를 사용해야 하는 이유
<SuspenseQuery />를 이용한
- Suspense를 사용하면 subtree에서
Suspense와useSuspenseQuery를 같이 사용하기 위해서는, data fetching만을 위해서<PostList />와 같이 컴포넌트를 추가로 만들어야 한다.
AS-IS
function PostsPage() {
return (
<>
...
<ErrorBoundary>
<Suspense fallback={<List.Skeleton/>}>
<PostList/>
</Suspense>
</ErrorBounbary>
...
</>
);
}
function PostList() {
const { data: posts } = usePosts();
return (
<>
{posts.map((props) => (
<Post {...props} />
))}
</>
);
}
TO-BE
- data fetching
export function PostPage() {
return (
<ErrorBoundary fallback={List.Error}>
<Suspense fallback={List.Skeleton}>
<SuspenseQuery {...postsQueryOptions}>{({ data: { posts } }) => posts.map((post) => <Post {...post} />)}</SuspenseQuery>
</Suspense>
</ErrorBoundary>
);
}
Client-only suspense
useSuspenseQuery를 사용하게 되면 Next.js 서버에서도 실행된다. 이를 막기 위해서 특정 컴포넌트 트리를 client서만 실행할 수 있게 설정해야 하는데, 이 때 Suspensive의
Loader UI의 깜빡거림을 컴포넌트로 방지
data fetching시에 응답이 빠르게 온다면, Loader UI에서 데이터 UI로 전환될 때 깜빡인다는 느낌을 사용자에게 줄 수 있다. ` 컴포넌트를 사용하여 Loader UI 렌더링을 의도적으로 지연시켜서, 깜빡거린다는 느낌을 줄일 수 있다.
tsx
function PostPage() {
return (
<Suspense fallback={<Delay ms={200}><Skeleton/></Suspense>}
<PostListSection />
</Suspense>
)
}
Loader UI가 Delay 250ms 후에 렌더렝되고, data fetching이 300ms에 이루어진다면 유저는 여전히 깜빡이는 느낌을 받을 것이다. 이를 해결하기 위해서 Loader UI에 최소 렌더링 시간을 부여할 수도 있을 것이다. 다만 그렇게하면 데이터를 보여주기까지의 시간도 같이 길어질 수 있다는 문제가 있다. 다음의 시나리오를 확인하라.
Loader UI의 렌더링을 200ms만큼 지연시키고, 최소 렌더링 시간을 보장하지 않는 경우 발생하는 시나리오
- 0ms~200ms: data fetching을 시작하고, 빈 화면을 보여준다.
- 200ms~300ms: Loader UI를 보여준다.
- 300ms~: data fetching이 완료되어 데이터 UI를 보여준다.
Loader UI의 렌더링을 200ms만큼 지연시키고, 최소 렌더링 시간을 300ms만큼 보장하는 경우 발생하는 시나리오
- 0ms~200ms: data fetching을 시작하고, 빈 화면을 보여준다.
- 200ms~300ms: Loader UI를 보여준다.
- 300ms~400ms: data fetching이 완료되었지만, Loader UI의 최소 렌더링 시간 200ms를 채우기 위해서 계속 Loader UI를 보여준다.
- 400ms~: data fetching이 완료되어 데이터 UI를 보여준다.
최소 렌더링 시간을 보장하게 되면 UI의 깜빡거림은 막을 수 있겠지만, 데이터 UI를 보여주는데 걸리는 시간이 지연된다. 유저가 원하는 정보를 가장 빠르게 보여주고 싶다면 Loader UI의 최소 렌더링 시간은 설정하지 않고, Delay 컴포넌트로 지연만 주는 것이 최선의 선택이다. 최소 렌더링 시간의 트레이드오프를 고려해서 선택하라.
TL;DR
- 작게 보면 비동기, 크게 보면 서버 상태를 관리하기 위해서 react query를 사용한다. 2.
- Client-only suspense, Loader UI 렌더링 지연을 통한 깜빡거림 방지, SuspensiveQuery/SuspenseQueries를 위하여 Suspensive를 사용한다.
Top comments (0)