React Query와 상태관리 :: 2월 우아한테크세미나 정리

Ziggy Stardust·2025년 2월 2일
0

다음 영상을 보고 정리했습니다.
React Query와 상태관리 :: 2월 우아한테크세미나

상태

주어진 시간에 대해 시스템을 나타내는 것으로 언제든지 변경될 수 있다.

개발자에겐 관리해야할 데이터

생태계 흐름

프론트엔드라는 분야가 구분될만큼 사용자에게 보여지는 부분이 많아졌다.

관리해야할 상태가 더 많아짐

안그래도 계속해서 변경되는 상태가 양까지 많아졌다.

마주친 문제

연사님이 당시에 마주한 문제는 전역 상태 Store 에서 상태관리보다는 API 통신 코드가 많았다는 것이다.

  • API 통신 관련 코드가 Store 에 존재
  • 반복되는 isFetching, isError 등 API 관련 상태

서버로부터 받는 상태

이러한 현상을 유발하는 상태들의 특성을 다시 정리해야했다.

  • Client 에서 제어하지 못하고 소유하지 못한 원격 장소에서 관리되고 유지됨
  • 이 상태를 가져오기 위해 비동기 API 호출이 필요함 (Fetching, Updating)
  • 사용자가 모르는 사이에 변경될 가능성이 존재 (관리되는 쪽이 오픈된 원격)
  • 신경 쓰지 않는다면 잠재적으로 out of date 가 될 가능성이 존재

이래서 원격으로부터 받아온 상태는 일종의 캐시처럼 존재한다.

클라이언트 상태와 서버 상태

상태의 소유권으로 상태를 구분할 수 있게 되었다.
그 특성을 정리하자면 아래와 같다.

클라이언트 상태 (소유권이 클라이언트에게)

  • 클라이언트에서 소유하며 온전히 제어가능
  • 초기값 설정이나 조작에 제약사항 없음
  • 다른 사람들과 공유되지 않으며 클라이언트 내에서 UI/UX 혹은 사용자 인터렉션에 의해 변경될 수 있음
  • 항상 최신 상태로 관리됨

서버 상태 (소유권이 서버에게)

  • 원격에서 상태가 관리됨
  • 상태를 가져오기 위해 비동기 API 호출이 필요
  • 사용자가 모르게 원본 상태가 변경될 수 있음
  • out of date 가능성이 높음

React Query

이러한 문제를 해결하기 위해 나온 라이브러리가 React Query

React Query 홈페이지에선 다음과 같이 설명한다.

Performant and powerful data synchronizatioiin for React

효과적이고 강력한 데이터 동기화툴

Fetch, cache and update data in your React and React Native applications all without touching any "global state"

리액트 환경에서 전역 상태를 건드리지 않고 캐시를 동기화한다.

원격에 있는 상태를 클라이언트와 동기화시켜주는 기술이라 볼 수 있다.

특징적으로는 선언적이며 React Hook 과 유사한 인터페이스로 친숙함, 그리고 Zero Configuration 으로 간편하고 기능적으로 강력하다.

공식 예제 코드

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 { isPending, error, data } = useQuery({
    queryKey: ['repoData'],
    queryFn: () =>
      fetch('https://api.github.com/repos/TanStack/query').then((res) =>
        res.json(),
      ),
  })

  if (isPending) 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>
  )
}

React Query 를 사용하려면 QueryClientProvider 는 필수

import {
  QueryClient,
  QueryClientProvider,
  useQuery,
} from '@tanstack/react-query'

const queryClient = new QueryClient()

export default function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <Example />
    </QueryClientProvider>
  )
}

Core Concept

  • Queries
    data fetching, 즉 Read 담당. Get API 에 자주 사용

    A query is a declarative dependency on an asynchronous source of data that is tied to a unique key.

    라고 설명하는데 unique key 를 이용해 데이터를 비동기적으로 가져오는 의존성이라는 설명입니다. 서버로부터 값을 가져오기위한 Promise 기반 method 에 사용 가능하며 서버의 값에 수정을 가할 수 있는 작업은 Mutation 을 사용하길 권장합니다.

    Query 는 key, Value 구조를 연상할 수 있고 Query Key 에 따라 query caching 을 관리합니다.
    Query Key 는 String, Array 형태로 존재할 수 있습니다.


    useQuery 의 반환값
    data : 마지막으로 성공한 resolved된 데이터 (Response)
    error : 에러가 발생했을 떄 반환되는 객체
    isFetching : Request 가 in-flight 중일 때 true
    status, isLoading, isSuccess, isLoading 등 : 현재 query 의 상태
    refetch : 해당 query refetch 하는 함수 제공
    remove : 해당 query cache 에서 지우는 함수 제공
    기타 등등 : 이 있닷.


    useQuery 의 세 번째 인자로 커스텀 옵션을 줄 수 있다.
    onSuccess, onError, onSettled : query fetching 성공 / 실패 / 완료 시 실행할 Side Effect 정의
    enabled : 자동으로 query 를 실행시킬지 말지 여부
    retry : query 동작 실패 시 , 자동으로 ㄱetry 할지 결정하는 옵션
    select : 성공 시 가져온 data 를 가공해 전달
    keepPreviousData : 새롭게 fetching 시 이전 데이터 유지 여부
    refetchInterval : 주기적으로 refetch 할지 결정하는 옵션 (폴링 시 유리)
    기타 등등
