본문 바로가기
항해99/프로젝트

[TIL-35] react-query와 검색 기능에서의 캐싱 처리

by junvely 2023. 6. 15.

Today목표 : 06/15일 react-query와 검색 기능에서의 캐싱 처리

요즘 실전 프로젝트로 인해 너무 바쁘다... 이제 겨우 리팩토링을 진행하면서 저번 중간 발표회 때 받은 react-query에 대한 질문과, 검색 기능을 이용할 때 react-query를 사용했을 때의 문제점 등을 정리해 보고자 한다.


질문 받은 점,

1. 리액트 쿼리의 장점에 대한 답변 중 꼬리 질문 => 리액트 쿼리에서 refetch를 사용하면 서버의 데이터 변경을 알 수 있는 것인가? refetch를 사용하면? 서버의 데이터가 변경됐을 때 refetch로 변경된 데이터를 가져올 수 있다는 걸로 들린다.

=> 공부한 내용 : refetch는 서버의 데이터 변경 여부를 실시간으로 감지하는 기능은 아니며, refetch를 호출하는 시점에서 서버에 요청하여 업데이트된 데이터를 가져오는 것이라고 할 수 있다. invalidateQueries 메서드 또한 서버의 데이터 변경을 감지하는 것이 아니라, 강제로 캐시된 데이터를 무효화하고 새로운 데이터를 가져오도록 초기화하는 기능이다. 둘 다 캐싱 처리를 하는 것이지 클라이언트가 서버 데이터가 변경되는 것을 자체적으로 감지 하는 것은 아니다. 하지만 useQuery는 polling(주기적으로 조회)으로 일정 간격으로 데이터를 다시 가져와 데이터 변경을 확인할 수 있고 그밖에 리액트 쿼리의 기능으로 어떤 시점에 서버의 최신 데이터를 받아와 캐싱하여 데이터 변경을 알 수는 있다. 또 소켓 등과 같이 서버에서의 푸시 메커니즘을 수신하여 invalidateQueries로 캐싱을 갱신 할 수는 있다. 클라이언트가 데이터가 변경된 것을 감지하는 것은 아니고 어떤 시점에 최신 데이터를 받아오거나, 서버의 푸시를 수신해 캐싱하는 것이다.

2. 다른 팀에 대한 질문 중 => 리액트 쿼리의 캐싱 처리가 검색 기능에 미치는 영향, 결과를 바로 반영하고 있는지?

검색 기능에서 리액트쿼리, refetch 기능을 사용하여 구현하였는데, 1번 질문에 대해 정확히 답변하지 못했었다. 리액트 쿼리에 대한 이해가 부족했단 것을 깊게 느끼고 공부의 필요성을 깨닫고 공식문서를 보면서 알게된 점을 정리해 보고자 한다.

 

문제 상황

먼저 저 질문이 나온 기본적인 이유는, 리액트 쿼리가 캐싱 처리를 통해 중복, 반복되는 데이터 요청 시에는 서버에 데이터를 요청하지 않고 기존에 있는 캐싱된 데이터를 사용해 최적화를 한다는 점이다.

다만 이 부분이 검색 기능 구현에서는 문제가 될 수 있다. useQuery를 이용해 검색 키워드로 계속해서 데이터를 요청 할 경우,  캐싱 처리된 데이터를 사용하기 때문에 실시간으로 최신 업데이트된 서버의 데이터가 반영되지 않는 문제가 발생할 수 있다.  이 때문에 2번 질문이 나왔다.

1. 실시간 업데이트가 필요한 경우

2. 동적인 데이터 의존성이 높을 경우

3. 데이터 갱신 빈도가 높을 경우

이와 같은 몇몇의 상황에서는 오히려 캐싱 처리가 좋지 않을 수도 있다. 따라서 공부하면서 이런 상황에 대한 개선 방법을 정리해 보려고 한다. 내가 생각하였을 때 가장 낫다고 판단하는 우선 순위로 정렬하였다.

 

 

해결 방법,

1. 검색 키워드로 데이터를 재 요청 시, useQuery의 refetch기능을 이용한다.

내가 선택한 방법이었다. searchQuery를 전역 데이터로 관리하고, 메인 페이지에서는 searchQuery의 쿼리값으로 useQuery를 통해 데이터를 요청한다. searchQuery가 변경되면 자동으로 useQuery의 refetch가 실행되어 현재의 searchQuery 값으로 데이터를 다시 요청한다.

