(React) React Query

Mirrer·2023년 6월 10일
0

Library

목록 보기
16/17
post-thumbnail

React Query

React Query서버 데이터를 가져와 캐싱, 동기화, 업데이트와 같은 기능들을 쉽게 사용할 수 있게 도와주는 라이브러리입니다.


React Query를 사용하는 이유

React Qeury를 사용하면 아래와 같은 기능들을 직접 구현하지 않고 쉽고 빠르게 사용할 수 있습니다.


1. 클라이언트 상태와 서버 상태의 분리

클라이언트에서 사용하는 상태와 서버에서 가져온 상태를 혼합하여 사용할 경우 상태 관리와 로직 작성이 복잡해지고 유지보수가 어려워질 수 있는데 이러한 경우에 React Query를 사용해 서버 상태를 따로 관리하면 프로젝트 관리가 편해집니다.

2. 최신 상태 유지

쉽게 최신 상태를 가져오는 기능을 제공합니다. 일정 시간마다 상태를 업데이트하거나 특정 동작 이후에 상태를 업데이트할 수 있습니다.

3. 캐싱, 중복 요청 방지

API 요청 결과를 캐싱하여 관리합니다. 서버에 API 요청하여 받아온 결과를 캐싱하며 중복 요청을 최소화할 수 있습니다.

4. 비동기 요청에 대한 상태 핸들링

비동기 API 요청에 대한 로딩 상태, 결과 값, 에러 상태와 같은 여러가지 상태를 확인하는 기능을 제공합니다.


Code Example

React Query를 사용하면 단순 기능 구현, 기능 테스트, 오류 수정과 같은 과정에서 많은 개발 인력 및 시간을 감소시킬 수 있습니다.

다음은 게시글의 정보를 불러오기 위해 서버에 GET method 요청을 보내는 과정입니다.


1. API 요청 결과를 보관할 상태를 생성

const [posts, setPosts] = useState(null);

2. API 요청 과정을 표현할 상태를 생성

// 사용자에게 로딩 UI를 보여주기 위한 loading 상태
const [loading, setLoading] = useState(false); 
// 에러가 발생했을 때 사용자에게 UI를 보여주기 위한 error 상태
const [error, setError] = useState(false); 

3. axois를 사용한 비동기 API 요청 로직 작성

const fetchPosts = async () => {
    if (loading) return;
    setLoading(true);

    try {
      const response = await axios.get('https://jsonplaceholder.typicode.com/posts');
      setPosts(response.data);
    } catch (error) {
      setError(true)
    }
    setIsLoading(false);
};

위와 같이 간단하게 게시글 정보를 불러오는 작업만으로도 많은 코드를 작성해야 합니다.

물론 해당 로직의 재활용성을 높이기 위해 custom hook으로 작성할 수도 있지만 더 자세한 코드를 작성해야 하고, 직접 테스트하는 것에도 많은 시간이 투자됩니다.


반면 React Query는 위와 같이 복잡한 로직을 다음과 같이 더 쉽고 간결하게 표현할 수 있습니다.

const fetchPosts = async () => await axios.get('https://jsonplaceholder.typicode.com/posts').then(res => res.data);

const { data, isLoading, error } = useQuery(['fetchPosts'], fetchPosts);

Life Cycle

라이프사이클(Lifecycle)은 생명주기를 뜻하며 브라우저 상에서 나타나고, 업데이트되고, 사라지게 되는 과정을 의미합니다.

React Query에서는 아래와 같은 라이프사이클 상태가 존재합니다.


fetching

  • 데이터 요청 상태

fresh

  • 데이터가 만료되지 않은 신선한 상태

  • 컴포넌트의 상태가 변경되도, 데이터를 다시 요청하지 않는다.

stale

  • 데이터가 만료된 상태

  • 클라이언트로 전달된 서버 데이터는 다른 유저가 CRUD연산을 적용할 수 있기 때문에 데이터 최신화가 필요하여 컴포넌트의 마운트, 업데이트시 데이터를 재요청한다.

  • fresh에서 stale로 넘어가는 시간의 Default값은 0ms이다.

