React Query 파보기

차차·2024년 1월 2일
8

react study

목록 보기
3/5
post-thumbnail

이전 두번의 팀 프로젝트에서 React query 를 사용했었다. 하지만 부족한 이해도로 인해 '이게 되네?' 또는 '이게 왜 안돼?' 하는 상황이 종종 있었다.
이 친구를 다시 사용하게 될 미래를 위해, 제대로 사용하기 위한 개념과 사용 패턴들을 차근차근 정리해보고자 한다 !


서버 상태에 대해


Server State VS Client State

클라이언트 상태 (Client State)

  • 클라이언트가 자체 생성한 상태, 서버 데이터에 종속되지 않음
  • 사용자의 input 값, toggle 유무와 같은 값들

서버 상태 (Server State)

  • 서버 DB 에 저장되고 관리되는 데이터
  • 여러 사람에 의해 조작될 수 있음
  • 클라이언트 측 코드에 의해 직접적으로 처리되거나 조작될 수 없음
    그치만 클라이언트에 표시되어야 함
  • 비동기적으로 가져오고 업데이트 해줘야 함

상태 관리

클라이언트 상태 (Client State)

  • React 내장 hook (useState, useReducer …)
  • 상태 관리 라이브러리 (redux, mobx, zustand …)

서버 상태 (Server State)

  • React-query 이전
    • 상태 관리 라이브러리 사용 = 비동기로 이루어지는 서버 상태 관리에는 적합하지 않음
    • 개발자가 직접 서버 상태(loading, error, pending…)를 핸들링하기 위한 로직을 구현 했어야 함 ⇒ 예를 들면 try-catch
  • React-query (tanstack-query)
    • 서버 상태 관리 라이브러리
    • 캐싱 / 동일 데이터에 대한 요청 중복 제거 / 백그라운드 상에서 오래된 데이터 업데이트 / 서버 데이터 업데이트 빠르게 반영 / …


staleTime & gcTime


staleTime

  • number | Infinity default = 0
  • 데이터가 fresh(최신) 에서 stale(오래된) 한 상태로 되는 데에 걸리는 시간
    • staleTime = 5000 라면, 5초 뒤에는 stale 한 데이터로 판정
  • fresh 상태일 경우, 쿼리 인스턴스가 새로 생성되어도 네트워크 요청이 발생하지 않음.

gcTime

  • number | Infinity default = 5분
  • 사용되지 않는 데이터의 캐시를 유지하는 시간
  • 데이터를 사용하지 않거나, inactive 일 때 캐싱되어 있는 시간
    • 쿼리 인스턴스가 unmount 되면 inactive 상태로 전환
    • 이후 gcTime 만큼 캐시 유지
    • gcTime 지나면 garbage collector 행

Life Cycle

1️⃣ 해당 페이지 진입, 브라우저 재포커싱 ⇒ 컴포넌트 mount

2️⃣ 쿼리 인스턴스 mount (pending)

3️⃣ staleTime 이 지났는가? (stale 한 데이터인가?)

  1. 안지남 ⇒ fresh 하기 때문에 데이터 업데이트 X = 캐시에서 가져옴
  2. 지남 ⇒ stale 하기 때문에 데이터 업데이트 (fetching)
    1. gcTime 이 지났는가? (캐싱된 데이터가 있는가?)
      1. 지남 ⇒ 처음부터 새로 가져옴 (loading)
      2. 안지남 ⇒ 일단 캐시된거 로드 ⇒ 업데이트

이걸 도식화해보면 아래와 같다!

😳 그동안 잘 모르고 사용했거나, 헷갈리는 부분 정리

  • staleTime 의 default 값은 0 이기 때문에, 기본적으로 캐시 기능을 사용하고 있지 않다고 보면 된다. 쿼리 인스턴스가 mount가 되면, 무조건 데이터 업데이트가 일어나기 때문이다.

  • 그렇다면 staleTime 이 0 일때 캐시의 역할은, 데이터를 업데이트할 때 빈화면이나 로딩화면이 아닌 이전 데이터를 보여주기 위함이다.

  • isLoading은 캐싱된 데이터가 없을 때, 즉 초기 데이터일 때만 true 이다. 따라서 isLoading 을 무조건적인 로딩UI의 trigger로 쓰는 것은 좋지 않다.

  • 쿼리 인스턴스가 반환하는 refetch 함수를 호출하게 될 경우, stale/fresh에 관계 없이 데이터 업데이트가 이루어진다. 따라서 이 함수에 의존하는 것은 리액트 쿼리 기능에 위배된다.

  • 캐시된 데이터에 무관하게 즉시 업데이트를 원한다면, invalidateQuery 를 통해 해당 쿼리의 캐시를 초기화시켜야 한다.

staleTime > gcTime ?

