클라이언트 상태는 웹 브라우저나 모바일 앱 내에서 관리되는 데이터를 의미한다.
이는 사용자의 입력, UI상태(ex. 드롭다운 메뉴가 열려 있는지), 특정 컴포넌트의 상태 등을 포함할 수 있다.
서버 상태는 원격 서버에 저장되어 있는 데이터를 의미한다.
이 데이터는 데이터베이스, 파일 시스템, 클라우드 스토리지 등의 백엔드 저장소에 보관되며, 클라이언트가 API를 통해 요청할 때마다 해당 데이터가 전달된다.
React Query는 React에서 비동기 데이터를 가져오고, 캐싱하고, 동기화하고, 업데이트하는 작업을 간소화하기 위한 라이브러리이다.
서버 상태는 클라이언트 상태와는 다르게 특정한 특성과 복합성을 가진다.
대부분의 웹 프레임워크는 데이터를 가져오고 업데이트하는 방식에 대한 "기본 가이드"나 "권장 사항"등의 완전한 솔루션을 제공하지는 않는다. 이로 인해 개발자들은 종종 데이터 패치에 대한 복잡한 메타 프레임워크나 자체적인 방법을 만들게 된다.
예를들어 React는 UI를 관리하는 방법에 중점을 둔 라이브러리로써, React 자체적으로 API 데이터 관리나 캐싱에 대한 구체적인 패턴을 제공하지 않는다. 따라서 개발자들은 Axios, Redux, MobX, React Query등과 같은 라이브러리나 도구를 사용하여 데이터를 가져오고 관리하는 방법을 정의하게 된다.
이러한 라이브러리는 클라이언트 상태 관리에는 유용하지만, 비동기나 서버 상태 관리에는 적합하지 않을 수 있다.
예를들어 많은 상태 라이브러리(Redux, MobX 등)은 클라이언트의 상태(UI상태 혹은 사용자 입력 등)을 관리하는 데 유용하게 설계되어 있지만 서버 상태(API에서 가져온 데이터 또는 다른 원격 소스에서 가져온 데이터)를 관리할 때 이러한 라이브러리는 여러 문제에 직면(비동기성, 캐싱, 데이터 동기화, 오래된 데이터에 대한 무효화)한다.
리액트 쿼리는 자동으로 데이터를 캐싱한다. 따라서 동일한 요청을 여러 번 수행할 때마다 서버에 다시 요청하지 않아도 된다.
중복 요청 방지: 사용자가 동일한 페이지나 컴포넌트를 반복해서 방문할 때마다 데이터를 서버에서 가져오는 것은 비효율적이다. React Query는 이전에 실행된 쿼리의 결과를 자동으로 캐시하여 동일한 쿼리가 다시 실행될 때 캐시된 데이터를 즉시 반환한다.
백그라운드 데이터 갱신: 데이터가 캐시에서 반환되더라도, React Query는 백그라운드에서 새로운 네트워크 요청을 실행하여 데이터를 최신 상태로 유지할 수 있다.
데이터의 생명주기 관리: React Query는 캐시된 데이터의 생명주기를 자동으로 관리한다. 데이터가 오래되거나 더 이상 사용되지 않으면, 해당 데이터는 자동으로 캐시에서 삭제된다.
성능 향상: 자동 캐싱 기능은 불필요한 네트워크 요청을 크게 줄여 웹 애플리케이션의 응답 시간을 단축시킨다. 이로 인해 사용자는 빠른 로딩 시간과 부드러운 인터랙션을 경험할 수 있게 된다!😀
초기 쿼리: useQuery를 사용하여 처음으로 ['todos']
키로 쿼리를 실행하면 네트워크 요청이 발생하여 데이터를 가져온다. 가져온 데이터는 ['todos']
키로 캐시에 저장된다.
데이터 상태 관리: 캐시에 데이터가 저장되면 해당 데이터는 즉시 "stale" (오래된) 상태로 표시된다. 이는 staleTime
의 기본값이 0이기 때문이다.
동일한 쿼리의 재실행: 같은 ['todos']
키로 쿼리를 다시 실행하면, 이전에 캐시된 데이터가 즉시 반환된다. 그리고 동시에 새로운 네트워크 요청도 발생하여 데이터를 최신 상태로 유지한다.
쿼리의 생명주기: 동일한 쿼리의 인스턴스가 모두 언마운트되면 해당 쿼리는 더 이상 활성 상태가 아니게 된다. 이때 cacheTime
(기본값 5분) 동안 해당 쿼리의 데이터가 캐시에 유지된다. 만약 이 시간 동안 동일한 쿼리가 다시 마운트되지 않으면, 캐시의 데이터는 삭제되고 가비지 컬렉션된다.
앱이 포커스되거나 네트워크가 다시 연결될 때 자동으로 데이터를 동기화한다. 이 기능은 앱의 데이터가 최신 상태를 유지하도록 도와준다.
백그라운드 동기화란? 🤔
사용자가 알지 못하는 상태에서, 즉 화면의 전면에서 직접적인 작업 없이 데이터를 업데이트하는 것을 말한다.
React Query에서의 백그라운드 동기화
앱 포커스: 사용자가 다른 탭이나 애플리케이션으로 이동했다가 원래의 애플리케이션 탭으로 돌아오면, React Query는 자동으로 해당 탭의 데이터를 최신 상태로 동기화한다. 이는 사용자가 항상 최신 데이터를 보게 되도록 보장한다.
네트워크 재연결: 인터넷 연결이 끊어진 상태에서 애플리케이션을 사용하다가 연결이 다시 복구되면, React Query는 네트워크 연결이 복구되자마자 데이터를 최신 상태로 동기화한다. 이렇게 하면 네트워크 연결 문제로 인해 데이터가 오래되거나 누락되는 것을 방지할 수 있다.
데이터 신선도 유지: 애플리케이션 내에서 중요한 데이터는 지속적으로 변할 수 있다. 백그라운드 동기화를 통해 React Query는 데이터의 신선도를 지속적으로 유지하고, 사용자에게 항상 최신의 정보를 제공한다.
중요한 이유
웹 애플리케이션에서 데이터의 신선도는 매우 중요하다. 예를 들어, 쇼핑 앱에서 상품의 재고나 가격 정보가 실시간으로 바뀔 수 있다. 이런 경우, 사용자는 항상 최신의 정보를 보기를 원할 것이다. 백그라운드 동기화는 이러한 요구를 만족시키기 위한 핵심 기능이다. 😀
또한, 애플리케이션 사용 중에 네트워크 연결이 끊길 수도 있다. 연결이 복구되었을 때, 자동으로 데이터를 동기화하는 기능은 사용자 경험을 크게 향상시킬 수 있다.
특정 데이터가 변경되었을 때 관련된 다른 데이터를 자동으로 업데이트하거나 다시 가져올 수 있다.
쿼리 무효화 (Query Invalidation)
특정 조건이나 액션에 의해 특정 쿼리의 캐시된 데이터가 더 이상 유효하지 않다고 판단되면 해당 쿼리는 '무효화'된다.
목적 : 데이터의 변경, 추가, 삭제 등의 액션 이후에 관련된 다른 쿼리의 데이터가 최신 상태가 아닐 수 있다. 이럴때 해당 쿼리를 무효화하여 데이터의 일관성을 유지할 수 있다.
리패칭 (Refetching)
무효화된 쿼리나 특정 조건을 만족하는 쿼리는 자동으로 '다시 패칭'된다. 즉, 원격 서버로부터 최신 데이터를 다시 가져온다.
개발자는 mutate
나 invalidateQueries
와 같은 메서드를 사용하여 필요한 쿼리를 무효화하고 리패칭할 수 있다고한다.
목적 : 무효화된 쿼리의 데이터를 최신 상태로 유지하고, 데이터의 일관성과 신선도를 보장하기 위해서이다.
사용 사례
예를 들어, "todos"
라는 쿼리를 사용하여 할 일 목록을 가져온 후, 사용자가 할 일을 추가하거나 변경하는 액션을 수행한다고 가정해 보자. 이 액션 이후에는 "todos"
쿼리의 캐시된 데이터가 더 이상 최신 상태가 아니다.
React Query에서는 이런 상황에서 "todos"
쿼리를 자동으로 무효화하고 리패칭할 수 있다. 따라서 사용자는 항상 최신의 todo 목록을 볼 수 있다.😀
페이지네이션 및 무한 스크롤링과 같은 복잡한 UI 패턴을 쉽게 구현할 수 있도록 지원한다.
(1) 페이징 (Pagination)
데이터셋을 여러 페이지로 분할하여 한 번에 한 페이지의 내용만 사용자에게 보여주는 방법이다. 일반적으로 페이지 번호나 '이전', '다음'과 같은 네비게이션 요소를 통해 다른 페이지로 이동할 수 있다.
React Query에서 사용하기
React Query는 usePaginatedQuery
훅을 제공하여 데이터의 페이징 처리를 지원한다. 이 훅은 각 페이지의 데이터를 자동으로 캐시하고, 사용자가 페이지를 전환할 때 캐시에서 데이터를 가져오거나 새로운 데이터를 요청한다.
(2) 무한 스크롤링 (Infinite Scrolling)
사용자가 페이지의 끝에 도달하면 자동으로 추가 데이터를 로드하는 UI패턴이다. 이 방법은 전통적인 페이지 네비게이션 요소 없이 연속적인 데이터 스크롤링 경험을 제공한다.
React Query에서 사용하기
React Query는 useInfiniteQuery
훅을 제공하여 무한 스크롤링을 지원한다. 이 훅은 스크롤 위치, 로드할 추가 데이터의 페이지 번호 등의 정보를 추적하여 적절한 시점에 새로운 데이터를 요청하고 이를 기존 데이터에 추가한다.
특정 쿼리의 동작을 정확히 제어하고자 할 때 다양한 설정과 옵션을 제공한다.
React Query는 많은 설정과 옵션을 제공하여 개발자가 웹 애플리케이션의 특정 요구 사항과 환경에 맞게 쿼리의 동작을 세밀하게 제어할 수 있다.
주요 옵션과 설정
staleTime: 데이터가 "stale" (오래된) 상태가 되기 전까지 얼마나 지속될지를 지정한다. 이 값을 통해 얼마나 자주 데이터를 백그라운드에서 새로 가져올지 제어할 수 있다.
cacheTime: 쿼리 데이터가 캐시에서 유지되는 시간을 지정한다. 이 시간 이후에는 해당 데이터가 가비지 컬렉션된다.
retry: 쿼리가 실패했을 때 재시도하는 횟수를 지정한다.
retryDelay: 재시도 사이의 지연 시간을 지정한다.
onSuccess, onError, onSettled: 쿼리의 수명주기에 따른 콜백 함수를 설정할 수 있다.
예를 들어, 데이터를 성공적으로 가져온 후 특정 액션을 수행하거나, 에러 발생 시 알림을 표시하려면 이러한 콜백을 사용할 수 있다.
refetchOnWindowFocus: 윈도우 포커스가 변경될 때 쿼리를 자동으로 리패칭할지 여부를 설정한다.
manual: 이 옵션을 true로 설정하면 쿼리가 자동으로 실행되지 않는다. 대신 개발자는 수동으로 쿼리를 실행할 수 있다.
pagination: 페이지네이션된 쿼리를 위한 설정을 제공한다. 예를 들어, 각 페이지의 크기나 현재 페이지 번호 등의 정보를 지정할 수 있다.
backgroundSyncing: 백그라운드에서 데이터를 동기화할지 여부를 설정한다.
개발 중에 쿼리의 상태와 동작을 확인할 수 있는 강력한 개발 도구를 포함한다.
사용법
React Query Devtools를 프로젝트에 포함하려면, 개발 의존성으로 설치하고 애플리케이션의 주요 컴포넌트 (예: App 컴포넌트)에 추가하면 된다. 일반적으로 Devtools는 개발 모드에서만 포함되며, 프로덕션 빌드에서는 제거된다.
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
function App() {
return (
<QueryClientProvider client={queryClient}>
{/* The rest of your application */}
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
)
}
initialIsOpen
를 true
로 설정!주요 기능
쿼리 상태 감시: Devtools를 사용하면 현재 실행 중이거나 캐시된 모든 쿼리의 상태를 실시간으로 확인할 수 있다. 쿼리의 데이터, 오류, 상태 (로딩, 성공, 실패 등) 및 다른 메타데이터를 쉽게 볼 수 있다.🧐
쿼리 제어: 개별 쿼리를 직접 리패칭하거나 캐시에서 제거하는 등의 액션을 수행할 수 있다.
쿼리 데이터 조작: 캐시된 데이터를 직접 수정하여 애플리케이션의 동작을 테스트할 수 있다.
설정 및 옵션 확인: 현재 애플리케이션의 React Query 설정과 사용 중인 다양한 옵션을 확인할 수 있다.
무효화 및 가비지 컬렉션: Devtools를 사용하여 특정 쿼리를 수동으로 무효화하거나 가비지 컬렉션을 트리거할 수 있다.
UX 통합: React Query Devtools는 애플리케이션의 UI에 손쉽게 통합될 수 있으며, 필요에 따라 토글하여 표시하거나 숨길 수 있다.
react-query 설치
yarn add @tanstack/react-query
devtools 설치
yarn add @tanstack/react-query-devtools
React Query를 사용해서 GitHub 리포지토리에 대한 정보를 가져오기
// 전체코드
import {
QueryClient,
QueryClientProvider,
useQuery,
} from '@tanstack/react-query'
const queryClient = new QueryClient()
export default function App() {
return (
<QueryClientProvider client={queryClient}>
<Example />
</QueryClientProvider>
)
}
function Example() {
const { isLoading, error, data } = useQuery({
queryKey: ['repoData'],
queryFn: () =>
fetch('https://api.github.com/repos/TanStack/query').then(
(res) => res.json(),
),
})
if (isLoading) return 'Loading...'
if (error) return 'An error has occurred: ' + error.message
return (
<div>
<h1>{data.name}</h1>
<p>{data.description}</p>
<strong>👀 {data.subscribers_count}</strong>{' '}
<strong>✨ {data.stargazers_count}</strong>{' '}
<strong>🍴 {data.forks_count}</strong>
</div>
)
}
import {
QueryClient,
QueryClientProvider,
useQuery,
} from '@tanstack/react-query'
const queryClient = new QueryClient()
export default function App() {
return (
<QueryClientProvider client={queryClient}>
<Example />
</QueryClientProvider>
)
}
function Example() {
const { isLoading, error, data } = useQuery({
queryKey: ['repoData'],
queryFn: () =>
fetch('https://api.github.com/repos/TanStack/query').then(
(res) => res.json(),
),
})
if (isLoading) return 'Loading...'
if (error) return 'An error has occurred: ' + error.message
return (
<div>
<h1>{data.name}</h1>
<p>{data.description}</p>
<strong>👀 {data.subscribers_count}</strong>{' '}
<strong>✨ {data.stargazers_count}</strong>{' '}
<strong>🍴 {data.forks_count}</strong>
</div>
)
}
['repoData']
로 지정한다.✍️ 이 코드를 통해, React Query를 사용하여 API로부터 데이터를 가져오고, 해당 데이터의 로딩 상태나 오류 상태 등을 쉽게 관리할 수 있음을 볼 수 있다! 😀
reference)