inactive

  • 사용하지 않는 상태로 일정 시간이 지나면 가비지 컬렉터(Garbage Collection, 쓰레기 수집)가 캐시에서 제거한다.

  • Default값은 5000ms(5분)으로 만약 해당 값을 무한대(infinite)로 설정하면 데이터 누수가 심해지고, 오류가 발생하여 권장하지 않는다.

delete

  • 가비지 컬렉터(Garbage Collection, 쓰레기 수집)에 의해 캐시에서 제거된 상태

Life Cycle의 동작 예시


1. A 쿼리 인스턴스가 mount

사용자에게 컴포넌트가 보여질 때 A 쿼리 인스턴스를 서버에 요청한다.

2. 네트워크에서 데이터를 fetch한 뒤 A라는 query key로 캐싱

서버에서 데이터를 가져온 후 해당 데이터를 브라우저에 캐싱한다.

3. fresh 상태의 데이터는 staleTime 이후 stale 상태로 변경

캐싱된 데이터는 만료되지 않는 상태에서 지정한 staleTime(Default 0ms)이후 만료된 상태로 변경된다.

4. A 쿼리 인스턴스가 unmount

A 쿼리 인스턴스가 컴포넌트에서 사라진다.

5. cacheTime만큼 캐시가 유지된 뒤 Gerbage Collection으로 수집

브라우저에 저장된 캐시는 지정한 cacheTime(Default 5000ms) 이후 가비지 컬렉터에 의해 수집되어 삭제된다.

만약 cacheTime이 지나기 전에 A 쿼리 인스턴스가 새롭게 요청되면, 데이터를 요청하고, 데이터를 새로 가져오기전까지는 이미 브라우저에 캐시 된 데이터를 보여준다.


StaleTime, CacheTime

StaleTime

데이터 요청 후 만료되기 까지 걸리는 시간

StaleTime은 데이터가 fresh에서 stale 상태로 변경되는 데 걸리는 시간을 뜻한다.

fresh 상태일 때는 쿼리 인스턴스가 새롭게 mount 되어도, 네트워크 fetch가 일어나지 않는다.

즉 데이터가 만료 되기 전까지는 쿼리를 새롭게 실행시켜도 새로운 요청을 하지 않는다.


cacheTime

데이터를 사용하지 않은 상태

데이터가 inactive된 상태로 캐싱되어 남아있는 시간을 의미한다.

쿼리 인스턴스가 unmount 되면, 데이터는 inactive 상태로 변경된다.

즉 쿼리 요청 후 쿼리 요청 작업이 끝난 데이터는 사용하지 않은 상태로 변경된다.

캐시는 cacheTime만큼 유지되며, 기본값은 5000ms(5분)으로 cacheTime이 지나면 가비지 콜렉터(Garbage Collection)에 수집된다.

만약 cacheTime이 지나기 전에 쿼리 인스턴스가 다시 마운트 되면, 데이터를 fetch 하는 동안 캐시 데이터를 보여준다.


React Query 사용하기

아래 명령어를 통해 React Query를 설치한다.

npm i @tanstack/react-query
yarn add @tanstack/react-query

이 후 프로젝트에서 사용하기 위해 QueryClientProvider로 컴포넌트를 감싼 뒤QueryClient 인스턴스를 생성해서 props로 전달한다.

const queryClinet = new QueryClient()

root.render(
  <React.StrictMode>
    <QueryClientProvider client={queryClinet}>
      <App />
    </QueryClientProvider>
  </React.StrictMode>
);

useQuery

React Query로 데이터를 가져오기 위해서는 useQuery를 사용해야 하며 Query key, API를 요청하는 비동기 함수, Query Options를 순서대로 인자로 넣어 사용할 수 있습니다.

이때 첫 번째 인자인 Query key는 캐싱 처리 및 쿼리 호출에 사용되기 때문에 중복되지 않게 관리해야 합니다.

const fetchPosts = async () => await axios.get('https://jsonplaceholder.typicode.com/posts').then(res => res.data);

const { data, isLoading, error } = useQuery(['fetchPosts'], fetchPosts);

Return Values

useQuery가 반환하는 return값의 종류는 다음과 같습니다.


