본문 바로가기
챌린지/패스트캠퍼스 공부 루틴 챌린지

패스트캠퍼스 챌린지 42일차 (react-query)

by 무벅 2022. 3. 6.
반응형

22.3.6 (일) +42days

React Query 란 ?

react-query 는 React Application에서 서버 상태를 가져오고, 캐싱하고, 동기화하고, 업데이트 하는 작업을 합니다.

Motivation

기본적으로 React Application 에서는 서버로부터 데이터를 가져오거나 업데이트하는 방법을 제공하지 않습니다.

개발자는 자체적으로 이 방법을 구축해야 하는데 일반적으로는 react hooks 를 사용하여 컴포넌트 state 및 effect 를 조합하거나 글로벌 상태 머신을 사용하여 React Application 전체의 비동기 처리를 다룹니다.

대부분의 전통적인 상태 관리 라이브러리는 비동기처리나 서버 상태 작업 보다는 클라이언트 상태 관리에 적합한데 여기서 서버 상태란 아래의 특징을 갖습니다.

  • 사용자가 제어하지 못하는 위치에서 원격으로 유지합니다.
  • 비동기 API 를 통해 서버 상태를 가져오거나 업데이트 합니다.
  • 소유권이 다른 사람과 공유됩니다.
  • 주의하지 않으면 Application 에서 잠재적으로 out-of-date 상태가 될 수 있습니다.

클라이언트에서 서버 상태를 처리할 때 다양한 문제를 마주하게 됩니다.

  • 캐싱(Caching)
  • 백그라운드에서 out-of-date 된 데이터를 갱신해야 할 때
  • 데이터가 out-of-date 될 때를 파악해야 할 때
  • 가능한 빠르게 데이터 업데이트를 반영해야 할 때
  • 페이징이나 지연 로딩(Lazy Loading) 데이터를 최적화해야 할 때
  • 메모리 가비지 컬렉션을 운영해야 할 때
  • 쿼리 결과를 메모이제이션(Memoization)

1. Important Defaults

useQuery, useInfiniteQuery 를 통한 쿼리 인스턴스는 기본적으로 캐시된 데이터를 오래된(stale) 데이터로 간주합니다

이 동작을 변경하려면 staleTime 옵션을 변경하여 글로벌 또는 각 쿼리 당 데이터를 refetch 하는 시간 간격을 조정할 수 있습니다.

오래된 쿼리는 아래와 같은 경우 백그라운드에서 자동으로 다시 가져옵니다.

  • 쿼리의 새 인스턴스가 마운트 될 때
  • window 가 다시 포커스 될 때
  • 네트워크가 다시 연결될 때
  • 쿼리의 refetch interval 이 설정되어 있을 때

refetchOnMount, refetchOnWindowFocus, refetchOnReconnect, refetchInterval 의 옵션들을 사용하여 위 기능들의 인터벌을 설정할 수 있습니다.

useQuery , useInfiniteQuery , query observer 의 활성 인스턴스가 더 이상 없을 경우 inactive 로 지정되고 나중에 다시 사용할 경우를 대비하여 캐시에 남아 있습니다.

기본적으로 inactive 쿼리는 5분 뒤에 가비지 콜렉팅 됩니다.

쿼리의 기본 cacheTime 을**1000 * 60 * 5 ms** 가 아닌 다른 값으로 변경할 수 있습니다.

실패한 쿼리를 캡쳐하여 UI 에 에러를 표시하기 전에 3회 자동으로 retry 합니다.

이를 변경하려면 쿼리에 대한 retry , retryDelay 옵션을 다른 값으로 설정해주면 됩니다.

기본적으로 쿼리 결과는 데이터가 실제로 변경 되었는지 감지하기 위해 공유되고, 변경되지 않은 경우 데이터 참조가 변경되지 않은 상태로 유지 됩니다.

JSON 과 호환되는 값만 동작하고, 다른 유형의 데이터 포맷은 항상 변경된 것으로 간주됩니다. 예를 들어, 쿼리 응답이 너무 무거워서 성능 문제를 야기할 경우config.structuralSharing 플래그를 사용하여 이 기능을 사용하지 않도록 설정할 수 있습니다. 쿼리 응답이 JSON과 호환되지 않는데도 데이터가 변경되었는지 확인하려면 config.isDataEqual 을 사용하여 데이터 비교 기능을 커스텀 할 수 있습니다.

2. Queries - 기본사항

쿼리는 기본적으로 **유니크한 키(Unique Key)**로 구분되며 프로미스(Promise) 기반 메서드 (GET, POST 등)로 서버에서 데이터를 가져올 수 있습니다.

서버의 데이터를 수정해야 하는 경우는 Mutations 을 사용하는 것이 적절합니다.

컴포넌트나 커스텀 훅에서 쿼리를 구독하려면 다음과 같이 useQuery 를 호출해야 합니다.

import { useQuery } from 'react-query'
 
 function App() {
   const info = useQuery('todos', fetchTodoList)
 }

여기서 유니크한 키(Unique Key) 는 Application 전체에서 쿼리를 다시 가져오고(refetching), 캐싱(caching)하고, 공유(sharing)하기 위해 내부적으로 사용됩니다.

