라이브러리

React Query와 전역 상태 관리

홍준혁 [Hong-JunHyeok] 2022. 4. 25. 13:49
728x90

 

전역 상태 관리 라이브러리인 리덕스로 대부분의 프로젝트를 진행을 했었는데, 사용하면서 느낌 문제점은 다음과 같습니다.

- 대부분의 전역 상태는 비동기 통신을 위해서 쓰이고 있다.

- Store의 역할이 너무 비대하다.

- 보통 비동기 전역 상태는 data, error, loading이 세가지로 나누는데 그럼, 전역으로 관리하는 상태가 많아진다면 비효율적일 것이다.

- 순수 클라이언트의 전역 상태와 비동기 전역 상태와 비교했을 때 비동기 전역 상태가 월등하게 많다.

 

해결 방안은 무엇일까요?

- store에서 비동기 통신하는 부분들을 걷어내고 온전한 Client의 상태만을 남겨 놓는다.

- Store밖에서 서버와 관련된 상태 관리방안 마련한다.

- (중요) 서버와 관련된 상태는 전역 상태처럼 사용할 수 있어야 함.

 

"왜 전역 상태여야 하는가?"에 대한 답변은 https://slog.website/post/13 를 참고하면 좋을 것 같습니다.

 

React Query란?

 

React Query는 Server State를 효율적으로 관리하는 라이브러리입니다.

React Query뿐만 아니라 SWR, RTK-query 등 다양한 라이브러리에서 위의 스펙을 구현했습니다.

 

Server State를 관리하면서 생기는 다음과 같은 문제점이 있습니다.

1. 원격 데이터는 서버에서 관리되는 데이터임

2. API를 통해서 관리되는 데이터를 받아옴

3. 해당 데이터를 원격에서 변경될 수 있음

4. 그럼 해당 응답은 구식(out of date)가 될 수 있음

 

React Query는 이를 HTTP stale-while-revalidate의 스펙을 따라 구현함으로써 해결해줍니다. 

Cache-Control: max-age=1, stale-while-revalidate=59

위의 코드는 HTTP Header의 Cache를 컨트롤 하는 로직인데요, 해석하자면 다음과 같습니다.

  • 1초 내에 반복적인 요청이 발생한 경우, 캐시된 값을 그대로 반환한다.
  • 1 ~ 60초 사이에 반복적인 요청이 발생한 경우, 이전에 응답했던 낡은 데이터를 반환한다. 이와 동시에 향후 사용을 위해서 캐시를 새로운 값으로 채우도록 재검증 요청이 들어간다.
  • 60초 이후에 들어온 HTTP 요청에 대해서 다시 서버에 요청을 보낸다.

 

const { isLoading, error, data, isFetching } = useQuery("repoData", () =>
    fetch(
      "https://api.github.com/repos/tannerlinsley/react-query"
    ).then((res) => res.json())
);

useQuery의 첫번째 인자로 Unique Key를 받습니다. 다른 곳에서도 해당 키를 통해서 결과를 꺼내올 수 있습니다.

useQuery함수의 결과값은 객체이며 isFetching, isLoading 등의 동작 상태, data를 통해 결과, error를 통해 에러 여부를 알 수 있습니다.

 

onSuccess, onError와 같이 Fetch가 완료된 이후 실행될 콜백을 정의해놓을 수 있습니다.

refetchInterval로 polling을 구현할 수 있습니다.

 

여기서 중요한점은 서버 데이터를 전역 상태처럼 사용할 수 있어야 한다는 점입니다.

QueryClient를 사용하면 아까 정의한 Unique Key를 통해서 꺼내 사용할 수 있습니다. (내부적으로 Context를 사용하기 때문)

 

Mutation

useMutation훅으로 서버의 데이터 변경을 요청할 때 사용하는 함수입니다.

즉, Server State에 Side Effect가 발생했을 때 사용합니다. (Create, Update, Delete)

const queryClient = useQueryClient()
 
 useMutation(updateTodo, {
   // When mutate is called:
   onMutate: async newTodo => {
     // Cancel any outgoing refetches (so they don't overwrite our optimistic update)
     await queryClient.cancelQueries('todos')
 
     // Snapshot the previous value
     const previousTodos = queryClient.getQueryData('todos')
 
     // Optimistically update to the new value
     queryClient.setQueryData('todos', old => [...old, newTodo])
 
     // Return a context object with the snapshotted value
     return { previousTodos }
   },
   // If the mutation fails, use the context returned from onMutate to roll back
   onError: (err, newTodo, context) => {
     queryClient.setQueryData('todos', context.previousTodos)
   },
   // Always refetch after error or success:
   onSettled: () => {
     queryClient.invalidateQueries('todos')
   },
 })

여기서 invalidateQueries는 Mutate 되어 해당 키의 쿼리는 다시 Fetching 하게 됩니다.

 

useQuery는 병렬처리를 해서 동시성을 극대화합니다. (useQueries도 지원) 즉, 개발하는 입장에서 전혀 신경 쓰지 않아도 무관하죠.

 

물론 다양한 상태 관리 기법이 있겠지만 위와 같은 라이브러리를 이용해서 Store에는 팝업 등 여러 UI 상태 관리, 인증 정보 관리 등 Client 내에서 사용되는 상태만 관리를 하고 비동기 처리를 통해 가져온 Server State는 캐싱하는 작업으로 관리하는 것도 좋은 방법인 것 같습니다.

 

+ 여담

React Query에 enable 속성은 왜 필요할까?

 

이런 경우가 있다고 생각해봅시다.

초기 마운티 시에는 마운트 되지 않고 어떤 액션이 일어나을 경우에만 (Button 클릭 등) Fetch를 하고 싶은 경우가 있을 겁니다.

그럴 때 enable속성에 false를 해주는 작업이 필요합니다.

그 enable에 대한 상태 값을 만들고 조건부 처리로 true일 때만 fetch 하는 작업을 할 수 있습니다.

 

728x90