Status

  • idle : 초기 상태
  • loading : 데이터 fetching일 때의 상태
  • success : 데이터 fetch에 성공한 상태
  • error : 데이터 fetch에 실패한 상태

isFetcing

  • 데이터가 fetch 상태일 때 true를 반환
  • 캐싱 데이터가 존재하여 백그라운드에서 fetch 되어도 true를 반환

isLoading

  • 캐싱된 데이터가 없을 때 또는 fetch 중인 경우 true를 반환

data

  • 응답받은 데이터

error

  • 실패 정보

refetch

  • 수동으로 데이터 refetch를 실행하는 함수
  • scale이나 cache 같은 설정들이 무시되고, 무조건 데이터를 다시 fetching

Query Options

useQuery에서 세번째 인자로 사용되는 Option 종류는 다음과 같습니다.


enabled (boolean)

  • default값은 true이며, 쿼리의 자동 실행여부를 설정하는 옵션

retry (boolean | number | (failureCount: number, error: TError) => boolean)

  • retry 는 실패한 쿼리를 재시도하는 옵션으로 기본적으로 쿼리 실패시 3번 재시도
  • true 로 설정하면 쿼리 실패시 무한 재시도하고 false로 설정하면 재시도를 하지 않는다.

staleTime (number | Infinity)

  • staleTime 은 데이터가 fresh 상태로 유지되는 시간이다. 해당 시간이 지나면 stale 상태가 된다.
  • default staleTime은 0ms 이다.
  • fresh 상태에서는 쿼리가 다시 mount 되어도 fetch가 실행되지 않는다.

cacheTime (number | Infinity)

  • cacheTime inactive 상태인 캐시 데이터가 메모리에 남아있는 시간이다. 이 시간이 지나면 캐시 데이터는 가비지 컬렉터에 의해 메모리에서 제거된다.
  • default cacheTime 은 5000ms(5분)이다.

onSuccess ((data: TDdata) => void)

  • onSuccess 는 쿼리 성공 시 실행되는 함수이다.
  • 매개변수 data는 성공 시 서버에서 넘어오는 response이다.

onError ((error: TError) => void)

  • onError 는 쿼리 실패 시 실행되는 함수이다.
  • 매개변수로 에러 값을 받을 수 있다.

onSettled ((data?: TData, error?: TError) => void)

  • onSettled쿼리가 성공해서 성공한 데이터가 전달되거나, 실패해서 에러가 전달 될 때 실행되는 함수이다.
  • 매개변수로 성공 시엔 성공 데이터, 실패 시에는 에러가 전달된다.

select : (data : TData) ⇒ unknown

  • 응답 데이터를 가공할 때 사용하는 함수이다.

initialData (TData | () => TData)

  • 쿼리가 아직 생성되지 않았을 때 initialData 를 설정하면 쿼리 캐시의 초기 데이터로 사용된다.
  • staleTime 이 설정되지 않은 경우 초기 데이터는 기본적으로 stale 상태로 간주한다.

useQuery 동기적 실행

위에서 설명한 Query Optionsenabled 옵션의 값이 true일때 useQuery를 실행하는 특성을 이용하여 동기적으로 useQuery를 사용할 수 있습니다.

const fetchPosts = async () => await axios.get('https://jsonplaceholder.typicode.com/posts').then(res => res.data);
const fetchUsers = async () => await axios.get('https://jsonplaceholder.typicode.com/users').then(res => res.data);

const { data: posts, isLoading, error } = useQuery(['fetchPosts'], fetchPosts);
const { data: users, isLoading, error } = useQuery(['fetchUsers'], fetchUsers, {
	enabled: !!posts // true가 되면 fetchUsers를 실행
}); 

Query key Parameter

특정 정보를 포함하여 Query key를 관리하고 싶다면 useQuery의 첫 번째 인자에 배열로 파라미터를 추가할 수 있습니다.

const fetchPost = async id => await axios.get(`https://jsonplaceholder.typicode.com/post/${id}`).then(res => res.data);

const id = 1
const { data, isLoading, error } = useQuery(['fetchPost', id], () => fetchPosts(id));

useQueries

useQueries를 사용하면 여러 개의 useQuery를 배열에 담아 간결한 로직을 작성할 수 있습니다.