const todo = useQuery('todo', () => {
  return axios.get('/todos')
});
  • Mutations
    Create, Update, Delete 를 위한 의존성

    Unlike queries, mutaions are typically used to create/update/delete data or perform server side-effects. For this purpose, React Query exports a useMutation hook.

    Query 와 달리 서버의 데이터 생성, 수정, 삭제를 위해 사용하는 의존성

    key 필요 없이 호출 가능


    반환값
    mutate : mutation 을 실행하는 함수
    mutateAsync : mutate 와 비슷하지만 Promise
    reset : mutation 내부 상태 clear
    나머지는 Query 와 비슷

    옵션
    onMutate : Mutation 동작 전 먼저 동작하는 함수, Optimistic update 적용 시 유용
    나머지는 Query와 비슷

    Optimistic update 는 좋아요처럼 성공을 낙관적으로 판단해 API 응답 전에 성공 처리하는 기법 , 롤백 가능

const mutation = useMutation(newTodo => {
  return axios.post('/todos', newTodo)
});
  • QueryInvalidation
    queryClient 가 invalidate 메소드를 호출해서 처리
    이러면 해당 Key 를 가진 query 는 stale 취급되고, 현재 rendering 되고 있는 query 들은 백그라운드에서 refetch 됩니다.
// Invalidate every query in the cache
queryClient.invalidateQueries();
// Invalidate every query with a key that starts with `todos`
queryClient.invalidateQueries('todos')

Caching 과 Synchronization

위 자료에선 Opton 에 cacheTime, staleTime 이 있었다.
refetchOnWindowFocus, refetchOnMount 같은 것도 있었다.

React Query 의 refetch 메커니즘은 RFC 5861 의 개념을 참고하고 있습니다.
RFC 5861 (HTTP Cache-Control Extensions for Stale Content)

  • stale-while-revalidate
    백그라운드에서 stale response 를 revalidate 하는 동안 캐시가 가진 stale response 를 반환
Cache-Control: max-age=600, stale-while-revalidate=30

이렇게 동작하면 Latency(지연시간) 가 숨겨진다.
(stale-if-error 도 이 명세에 존재, 요건 실패 시 이전 값 제공)

요약하자면 max-age=600, stale-while-revalidate=30 인 경우 캐싱 중인 값이 600초가 넘으면 새로운 값을 가져오는데 이때 stale 기간 30초까진 이전 값을 제공하며 백그라운드에서 새롭게 값을 가져온다는 뜻입니다.

이런 컨셉을 메모리 캐시에도 적용한다. (react-query, swr, etc)

  • cacheTime : 메모리에 얼마만큼 있을건지 (해당 시간 이후 GC 에 의해 처리, default 5분)

  • staleTime : 얼마의 시간이 흐른 후에 데이터를 stale 취급할 것인지 (default 0)

  • refetchOnMount, refetchOnWindowFocus, refetchOnReconnect 들이 true 일 경우 Mount, window focus, reconnect 시점에 data 가 stale 이면 모두 refetch (모두 default true)

Query 상태흐름

활성화를 고려할 경우 (꺼졌다, 켜졌다하는 UI)

표시되진 않을 때 inactive 가 되며 다시 활성화되면 상태에 따라서 동작합니다. stale 상태라면 refetch 가 동작할 수 있습니다.

zero-config

관련 기본값
staleTime -> 0 (Queries 에서 cached data 는 언제나 stale 취급)
refetchOnMount, refetchOnWindowFocus, refetchOnReconnect -> true (각 시점에서 stale 이라면 항상 refetch)
cacheTime -> 60 5 1000 (inActive Query 들은 5분 뒤 GC 에 의해 처리)
retry -> 3 (Query 실패 시 3번까지 retry)
retryDelay -> exponential backoff function

궁금한 점

  • Q ) 전역상태처럼 관리되는 것 같은데 Component A, Component B 에서 중복적으로 호출 시 어떻게 되나?
    A ) 키가 동일하니 staleTime 이 유효하다면 refetch 동작을 안해 (뒤에 실행하는 게 무효화)
profile
spider from mars

0개의 댓글