React Query
April 10, 2023
React
React Query
컴포넌트를 사용해 중복된 요소들을 쉽게 표현할 수 있지만, 컴포넌트는 자료의 재사용이 목적이 아니므로 컴포넌트에 서버에 api 요청을 하는 로직이 추가 되어 있는 경우, 개발자의 의도와는 다르게 과도한 요청이 일어날 수 있다. 이런 문제는 Query를 사용하여 비교적 쉽게 해결할 수 있다.
위 그림은 Query의 메인페이지 인데 그림에서 볼 수 있듯이 쿼리는 상태관리와 패칭, 비동기 코드들을 관리 한다. 사용자들은 쿼리를 주로 패칭, 캐싱, 비동기와 서버상태의 업데이트를 위해 사용한다. 쿼리를 이용하면 컴포넌트의 중복된 패치 요청을 막을 수 있는데 이는 쿼리가 요청한 자료를 캐싱하여 재요청 여부를 결정하기 때문이다.
Quick Start
쿼리의 사용법은 기타 상태관리 library와 비슷하다.
import { useQuery, useMutation, useQueryClient, QueryClient, QueryClientProvider, } from '@tanstack/react-query' // Create a client const queryClient = new QueryClient() function App() { return ( // Provide the client to your App <QueryClientProvider client={queryClient}> <Todos /> </QueryClientProvider> ) }
queryClient
를 만들어 주고, 이를 적용될 범위에 부모요소로 감싸줘 스코프 설정을 한다. 이렇게 되면 일단 쿼리를 사용할 준비는 끝난다. 그 뒤 쿼리를 사용할 컴포넌트에서 useQuery
를 사용해 패치해준다.
const {isLoading, error, data} = useQuery(['data'], ftn)
useQuery
는 많은 정보를 제공하지만 여기서는 로딩중인지, 에러가 났는지, 그리고 데이터를 받아온다. useQuery
함수에는 배열 형태인 키와 패칭 함수를 전달인자로 받는다. 쿼리는 이제부터 패치한 데이터를 키를 기준으로 관리하게 된다. 동일 키를 가진 데이터에 대한 패치요청이 다시 발생하게 되면, 캐시된 데이터를 우선적으로 가져오는 방식이라 생각하면 된다.
Important Defaults
리액트 쿼리가 캐싱과 패칭을 관리해주는것은 사용자에게 굉장히 유익한 경험을 준다. 하지만 이런 경우 '어떤 키'로 패칭한 데이터에 대한 리패칭 이슈가 있을 수 있다.
한번 패칭이 일어난 데이터는 그 즉시 오래된(staled) 데이터로 간주되기 때문에 다음 요청에 바로 패칭이 일어나게 된다. (동일 렌더링에서는 한번만 요청한다.) 때문에 사용자가 의도한대로 선능 개선이 일어나지 않을 수 있으며, 상황에 따라 쿼리를 사용하기 전보다도 더 많은 api요청이 발생할 수도 있다. 따라서 패칭에 대한 관리도 사용자의 몫이 된다. 이를 위한 방법은 공식문서에 잘 나와있지만 여기에서는 가장 간단한 방법인 오래된(staled) 데이터로 간주하는 최소 시간을 설정해 주는 방법만 간단하게 소개한다.
const {isLoading, error, data} = useQuery(['data'], ftn, {staleTime:1000*60*1})
위와 같이 패칭 함수 뒤에 객체 형태로 조건을 넘겨줄 수 있는데, 쿼리는 이 조건에 따라 패칭 여부를 판단하게 된다.
리액트 쿼리와 상태관리
리액트는 그 자체적으로 데이터 패칭 기능을 제공해 주지 않기 때문에, 개발자가 직접 서버의 데이터를 가지고와 관리하는 방법을 강구해한다. 이는 보통 서버에서 가지고 온 데이터를 일반적인 상태관리 툴과 함께 사용하는 방법을 이용한다. 하지만 서버 데이터는 외부 데이터를 가지고 오는 과정을 거치므로 리액트 상태와는 달리 일반적인 상태관리 툴로 다루기에 적합하지 않다. 따라서 실시간으로 업데이트 되는 서버 상태를 클라이언트와 효과적으로 동기화 하기 위해 Query를 이용한다.
공식 문서에 나온 방법과 같이 react Query 또한 다른 상태관리도구들과 같이 스코프 설정을 해서 서버로부터 가지고 온 데이터를 제공할 컴포넌트들을 정한다.
function App() { return ( // Provide the client to your App <QueryClientProvider client={queryClient}> <Todos /> </QueryClientProvider> ) }
따라서 <QueryClientProvier>로 스코프 설정 안에 들어있는 컴포넌트들은 query가 받아온 데이터를 공유하게 된다. 이같은 이유로 원한다면 리액트 상태들도 Query의 스코프 설정안에서 공유되어 Query를 전역상태도구로 사용할 수도 있지만 react Query는 서버 상태를 관리하는 목적으로 최적화 됐기 때문에 그 용도에 맞지 않다. react Query는 키를 통해 데이터를 관리하게 된다.
import { getTodos } from '../my-api' const query = useQuery('todos', getTodos)
위 예시에서 query에 담긴 데이터는 'todos'라는 키와 함께 캐시에 저장되어 관리된다. 키와 함께 효율적인 리프레시 설정하여 성능 향상을 가지고 올 수 있지만 반대로 캐시된 데이터 때문에 오히려 데이터 동기화 문제가 생길 수 있다. 또한 react Query는 전역상태를 관리하는 목적으로 만들어 진게 아니기 때문에 Redux의 reducer나 action과 같은 전역상태 관리에 편의를 주는 기능들이 없다. 또한 서버 데이터 관리를 목적으로 만든만큼 전역상태를 관리할 때 성능측면에서도 좋지 않다고 하니 굳이 react Query를 고집하여 전역상태를 관리할 필요는 없다.
쿼리 무효화 전략
앞서 query에 담긴 데이터가 특정 키와 함께 캐시에 저장되어 데이터 동기화 문제를 야기한다고 했다. 이 점을 보완하기 위해서 특정 키와 일치하는 모든 쿼리를 무효화 한다. 무효화된 쿼리는 캐시에서 지워지지는 않지만 무효화된 쿼리를 대신할 새로운 쿼리를 구성하기 위해 리패치 과정이 유도되며 이 과정에서 리액트 컴포넌트의 적절한 리렌더링이 일어난다.
useQuery가 데이터를 가지고 오는 일은 하는 hook이라면, 서버 데이터에 변화를 주는 일에는 useMutation이 있다. 목적이 명확한 이 두 훅을 사용하면 앱에서 데이터를 가지고 오거나 변화를 주는 코드를 구분하기 쉽게하고 코드 유지보수를 쉽게 만들어준다. 또한 useMutation의 콜백함수인 onSuccess는 mutation이 성공적으로 이뤄졌을 때 호출되는데 이 콜백함수를 이용해 특정 queiry의 키를 무효화 할 수 있다. invalidateQueries를 사용하면 서버에서 데이터가 변경되었을 때 해당 변경 사항을 반영하도록 클라이언트 캐시를 새로 고칠 수 있다. 예를 들어, 데이터를 업데이트하는 뮤테이션을 실행한 후 해당 데이터에 대한 쿼리를 무효화하면 변경 사항이 반영된 새로운 데이터를 가져올 수 있다.
Query Custom Hook
여느 훅과 같이 Query hook으로 같은 작업은 여러 컴포넌트에서 실행하는 경우가 있다. 특히 Query hook 같은 경우는 서버에서 데이터를 가지고 오는 작업이므로 react hooks보다 클라이언트 여러곳에서 더 빈번하게 사용되는 경우가 흔하다. 또한 Query hook은 위에서 본것과 같이 key를 통해 data fetching을 결정하므로 key를 관리하는 측면에서도 여러 곳에서 반복되어 사용되는 Query hook을 하나로 관리할 필요가 있다.