useQuery 에서 반환 된 쿼리 결과엔 템플릿 및 기타 데이터 사용에 필요한 모든 정보가 포함됩니다.

const result = useQuery('todos', fetchTodoList);

result 객체엔 생산성을 높이기 위해 알아야 할 몇 가지 중요한 상태가 포함되어 있습니다. 쿼리의 상태는 항상 아래의 상태들 중 하나가 됩니다.

  • isLoading , status === 'loading' : 데이터 가져오는 중
  • isError, status === 'error' : 에러 발생
  • isSuccess , status === 'success' : 쿼링 성공
  • isIdle, status === 'idle' : 쿼링 불가능

쿼리 키(Query Keys)

react-query 는 쿼리의 유니크한 키(Unique Key) 를 기반으로 쿼리 캐싱을 관리합니다.

쿼리 키(Query Keys)는 문자열이 될 수도 있고 문자 배열이 될 수 도 있습니다.

  • 문자열 : 가장 단순한 형태의 쿼리 키는 문자열 입니다. 쿼리 키로 문자열이 전달되면 문자열을 사용하여 내부적으로 배열로 변환합니다.
// A list of todos
 useQuery('todos', ...) // queryKey === ['todos']
  • 문자 배열 : 계층 구조의 데이터 / 파라미터가 있는 쿼리
// An individual todo
 useQuery(['todo', 5], ...)
 // queryKey === ['todo', 5]
 
 // And individual todo in a "preview" format
 useQuery(['todo', 5, { preview: true }], ...)
 // queryKey === ['todo', 5, { preview: true }]
 
 // A list of todos that are "done"
 useQuery(['todos', { type: 'done' }], ...)
 // queryKey === ['todos', { type: 'done' }]

3. Queries - 병렬 & 종속 쿼리

병렬 쿼리

병렬 쿼리는 동시성을 최대화하기 위해 병렬로 실행 되거나 동시에 실행되는 쿼리입니다.

정적으로 병렬 쿼리 선언하기

useQuery , useInfiniteQuery 를 나란히 선언하면 자동으로 병렬 쿼리가 실행됩니다.

function App () {
   // The following queries will execute in parallel
   const usersQuery = useQuery('users', fetchUsers)
   const teamsQuery = useQuery('teams', fetchTeams)
   const projectsQuery = useQuery('projects', fetchProjects)
   ...
 }

Suspense 모드에서 리액트 쿼리를 사용할 때 첫번째 쿼리가 프로미스를 던지고 다른 쿼리가 실행되기 전에 컴포넌트를 일시 중단하기 때문에 병렬 쿼리가 실행되지 않습니다. 이 때, useQueries 를 사용하거나 useQuery 인스턴스에 대한 별도 구성을 해야합니다.

동적으로 병렬 쿼리 선언하기

react-hooks 의 규칙 에 의하면 반복문, 조건문 또는 중첩 함수 내에서 hook 를 선언할 수 없다.

때문에 이 경우엔 정적(수동)으로 병렬 쿼리를 선언할 수 없다.

이 때 useQueries 라는 hook 을 제공해서 동적으로 병렬 쿼리를 구성하여 실행할 수 있다.

function App({ users }) {
   const userQueries = useQueries(
     users.map(user => {
       return {
         queryKey: ['user', user.id],
         queryFn: () => fetchUserById(user.id),
       }
     })
   )
 }

종속쿼리

종속 (dependent) 쿼리는 이전 쿼리의 실행이 마무리 되어야 실행 될 수 있습니다.

이 같은 처리를 하기 위해선 아래와 같이 enabled 옵션으로 쿼리가 실행될 준비가 되었을 때를 알리면 됩니다.

// Get the user
 const { data: user } = useQuery(['user', email], getUserByEmail)
 
 const userId = user?.id
 
 // Then get the user's projects
 const { isIdle, data: projects } = useQuery(
   ['projects', userId],
   getProjectsByUser,
   {
     // The query will not execute until the userId exists
     enabled: !!userId,
   }
 )

Mutations - 기본사항

기본사항

쿼리와 달리 뮤테이션은 데이터 CUD 에 사용됩니다. 이 목적으로 react-query 는 useMutation hook 을 제공합니다.

function App() {
   const mutation = useMutation(newTodo => axios.post('/todos', newTodo));
 
   return (
     <div>
       {mutation.isLoading ? (
         'Adding todo...'
       ) : (
         <>
           {mutation.isError ? (
             <div>An error occurred: {mutation.error.message}</div>
           ) : null}
 
           {mutation.isSuccess ? <div>Todo added!</div> : null}
 
           <button
             onClick={() => {
               mutation.mutate({ id: new Date(), title: 'Do Laundry' })
             }}
           >
             Create Todo
           </button>
         </>
       )}
     </div>
   )
 }

뮤테이션은 아래의 상태 중 하나를 가지게 됩니다.

  • isLoading , status === 'loading' : 데이터 변경 중
  • isError, status === 'error' : 에러 발생
  • isSuccess , status === 'success' : 데이터 변경 성공
  • isIdle, status === 'idle' : 데이터 변경 불가능