const results = useQueries({
    ueries: [
        { queryKey: ['fetchPost', id], queryFn: () => fetchPost(id)},
        { queryKey: ['fetchComment', id], queryFn: () => fetchComment(id)}
        { queryKey: ['fetchUsers', id], queryFn: () => fetchUsers(id)}
    ]
})

useMutation

React Query데이터를 변경하기 위해서는 useMutation을 사용해야 하며 첫 번째 인자로 API를 요청하는 비동기 함수를, 두 번째 인자로는 옵션 데이터를 넣을 수 있습니다.

데이터를 변경할 때만 사용하기 때문에 캐싱 기능의 사용이 필요하지 않아 Query key는 사용하지 않습니다.

아래 코드는 게시글을 등록하는 간단한 예제입니다.

const editPost = async postData => await axios.post('https://jsonplaceholder.typicode.com/post', postData)
const { data, isLoading, error } = useMutation(() => editPost(postData));

useMutation 역시 API 요청에 대한 로딩 상태, 에러 상태, 결과 등 여러 상태를 반환합니다.


useMutation 이후 fetch 요청

일반적으로 API의 요청 이후 해당 결과를 업데이트하여 UI를 통해 사용자에게 노출시켜야 합니다.

이 때 useMutation의 옵션을 설정해주면 기존에 사용했던 Query key로 데이터를 쉽게 가져올 수 있습니다.

const queryClient = useQueryClient();

const editPost = async postData => await axios.post('https://jsonplaceholder.typicode.com/post', postData)

const { data, isLoading, error } = useMutation(() => editPost(postData), {
  onSuccess: () => () => queryClient.invalidateQueries(["fetchPosts"]),
});

위 코드는 useMutation의 두 번째 인자에 onSuccess를 정의하여 데이터 변경에 성공하면 queryClientinvalidateQueries를 실행합니다.

queryClient.invalidateQueries는 인자로 Query key를 받아 해당 key로 관리하는 API를 재호출합니다.

onSuccess 외에도 위에서 설명한 Query Options의 다양한 옵션을 사용하면 복잡한 기능을 더 간결한 로직으로 구현할 수 있습니다.


React Query Devtools

React Query DevtoolsReact Query와 함께 사용되는 내장 개발 도구로 사용중인 모든 쿼리의 상태를 시각화하며, 이 쿼리가 예상대로 작동하지 않는 경우 문제를 해결하는데 도움이 될 수 있습니다.

개발 도구를 실행하기 위해서는 앞서 설명한 QueryClientProviderReactQueryDevtools를 감싸줘야 합니다.

const queryClinet = new QueryClient()

root.render(
  <React.StrictMode>
    <QueryClientProvider client={queryClinet}>
      //initialIsOpen : open된 채로 시작
      //position : devtools를 열 수 있는 logo 위치 - 우측 하단으로 지정
      <ReactQueryDevtools initialIsOpen={false} position='bottom-right' />
      <App />
    </QueryClientProvider>
  </React.StrictMode>
);

위와 같이 적용 후, 실행하면 우측 하단에 아래와 같은 꽃 모양(React Query 로고)의 버튼이 생긴 것을 확인할 수 있습니다.

해당 로고를 클릭하면, 다음과 같은 개발도구가 화면에 나타나게 됩니다.


Detail Devtools

React Query Devtools는 다음과 같이 기능을 제공합니다.

  • queryKey로 쿼리를 표시합니다.

  • fresh, fetching, stale, inactive와 같은 쿼리 상태를 표시합니다.

  • 해당 API를 요청하는 observer 수 조회가 가능합니다.

  • 쿼리가 마지막으로 업데이트 된 시간을 확인할 수 있습니다.

  • refetch, invalidate, reset, remove 등의 actions를 GUI를 통해 쉽게 적용할 수 있습니다.

  • Data Explorer 탭에서 Chrome devtools의 Network 탭에서 존재하는 정보 또한 확인할 수 있습니다.


참고 자료

Window Focus Refetching | TanStack Query Docs
React Query Tutorial for Beginners - Codevolution
React Query와 상태관리 :: 2월 우아한테크세미나 - 우아한테크

profile
memories Of A front-end web developer

0개의 댓글