refetch의 실행 과정은 다음과 같다.

1. 우선 기존에 캐싱된 데이터가 있다면 캐싱된 데이터를 반환한다. 이로써 동일한 검색 결과를 빠르게 보여주고 성능상에서도 이점을 제공한다.

2. 캐싱된 데이터를 반환과 동시에 서버에 searchQuery를 기반으로 새로운 데이터를 요청하여 반환한다. 이 최신 데이터로 캐시를 업데이트 한다. 

결과적으로 refetch는 캐시된 데이터를 사용하여 빠르게 결과를 반환하고, 동시에 서버에 새로운 데이터를 요청하여 캐시를 업데이트 한다. 따라서 refetch를 호출하면 두 번째 데이터 요청이 발생하게 된다. 이로써 사용자에게 빠른 응답 속도와 최신 데이터를 제공하는 장점을 가지고 있다.

그렇다면 데이터가 두 번 요청이 되는데 성능상에 안좋은 것이 아닐까?

refetch를 호출할 때 데이터 요청이 두 번 발생한다는 것은 사실이지만, 일반적으로 성능에 큰 영향을 미치지는 않는다고 한다. 이것은 react-query가 효율적으로 데이터 캐싱 및 재사용 전략을 사용하기 때문이다.

1. 첫 번째 요청은 캐시 저장으로 네트워크 오버헤드 발생을 줄여 성능상에 이점이 있다.

2. 두 번째 요청에서는 사용자가 인지하지 못하도록 백그라운드에서 서버에 데이터를 요청하여 가져온다. 또 가져올 때에도 요청을 최적화해 필요한 데이터만 가져오도록 처리한다. 이를 통해 성능을 향상시킬 수 있다.

따라서 일반적으로는, 검색 기능을 사용할 때 refetch를 호출하면 캐싱된 데이터를 사용하면서도 최신 데이터를 가져올 수 있다. 이를 통해 성능을 향상시키면서 검색 결과를 정확하게 표시할 수 있다.

단점으로는 서버로부터 새로운 데이터를 가져오기 위해 두 번의 요청이 필요하므로, 성능 상 약간의 오버헤드가 발생할 수 있다는 점(시간적 오버헤드 : 두 번의 요청으로 인한 네트워크 지연, 서버 응답 시간, 데이터 양 등 다양한 요인에 따라 달라질 수 있다) 과 검색 쿼리 변경 시 이전 캐시 데이터가 잠시 동안 화면에 표시될 수 있다는 점이다. 캐시 데이터와 서버 데이터를 동기화하는 시간 차이로 인해 발생하는 현상이다. 미세한 차이라 사용자는 잘 느끼지 못하지만, 기존 캐싱된 데이터를 일시적으로 사용하다가 새로운 데이터를 가져오기 때문에 캐싱된 데이터와 새로운 데이터의 차이가 클 경우, 사용자가 업데이트되지 않은 정보를 보게 될 수도 있다.

 

 

2. queryClient.invalidateQueries

검색 시 useQuery를 사용해 캐싱기능을 사용하면서, 데이터를 재 요청할 경우에 따로 queryClient.invalidateQueries하는 함수를 생성하고 useEffect에서 재요청이 필요할 때 마다 queryClient.invalidateQueries를 호출해 기존 캐싱을 초기화 하고 최신 데이터로 업데이트 하는 방법

따로 함수를 생성하는 이유는 useMutation처럼 onSuccess에서 실행시키면 또 성공하여 queryClient.invalidateQueries를 실행시켜 무한 루프를 돌기 때문이다. (나도 알고싶지 않았다..)

개인적으로는 이 방법은 기존 캐싱을 초기화하고 새로 데이터로 업데이트 하는 것이기 때문에  refetch를 사용해 기존 캐싱 사용 + 새로운 데이터만 가져와 업데이트 하는 것보다 네트워크 트래픽이 더 발생하고, 데이터를 새로 받아오기 때문에 캐시된 데이터를 먼저 보여주는 refetch에 비해 속도가 더 느릴 것이라고 생각한다. 때문에 최신 업데이트될 데이터양이 그렇게 많지 않은 경우(즉 사용자가 이전 데이터를 보게될 확률이 낮을 경우) useQuery + refetch가 더 적합하다고 생각한다. 반면 데이터의 정확도나 일관성이 더 중요하다면 아예 새로 데이터를 보여주는  queryClient.invalidateQueries를 사용할 수도 있겠다.

 

3. useMutation 사용하기