staleTime 이 지나지 않으면 데이터 패칭을 하지 않으며, 그렇게 되면 캐시도 쳐다보지 않기 때문에 캐싱이 의미가 없음

그렇다면 무조건 staleTime < gcTime ?

자주 사용하지 않는 데이터의 경우, staleTime 을 길게 설정할 수 있다. 이러한 경우 gcTime 을 굳이 더 길게 잡으면 메모리를 낭비하게 된다.

staleTime vs cacheTime · TanStack/query · Discussion #1685



사용 패턴


Polling

실시간 처리, 특정 주기를 가지고 서버와 응답을 주고 받는 방식

  • refetchInterval
    특정 시간마다 자동으로 refetch

  • refetchIntervalInBackground
    브라우저 탭/창이 백그라운드에 있는 동안에도 (focus 되지 않아도) refetch

  • refetchOnWindowFocus
    브라우저 탭/창이 focus 될 때 refetch

1. refetchInterval In Background

const { data, ... } = useQuery({
  ...queryOption,
  refetchInterval: 2000,
  refetchIntervalInBackground: true,
});

2. refetch On Window Focus

const { data, ... } = useQuery({
  ...queryOption,
  refetchInterval: 2000,
  refetchOnWindowFocus: true
});

PlaceholderData

쿼리가 pending 인 동안 해당 쿼리의 데이터로 반환됨

  • placeholderData = 서버 데이터랑 관련 없는, 보여주기용 가짜 데이터

  • 캐시에 유지되지 않음 (initialData 와의 차이점)

  • pending = 쿼리 인스턴스에 데이터가 없는 상태

    • placeholderData 가 있다면 pending 이 false 이기 때문에,
      이를 위한 상태값인 isPlaceholderData 를 사용할 수 있음

1. 단순 값을 placeholderData 에 할당하기

const result = useQuery({
  queryKey: ['todos'],
  queryFn: () => fetch('/todos'),
  placeholderData: placeholderTodos,
})

2. 함수를 placeholderData 에 할당하기

  • 함수 : (이전 쿼리 데이터, 이전 쿼리 인스턴스) => 이전 쿼리 데이터

  • 하나의 쿼리([’todos’, 1])에서 가져온 데이터를 다른 쿼리([’todos’, 2])의 데이터로 사용

  • pagination 기능을 구현할 때 사용

    • 각 페이지/커서마다 쿼리 키가 달라지고, 새로운 쿼리가 생성되기 때문에 로딩 화면/깜빡임이 발생함
    • 이를 위해 placeholderData 에 이전 쿼리 데이터를 할당하여 사용자 경험 개선
const result = useQuery({
  queryKey: ['todos', id],
  queryFn: () => fetch(`/todos/${id}`),
  placeholderData: (previousData, previousQuery) => previousData,
})
import { keepPreviousData, ... } from '@tanstack/react-query'

const result = useQuery({
  queryKey: ['todos', id],
  queryFn: () => fetch(`/todos/${id}`),
  placeholderData: keepPreviousData
})

InitialData

쿼리 초기 데이터를 캐시에 제공

  • isLoading = 캐시에 데이터가 없어서 API 요청을 새로 하는 상태

    • isLoading 상태일 때, initialData 에 할당된 값이 캐시에 채워짐
    • 쿼리를 미리 채우고, 초기 로딩을 건너뛸 수 있음
  • iniaitlData 는 기본적으로 fresh 데이터로 처리되기 때문에, staleTime 로직에 영향을 줌

    • initialDataUpdatedAt 옵션에 time stamp 값을 할당하여 initialData 가 업데이트된 시간을 지정해줄 수 있음
    • initialData 에 함수를 전달하여 한번만 실행되게 할 수 있음

1. 전체 데이터를 상세 데이터 초기값에 할당하기

  • 전체 리스트 페이지에서 상세 페이지로 접속할 때,
    같은 데이터를 사용함에도 똑같이 쿼리 작업이 수행되는 부분을 해결하기 위함

  • 상세 페이지에서는 쿼리 작업을 하지 않고 앞에서 구한 데이터를 가져와서 쓰기
    ⇒ 네트워크 사용량 감소

const result = useQuery({
  queryKey: ['todo', todoId],
  queryFn: () => fetch(`/todos/${todoId}`),
  initialData: () =>
    queryClient.getQueryData(['todos'])?
							 .find((d) => d.id === todoId),
})


이어서

글이 너무 길어져서, 다시 돌아오겠숩니다..!

  • 남은 사용 패턴들
    • Parallel, Dependent, Optimistic Update, Prefetch …
  • 쿼리 동작 원리 (그래서 캐싱은 어떻게 하는 걸까)

2개의 댓글

comment-user-thumbnail
2024년 1월 3일

정리한 지식 호로록 흡수할게요

답글 달기
comment-user-thumbnail
약 23시간 전

까먹을 때마다 계속 보고 있어요.

답글 달기