ReactQuery와 상태관리

걸음걸음·2023년 5월 2일
0

React

목록 보기
8/9

공부에 참고한 배민 라이브 강의(우아한테크세미나)

상태란?

주어진 시간에 대해 시스템을 나타내는 것
언제든지 변경될 수 있음
문자열, 배열, 객체 등의 형태로 응용 프로그램에 저장된 데이터

개발자에게 상태 : 관리해야 하는 데이터들

모던 웹프론트엔드 개발

UI/UX의 중요성과 함께 프로덕트의 규모가 커짐, FE에서 수행하는 역할이 늘어남
관리하는 상태가 많아짐

상태관리는?

상태를 관리하는 방법에 대한 것
상태들은 시간에 따라 변화함
REact는 단방향 바인딩, Props Drilling 이슈가 존재
Redux나 MobX 같은 라이브러리를 활용해 해결하는 방법도 있음

  • 상태관리 영역이 서버값을 저장하는 데에까지 확장
    API 통신 관련 코드가 Store에 있는 현상, 반복되는 isFetching, isError 등 API 관련 상태, 비슷한 구조의 API 통신 코드 등
  • 서버에서 받아야 하는 상태들의 특성
    Client에서 제어하거나 소유되지 않은 원격의 공간에서 관리되고 유지됨
    Fetching이나 Updating에 비동기 API가 필요함
    다른 사람들과 공유되는 것으로 사용자가 모른ㄴ 사이에 변경될 수 있음
    신경 쓰지 않는다면 잠재적으로 "out of date"가 될 가능성을 지님
    --> FE에서 이 값들이 저장되어있는 state는 일종의 캐시

상태를 두가지 상태로 나눌 경우

Client StateServer State
Client에서 소유, 온전히 제어 가능Client가 아닌 다른 원격의 공간에서 관리되고 유지
초기값 설정이나 조작에 제약사항 없음Fetching/Updating에 비동기 API 필요
다른 사람과 공유되지 않음.
Client 내에서 UI/UX흐름이나 사용자 인터렉션에 따라 변할 수 있음
다른 사람과 공유됨. 사용자가 모르는 사이 변경될 수 있음
항상 Client에서 최신 상태로 관리잠재적으로 "out of date"가 될 가능성이 있음

React Query

공식문서
fetch, caching, update 등의 비동기 로직을 지원하는 fetching 라이브러리.
zero-config로 즉시 사용 가능(config로 커스텀 가능)

  • React에서 사용하려면 QueryClientProvider 필수 사용
function App () {
  return <QueryClientProvider client={queryClient}>...</QueryClientProvider>
}

공식 예제

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, isFetching } = useQuery({
    queryKey: ["repoData"],
    queryFn: () =>
      axios
        .get("https://api.github.com/repos/tannerlinsley/react-query")
        .then((res) => res.data),
  });
  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>{isFetching ? "Updating..." : ""}</div>
      <ReactQueryDevtools initialIsOpen />
    </div>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.createRoot(rootElement).render(<App />);

실제로 사용할 때는 queries 파일 분리도 추천

const fetchOrderREsult = useFetchOrder(orderNo,{
  onSuccess:fetchOrderResultData => {
    // onSuccess 로직
  },
  onError:error=>{
    // onError 로직
  },
})
const fetchDeliveryStatusResult = useFetchDeliveryStatus(orderNo,{
  refetchInterval:isPollingActive ? 5000 : false,
  onSuccess:fetchOrderResultData => {
    // onSuccess 로직
  },
  onError:error=>{
    // onError 로직
  },
})

query가 여러 개일 경우, 알아서 잘 작동 됨!

function App () {
  const usersQuery = useQuery('users', fetchUsers)
  const teamsQuery = useQuery('teams', fetchTeams)
  ...
}
  // 동적으로 작동하려면 다른 방법을 사용해야 함

세 가지 core 컨셉

Queries

보통 GET으로 받아온 대부분의 API에 사용
데이터 Fetching용(CRUD의 R에서만 사용)

function App() {
  const info = useQuery('todos', fetchTodoList);
  // (Query Key, Query Function)
}

Query Key에 따라 query caching을 관리

// String 형태
useQuery('todos', ...)
// Array 형태
useQuery(['todo',5], ...)

Query Function

Promise를 반환하는 함수(데이터를 resolve하거나 error을 throw)

useQuery의 반환값

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

useQuery Option

useQuery('fetchOrder', ()=>fetchOrder(orderNo), options)
  • onSuccess, onError, onSettled : query fetching 성공/실패/완료 시 실행할 side Effect 정의
  • enabled : 자동으로 query를 실행시킬지 말지 여부(기본값: true)
  • retry : query 동작 실패 시, 자동으로 retry 여부 결정
  • select : 성공 시 가져온 data 가공해서 전달
  • keepPreviousData : 새롭게 fetching 시 이전 데이터 유지 여부
  • refetchInterval : 주기적으로 refetch 할지 여부 결정
  • etc

Mutations

데이터 updating시 사용
데이터 생성/수정/삭제용(CRUD중 CUD에서 모두 사용)

const mutation = useMutation(newTodo => {
  return axios.post('/todos', newTodo)
})

useQuery보다 더 심플하게 Promise 반환 함수만 있으면 됨
(Query Key를 넣어주면 devtools에서 볼 수 있음)

useMutation 반환값

  • mutate : mutation을 실행하는 함수
  • mutateAsync : mutate와 비슷, Promise 반환
  • reset : mutation 내부 상태 clean
    그 외는 useQuery와 비슷함

