서비스를 출시하고 난 후 한 크루원 분께 이러한 피드백을 받게 되었습니다.
"모집 중, 모집 완료 필터를 바꿀 때 사용자가 선택하는 즉시 내부적으로 쿼리를 타서 결과를 보여주는 것 같은데, 그동안 사용자에게 결과를 검색중 이라는 피드백을 주지 못하고 화면이 멈춰있습니다. 데이터가 더 많아지면 멈춰있는 시간이 더 길어져서 사용자가 오해할 것 같습니다."
검색 페이지에서 필터링을 변경하게 될 경우 로딩 UI가 보이지 않고 화면이 멈춰 사용자 경험이 저해될 수 있다는 피드백이였습니다.
사실 개발 단계에서도 저 또한 인지하고 있었던 문제였습니다. 초기 로딩 뿐만 아니라 페이지 내에서 필터링이 변경될 경우에도 로딩 UI가 발생하는 것을 의도하고 Suspense로 감쌌으니깐요. 하지만 개발 기간 내에 결국 해결하지 못하고 해당 문제는 잊혀지게 됩니다.
하지만, 웹사이트를 이용하다보면 필터링을 재적용할 때 로딩 UI가 보이는 경우를 흔히 볼 수 있었는데 그 때마다 정말 좋은 사용자 경험을 느낄 수 있었습니다. 그래서인지 꼭 해결해보고 싶다는 의지가 생겼습니다.
저는 문제 상황이 주어졌을 때 근본적인 원인을 정확히 분석하면 정말 좋은 해결방안이 나온다 라는 것을 굳게 믿고 있습니다. 따라서, 문제점을 찾기 위해 여러 시도를 하고 문제 상황을 재현해보았습니다.
해당 프로젝트에서 비동기 로직은 React Query
로 관리하고 있습니다.
그렇기 때문에 첫 번째로 시도해본 방법은 isFetching
의 상태가 true
로 찍히는지 확인하는 것입니다. 보이는 대로 각각 다른 필터링을 적용해보아도 콘솔에는 false
로 출력되고 있습니다.
그래서 React Query의 개발자 도구를 열어보았습니다.
보이시나요? 개발자 도구에서는 Fetching이 분명히 파란 불로 들어오고 있습니다.
하지만 이상한 점 한 가지를 발견하게 됩니다. 새로운 카테고리를 클릭함과 동시에 새로운 쿼리가 추가되고 fresh한 쿼리가 stale한 상태로 변경되고 있습니다.
그리고 콘솔에 출력되는 타이밍과 개발자 도구의 Fetching이 불이 켜지는 타이밍을 비교해보았을 때
의 과정을 거치고 있었습니다.
여기서 하나 이상한 점을 캐치하게 됩니다. 저희 프로젝트에서는 서버 사이드 렌더링을 한 번 거친 후 클라이언트에서 재페칭을 하는 문제점을 막기 위해 staleTime을 5초로 설정해주었습니다.
const [queryClient] = React.useState(
() =>
new QueryClient({
defaultOptions: {
queries: {
staleTime: 5 * 1000,
},
},
}),
);
5초로 설정되어 있기 때문에 5초동안 Fresh 상태를 유지하고 있어야 하는데 즉시 stale로 변경되는 점에서 의구심을 품었습니다. 그래서 저는 캐싱된 값을 선탐색한 후 페칭을 시도하는 과정을 거친다고 추측하였고 staleTime
, gcTime
을 모두 0으로 설정하였습니다.
export const useGetStudyGroupList = (params = '') => {
return useSuspenseInfiniteQuery({
queryKey: ['useGetStudyGroupList', params],
staleTime: 0,
gcTime: 0,
});
};
gcTime
을 0
으로 설정해준 이유는 한 번 검색했던 조건으로 다시 검색할 때 페칭되는 과정에서 이전 데이터가 보임을 방지하고 로딩 UI를 보여주기 때문에 즉시 가비지 컬렉션 되어도 문제가 없다고 판단했기 때문입니다.
여기까지의 설정을 기반으로 다시 테스트 해보았습니다.
드디어 Suspense가 비동기 작업을 수행하고 있음을 catch하고 스켈레톤 UI가 정상적으로 보여지게 되었습니다 🎉!
export const useGetStudyGroupList = (params = '') => {
return useSuspenseInfiniteQuery({
queryKey: ['useGetStudyGroupList', params],
});
};
전역에 설정된 staleTime을 제거하고 useQuery 훅에 설정된 staleTime, gcTime을 모두 제거하였습니다. 이렇게 되면 staleTime의 디폴트 값은 0입니다. 이 상태에서 재현을 해보면 이미 검색했던 조건을 재검색 할 경우에만 Fetching이 활성화됩니다. 반대로 새로운 검색 조건일 경우 캐싱을 선탐색하는 문제가 발생합니다.
이 문제를 통해 반드시 사용하는 useQuery 훅 내에서 반드시 staleTime: 0
으로 설정해주어야 캐싱된 값을 탐색하지 않음을 파악했습니다.
왜 반드시 훅 내에서 staleTime
을 0으로 설정해주어야 할까요? 그렇지 않을 경우 암묵적으로 내부적으로 캐싱된 값이 존재하는지 탐색을 하는 걸까요? 라이브러리의 이슈일 수도 있고 제가 작성한 코드에 의해 사이드 이펙트가 발생한 것일 수도 있습니다. 혹은 제가 리액트 쿼리에 대해 정확히 이해하지 못한 것일 수도 있습니다.
최근에 라이브러리를 깊게 공부하는 것에 대해 큰 흥미와 중요성을 느끼고 있습니다. 언어도 언어지만 프레임워크 및 라이브러리도 깊은 이해를 기반으로 사용하는 것이 중요하다는 것을 크게 깨닫게 되는 계기였습니다.