useMutation을 일반적으로 데이터를 수정하거나 삭제하거나 변경해야 하는 경우에 많이 사용된다. 예를들어 http메소드로 말하자면 useQuery가 get요청에 주로 사용된다면 useMutation은 post, put, delete 등이라고 할 수 있다.

useMutation같은 경우는 캐싱처리를 하지 않는다. 목적 자체가 데이터를 변경할 목적의 hook이기 때문이다. 따라서 이를 사용해서 검색 기능을 처리하면 캐싱을 우회할 수 있다.

장점은 캐싱 기능을 우회하여 항상 서버로부터 최신 데이터를 받아와 데이터 업데이트를 즉시 반영할 수 있다는 점과 검색 요청에 대한 캐싱 관리가 필요하지 않기 때문에 불필요한 캐시 관련 문제를 피할 수 있다는 점이다.

단점은 캐싱 처리가 안되기 때문에 캐싱 기능을 직접 구현해야 하므로 코드가 복잡해질 수 있다. 또 캐싱 처리가 되지 않아 중복되는 요청이 많아지면 네트워크 트래픽이 증가하고 성능 저하가 발생할 수 있다.

개인적으로는 캐싱 처리를 직접 구현하는 것이 더 힘들고 useQuery에 비해 로직 구현이 번거롭다고 판단 하였고, 검색은 데이터를 조회하는 요청이기 때문에 useMutation은 선택하지 않았다.

* 네트워크 트래픽과 오버헤드의 차이 :
1) 네트워크 트래픽은 네트워크를 통해 전송되는 데이터의 양을 나타낸다. 네트워크를 통해 데이터를 주고받을 때, 데이터의 크기가 크면 더 많은 트래픽이 발생하게 된다. 네트워크 트래픽은 네트워크 대역폭을 소비하며, 데이터 전송 속도와 관련이 있다. 높은 네트워크 트래픽은 대역폭을 많이 차지하므로, 대역폭이 제한된 네트워크에서는 성능 저하를 초래할 수 있다.

2) 오버헤드는 추가적인 부담이나 비용을 의미한다. 네트워크 통신에서는 데이터 전송에 따른 오버헤드가 발생한다. 이는 데이터 전송을 위해 필요한 프로토콜 헤더, 에러 검사 비트, 라우팅 정보 등의 추가 정보를 포함한다. 오버헤드는 데이터 전송량에 비해 작지만, 전체 네트워크 성능에 영향을 줄 수 있다. 시간적 오버헤드 등(두 번 요청 시 기다리는 시간)

 

 

 

4. 실시간 업데이트 등 잦은 데이터 갱신이 필요할 경우

React Query의 캐싱 기능을 조정하거나 재설정하는 방법

1. staleTime 옵션을 사용하여 캐시의 유효기간을 설정하는 방법

2. queryInvalidationInterval 옵션으로 일정 간격으로 캐시된 데이터의 유효성을 주기적으로 갱신하기

const { data, isLoading, isError, refetch } = useQuery(
  'searchResults',
  () => {
    return fetchSearchResults(searchQuery);
  },
  {
    staleTime: 30000, // 30초 동안 캐시된 데이터를 사용
    queryInvalidationInterval: 60000, // 1분마다 서버에 재요청하여 캐시 갱신
  }
);

3. 차라리 소켓을 사용하는 것이 더 낫지 않을까 하는 생각도 든다.

 

 

알게 된 점,

오늘은 그 동안 바빠서 깊게 공부해 보지 못했던 리액트 쿼리에 대해 좀 더 깊게 고민해 본 시간이었다. 결론적으로는 내가 사용한 방법은 맞다고 판단되나 사용하기 전에 여러가지 방법들을 생각해 보고 분명한 차이를 알고 사용해야겠다는 생각이 들었다. 추가적으로 공식 문서를 보다 보니 내가 알지 못했던 옵션들도 발견하면서, 1. useQuery에서도 useMutation과 같이 onSuccess와 onError를 사용할 수 있다는 것도 깨달았다. 또 2. suspense라는 기능을 활용하면 내가 필요 시 컴포넌트마다 계속 반복하고 있는 isLoading과 isError 처리를 한 번에 할 수 있을 것 같다. 정말 유레카였다.. 이건 아직 사용해 보지 않았기 때문에 내일 이 suspense를 활용해 반복되는 로직을 줄이도록 리팩토링 해볼 예정이다. 역시 공식 문서를 잘 활용하도록 하자..!