Optimistic UI는 웹사이트나 앱에서 사용자가 어떤 동작을 했을 때, 모든 API 통신이 성공할 것이라 여기고 화면에 먼저 결과를 그리는 것을 말한다.
서버가 느려도 사용자는 자신의 동작 결과를 바로 볼 수 있기 때문에 사용자 경험을 향상시킬 수 있다. 하지만 서버에 문제가 생기면 보여준 결과를 취소해야 하고 결제 등과 같은 경우에는 확인 후 보여줘야 한다.
프로젝트를 진행하면서 카드 컴포넌트 구현 중 참여하기 버튼 클릭 시 API는 정상 작동하나 UI가 즉시 업데이트되지 않아 사용자 경험 저하되는 문제가 있었다.
Tanstack Query의 setQueryData
를 활용하여 즉각적인 UI 업데이트 구현하고, invalidateQueries
를 사용하여 서버 데이터와 클라이언트 캐시 동기화했다.
invalidateQueries
invalidateQueries
메서드는 캐시된 쿼리를 무효화하고, 활성 상태의 쿼리는 즉시 refetch되어 서버의 최신 데이터를 가져온다.
서버의 최신 데이터를 가져올 수 있어 데이터 일관성을 유지할 수 있지만 추가적인 네트워크 요청이 발생할 수 있다.
setQueryData
setQueryData
메서드는 캐시 된 쿼리 데이터를 수동으로 업데이트한다.
네트워크 요청 없이 즉각적인 UI 업데이트가 가능하지만 서버와 클라이언트 데이터 간의 불일치가 발생할 수 있다.
const joinMutation = usePostGatheringsJoin({
onSuccess: (updatedData) => {
console.log('참여하기 성공', updatedData);
const newData = {
...localData,
isJoiner: true,
participantCount: localData.participantCount + 1,
};
setLocalData(newData); // 로컬 상태를 즉시 업데이트
queryClient.setQueryData(['gatherings', data.gatheringId], newData); // 캐시에 있는 데이터를 즉시 업데이트
queryClient.invalidateQueries({ queryKey: ['gatherings'] }); // 데이터를 다시 가져오게 요청
},
onError: (error) => {
console.error('참여하기 실패', error);
},
});
개인적으로, 대부분의 경우에 무효화를 선호해야 한다고 생각합니다. 물론, 사용 사례에 따라 다르지만, 직접 업데이트가 신뢰성 있게 작동하려면 프론트엔드에 더 많은 코드가 필요하며 어느 정도 백엔드의 로직과 중복된 로직이 필요합니다. 예를 들어, 정렬된 목록은 직접 업데이트하기가 꽤 어렵습니다. 업데이트로 인해 내 항목의 위치가 변경되었을 수 있기 때문입니다. 목록 전체를 무효화하는 것이 “더 안전한” 접근 방법입니다.
TKDodo의 Mastering Mutations in React Query
TKDodo의 글에서는 invalidateQueries
를 더 안전한 접근 방법이라고 설명한다. 이번 프로젝트에서는 직접 업데이트를 하는 코드가 복잡하지 않아 setQueryData
를 사용해 구현했다.
Optimistic UI를 구현할 때, 데이터가 즉각적으로 반영되도록 Tanstack query의 캐시 관리를 효율적으로 활용하는 것이 중요하다. 특히 Query Key, staleTime, gcTime, 그리고 refetch와 같은 옵션을 적절하게 설정함으로써, 데이터를 안전하게 관리하고 사용자 경험을 향상시킬 수 있다.
Query Key는 캐시 데이터를 고유하게 식별하는 역할을 하며, 캐시된 데이터를 안전하게 업데이트하거나 무효화하기 위해 중요하다.
queryClient.setQueryData(['gatherings', data.gatheringId], newData);
queryClient.invalidateQueries({ queryKey: ['gatherings'] });
staleTime은 캐시된 데이터가 stale(오래된) 상태로 간주되기 전까지의 시간을 정의한다. 기본적으로 0으로 설정되어 있어, 쿼리가 활성화될 때마다 데이터를 다시 가져온다. 하지만 프로젝트 특성에 따라 staleTime을 늘리면 불필요한 네트워크 요청을 줄일 수 있다.
const { data, error } = useQuery(['gatherings', gatheringId], fetchGathering, {
staleTime: 1000 * 60 * 5, // 5분 동안 데이터를 stale로 간주하지 않음
});
gcTime은 캐시 데이터가 메모리에서 자동으로 삭제되기 전까지 유지되는 시간을 설정한다. 여기서 데이터의 효율적인 재사용, 사용자 경험 개선, 네트워크 요청 최적화 등의 이점을 위해 gcTime을 staleTime보다 길게 설정한다.
const { data, error } = useQuery(['gatherings', gatheringId], fetchGathering, {
gcTime: 1000 * 60 * 10, // 10분 동안 캐시 유지
});
Tanstack query는 다양한 refetch 옵션을 제공하여, 특정 조건에서 데이터를 자동으로 다시 가져올 수 있다. 예를 들어, refetchOnWindowFocus 옵션을 통해 브라우저 탭이 다시 활성화될 때 데이터를 새로 고칠 수 있다.
const { data, error } = useQuery(['gatherings', gatheringId], fetchGathering, {
refetchOnWindowFocus: true, // 윈도우 포커스 시 데이터 새로 고침
});
이렇게 Tanstack query의 다양한 옵션을 조합하여 사용자 경험을 극대화할 수 있는 효율적인 캐시 관리 전략을 구현할 수 있다.