useMutation Option

  • onMutate : 본격적인 Mutation 동작 전에 먼저 동작하는 함수, Optimistic update(API 통신 이전에 통신이 성공할거라는 전제로 UI를 업데이트) 적용시 유용
    그 외는 useQuery와 비슷함(onError, onSettled, onSuccess 등)

Query Invalidation

queryClient를 통해 invalidate 메소드를 호출해 사용

// cache에 있는 모든 쿼리
queryClient.invalidationQueries()
// todos로 시작하는 키의 모든 쿼리
queryClient.invalidationQueries('todos')

해당 Key를 가진 query는 stale 취급
현재 rendering 되는 query들은 백그라운드에서 refetch

메모리 캐시

  • cacheTime : 메모리에 얼마나 있을건지(해당 시간 이후 GC에 의해 처리, default 5분)
  • staleTime : 얼마의 시간이 흐른 후 데이터를 stale 취급할 것인지(default 0)
  • refetchOnMount / refetchOnWindowFocus / refetchOnReconnect : true일 경우 Mount / window focus / reconnect 시점에 data가 stale이라고 판단되면 모두 refetch (default : true)

Query 상태흐름
화면에 있다가 사라지는 query

  • fetching -[staleTime>0]-> fresh(staleTime이 만료되기 전까지) -[staleTime 만료]-> stale(스크린에서 사용되는 동안) -[스크린에서 사용X]-> inactive(cacheTime이 만료되기 전까지) -[cacheTime 만료]-> deleted(GC처리)
  • fetching -[staleTime = 0]-> stale ...
    화면에 있다가 없다가 하는 query
  • active상태의 query는 fetching과 stale 상태를 반복(stale->fetching)
    (refetch이벤트가 발생하거나 Mount, window focus등의 옵션에 따른 트리거 발생)

zero-config(default값)

  • staleTime : 0 (Queries에서 cached data는 언제나 stale취급)
  • refetchOnMount/refetchOnWindowFocus/refetchOnReconnect : true (각 시점에서 data가 stale이라면 항상 refetch 발생)
  • cacheTime : 60 5 1000 (inActive Query들은 5분 뒤 GC에 의해 처리)
  • retry : 3 (실패 시 3번까지 retry 발생)
  • retryDelay : exponential backoff function

어디에서 값을 관리하는지?

어떻게 Server State를 전역상태처럼 관리할 수 있는지

// Components A
function App(){
  const info = useQuery('todos', fetchTodoList)
}
// Components B
function App(){
  const info = useQuery('todos', fetchTodoList)
}

// stale이 지속되는 동안에는 A마운트 되고 B가 마운트 되어도 API 호출이 발생하지 않음

QueryClient 내부적으로 Context 사용

ReactQuery 적용 후

Client store : Client State만 남아 목적에 맞고 심플해짐
Server State : React Query와 함께 간단해짐
Component : 길이가 더 길어짐

마무리

장점

  • 서버상태 관리에 용이함. 직관적인 API 호출 가능
  • API 처리에 관한 각종 인터페이스 및 옵션 제공
  • Client Store가 FE에서 정말로 필요한 전역상태만 남아 Store답게 사용됨
    (Bolierplate코드 감소)
  • devtool 제공, 원활한 디버깅 가능
  • Cache 전략이 필요할 때 효율적

고민점

  • component가 상대적으로 비대해지는 문제(설계/분리에 대한 고민 필요)
  • 프로젝트 설계의 난이도가 높아짐(Component 유착 최소화 및 사용처 파악 필요)
  • React Query의 장점을 더 잘 활용할 방법 고민 필요(단순한 API 통신 이상의 가능성)

추천하는 경우

  • 수많은 전역상태가 API 통신과 엮여있어 Store가 비대해지는 경우
  • API 통신 관련 코드를 보다 간단히 구현하고 싶은 경우
  • FE에서 데이터 Caching 전략에 대해 고민하는 경우
  • (공부가 목적이라면) 모든 FE에게

QnA

  • 마운트 시에 데이터가 패치되지 않고 버튼을 클릭햇을 때 데이터를 패치 받아 데이터에 따라 history.push를 해야하는 상황
  1. enabled를 false로 두고 이벤트 핸들러에서 refetch()를 사용, 매뉴얼하게 패치하는 방법
  2. enabled에 해당하는 상태를 useState로 컴포넌트 내에 두고, 이벤트 핸들러에서 해당 상태값을 변경하여 enabled를 조건부로 만족시켜 패치하는 방법
    둘 중 어느 방법을 선호하는지(공식에선 선언적이란 이유로 2를 권장)
    -> 1번을 먼저 쓰고, 1번으로 안되는 경우 2번을 사용(케바케)
  • useQuery를 이용해 불러온 server state를 이용해 액션을 생성해야 하는 경우
    어느단계에서 server state를 참조하는지
    -> option의 onSucess에서 참조(서버 상태는 서버 상태로 관리, 클라이언트 상태랑 디팬던시가 생기는건 좋지 않음)

  • 개발 할 때 cacheTime이나 staleTime 등을 변경해서 사용하는 경우
    광고를 한시간동안 갱신하는 경우, staleTime을 1시간으로 주면 그 1시간동안은 기존에 가지고 있던 데이터를 보여줌

(질의응답 파트 시간이 나면 계속 듣기 1:38:15 ~)

profile
꾸준히 나아가는 개발자입니다.

0개의 댓글