InfinityQuery에서 fetch가 제대로 이루어지지 않는다?!?!

@pium · August 22, 2023 · 8 min read

이 글은 우테코 피움팀 크루 '클린'가 작성했습니다.

문제: 무한 스크롤 시에 데이터 fetch가 제대로 이루어지지 않는다.

에러 구현 방법: 특정 식물 상세보기 접속 → 타임라인 페이지 접속 → 뒤로가기 → 해당 식물 정보 수정 → 타임라인 페이지 접속

서론

‘피움’ 서비스 에서는 식물의 관리 이력(식물 물 주기, 물 주기 Cycle 변화, 식물의 환경 변화 등)을 확인할 수 있는 ‘타임라인’ 서비스를 제공하고 있습니다. 이 타임라인 기능에서는 다른 페이지로 이동할 필요 없이 아래로만 스크롤 하면 다음 데이터가 나오는 ‘무한 스크롤’ 기능을 사용자들에게 제공하고 있는데, 이 무한 스크롤을 좀 더 편리하게 사용하기 위해서 React-Query에서 제공하는 useInfiniteQuery를 통해 무한 스크롤을 구현하고 있습니다.

본론

사건의 발단은 다음과 같습니다. 식물의 관리 이력을 수정하고 변경 내역이 타임라인에 기록되었는지를 확인하기 위해 타임라인을 클릭 했더니 수정하기 전 타임라인을 제공하고 있던 것이었습니다. 당시에 운영 서비스 배포하기로 결정한 당일 이었기 때문에 상당히 당황한 기억이 있습니다.

문제를 찾아보기 위해 console을 통해서 데이터 fetch 순서를 한번 봤습니다. 처음에 접속해서 캐시되어 있던 데이터가 먼저 찍히는 것을 볼 수 있습니다. 그 다음에 타임라인에 사용될 데이터를 보여주고, 데이터 통신이 완료가 됩니다. GET /history 이력을 보면 데이터 fetch가 완료된 것을 볼 수 있는데, 데이터 fetch를 하고 끝이 납니다. 마지막으로 fetch한 데이터가 가장 최신의 데이터이고 타임라인에 적용되어야 할 데이터인데, 전혀 적용되지 않고 fetch만 하고 끝이 됩니다. 이 때문에 타임라인에는 이전 내용이 적용되어서 데이터가 나타나는 것입니다. 문제는 언제는 또 제대로 작동이 된다는 것 이었습니다. 이렇게 일관성 없는 fetch 동작에 상당히 많은 혼란이 있었습니다.

  • Response Data: 처음 fetch 되었을 때 나온 데이터
  • Select Data: fetch된 데이터를 가공하기 위해 select 옵션을 사용한 과정에서 찍히는 데이터
  • TimeLine Data: 실제 타임라인에 사용될 데이터

image

문제의 원인을 ‘캐시된 데이터’라고 추정한 가운데 생각한 해결책은 ‘데이터를 fetch 할 때마다 캐시를 하지 않게 하려면 어떻게 해야 하나?’ 였습니다. React-Query에 대해서 좀 더 자세히 알고 있었다면 금방 해결 됐을 문제이지만, 옵션 들에 대해 그렇게 자세하게 알지 못했고, 애꿎은 queryKeyenable 등을 건드렸다. Dependent Queries를 사용하면서, enable 옵션에 상태 값을 할당하는데, 바로 호출하는 방식이 클로저에 갇혀서 계속 같은 값으 제공하나? 이런 생각도 하고, queryKey가 같아서 계속 캐싱을 하나… filter를 없애야 하나 하는 생각도 했습니다.

이렇게 여러 가지를 시도해 보다가 한 가지 원인을 알게 되었는데, React-Query가 제공하는 캐시 예제에 있는 것과 동일합니다.

image

캐시의 시간이 완료되기 전에 똑같은 인스턴스가 마운트 된다면. 해당 쿼리는 즉시 사용 가능한 캐시 데이터를 반환하고, queryFn은 background에서 실행이 됩니다. 그리고 해당 fetch가 성공적으로 완료된다면, 방금 전에 fetch한 데이터가 캐시 데이터로 들어가는 것입니다.

즉, 저희가 겪었던 문제는 기본적으로 설정되어 있는 옵션들 (staleTime:0, gcTime:5분)에서는 자연스럽게 발생하는 문제들인 것이었습니다. 최초로 요청한 인스턴스가 가비지 콜렉션에 들어가기 전에 계속해서 같은 인스턴스를 요청하였고, 동작 원리에 따라 5분간은 같은 결과 (2 - 3 - 1 순서, 캐시 → fetch)로 작동하게 된 것이었습니다.

결국에 이를 해결하기 위해서는 캐시 시간을 짧게 설정하면 되는 문제였고, 타임라인의 경우에는 캐시를 하지 않겠다는 의미로 gcTime을 0으로 설정하여 해결할 수 있는 문제였습니다.

결론

이번 트러블 슈팅을 하면서 크게 2가지 과정에서 문제가 있지 않았나 하는 생각이 들었습니다.

첫 번째는 “잘 알지 못하는 라이브러리를 도입했다. 이 때문에 배포 당일 날에 문제를 마주했고, 쫄깃한 경험을 하게되었다.” 입니다. 물론 학습하는 입장에서 매우 좋은 경험이었지만, 실제 상황이었다면 살짝 아찔한 순간이었을 것 같습니다.

두 번째는 이러한 문제를 개발 서버에서 확인했다는게 약간 치명적이지 않았나 라는 생각이 들었습니다. msw를 통해서 mockAPI를 통해서 모든 처리를 하고 있는데, 해당 API 통신에 대한 빡빡한 검증이 부족했던 것 같기도 하고, 테스트의 범위를 어디까지 정해야 하는 지도 살짝 애매한 느낌이 들긴 했습니다. 타임라인 이력을 불러오는 API을 구현한다면 잘 작성한 mockAPI이지만, e2e테스트로 생각을 한다면 살짝 부족한 과정이지 않았나를 생각하면서 e2e테스트의 중요성과 msw를 작성하는데 공식적으로 사용할만한 DB가 없었다는게 살짝 아쉬워 지는 트러블 슈팅이었습니다.

@pium
우아한테크코스 5기 피움팀 기술 블로그입니다.