위 예제를 보면 단일 변수나 객체 형태로 mutate 함수에 전달 될 수 있습니다.

mutate 함수는 비동기 함수이므로 react 16 이전 버전에서는 사용할 수 없습니다. onSubmit 에서 이벤트에 접근할 때 mutate 를 다른 함수로 래핑해야 합니다. react 이벤트 풀링

// This will not work in React 16 and earlier
const CreateTodo = () => {
  const mutation = useMutation(event => {
    event.preventDefault();
    return fetch('/api', new FormData(event.target));
  })

  return <form onSubmit={mutation.mutate}>...</form>
}

// This will work
const CreateTodo = () => {
  const mutation = useMutation(formData => {
    return fetch('/api', formData);
  })
  const onSubmit = event => {
    event.preventDefault();
    mutation.mutate(new FormData(event.target));
  }

  return <form onSubmit={onSubmit}>...</form>
}

뮤테이션 상태 리셋

뮤테이션 요청에 대한 error, data 를 초기화할 필요가 있을 때 reset 함수를 사용합니다.

const CreateTodo = () => {
  const [title, setTitle] = useState('');
  const mutation = useMutation(createTodo);

  const onCreateTodo = e => {
    e.preventDefault();
    mutation.mutate({ title });
  }

  return (
    <form onSubmit={onCreateTodo}>
      {mutation.error && (
        <h5 onClick={() => mutation.reset()}>{mutation.error}</h5>
      )}
      <input
        type="text"
        value={title}
        onChange={e => setTitle(e.target.value)}
      />
      <br />
      <button type="submit">Create Todo</button>
    </form>
  )
}

사이드 이펙트

useMutation 는 뮤테이션 라이프 사이클에서 쉽고 빠르게 사이드 이펙트를 수행할 수 있게 몇 가지 헬퍼 옵션을 갖습니다.

useMutation(addTodo, {
   onMutate: variables => {
     // 뮤테이션 발생 시 !
     // 데이터를 포함한 컨텍스트를 선택적으로 리턴할 수 있습니다.
     return { id: 1 }
   },
   onError: (error, variables, context) => {
     // 에러 일 경우 !
     console.log(`rolling back optimistic update with id ${context.id}`)
   },
   onSuccess: (data, variables, context) => {
     // 요청 성공 시 !
   },
   onSettled: (data, error, variables, context) => {
     // 요청 성공, 에러 상관없이 ! 
   },
 })

Mutations - 무효화

일반적으로 뮤테이션을 통해서 데이터의 CUD가 발생하면 새로운 변경 분을 조회하기 위해 데이터를 가져와야 하는 쿼리가 어플리케이션에 있을 가능성이 매우 높습니다.

const mutation = useMutation(postTodo);

postTodo mutation 이 성공적으로 실행되면 모든 할 일 목록을 조회하는 쿼리가 무효화되고 새 할일 목록을 표시하기 위해 다시 가져올 수 있습니다.

useMutation 의 onSuccess 옵션과 클라이언트의 invalidateQuries 기능을 사용할 수 있습니다.

import { useMutation, useQueryClient } from 'react-query'
 
 const queryClient = useQueryClient()
 
 // When this mutation succeeds, invalidate any queries with the `todos` or `reminders` query key
 const mutation = useMutation(addTodo, {
   onSuccess: () => {
     queryClient.invalidateQueries('todos')
     queryClient.invalidateQueries('reminders')
   },
 })

Mutations - 응답 업데이트

데이터를 업데이트하는 뮤테이션을 처리할 때 뮤테이션의 응답으로 새롭게 갱신된 데이터가 반환되는 것이 일반적 입니다.

이 때, 새롭게 갱신된 데이터를 가져오기 위해 다시 쿼리하는 것은 네트워크 호출을 낭비하는 것 이므로 뮤테이션에서 반환된 객체를 활용해서 쿼리 클라이언트의 setQueryData 메서드를 사용해서 즉시 갱신된 데이터로 기존 쿼리를 업데이트 할 수 있습니다.

const queryClient = useQueryClient();
 
 const mutation = useMutation(editTodo, {
   onSuccess: data => {
     queryClient.setQueryData(['todo', { id: 5 }], data)
   };
 });
 
 mutation.mutate({
   id: 5,
   name: 'Do the laundry',
 });
 
 // The query below will be updated with the response from the
 // successful mutation
 const { status, data, error } = useQuery(['todo', { id: 5 }], fetchTodoById);

커스텀 훅을 사용해서 onSuccess 를 추상화 할 수 있습니다.

const useMutateTodo = () => {
   const queryClient = useQueryClient()
 
   return useMutation(editTodo, {
     // Notice the second argument is the variables object that the `mutate` function receives
     onSuccess: (data, variables) => {
       queryClient.setQueryData(['todo', { id: variables.id }], data)
     },
   })
 }

 

 

 

 

 

 

 

 

https://bit.ly/37BpXiC

본 포스팅은 패스트캠퍼스 환급 챌린지 참여를 위해 작성되었습니다

 

 

 

 

 

 

반응형

댓글