React Query

Happhee·2022년 4월 20일
4

💙  React 💙

목록 보기
13/18
post-thumbnail
post-custom-banner

React Query는 React 애플리케이션에서 서버 상태를 가져오기, 캐싱, 동기화 및 업데이트를 쉽게 만들어주는 라이브러리이다.

서버 상태를 관리하기 위한 최고의 라이브러리 React Query에 대해 알아보자.


✨ React Query

비동기 처리를 쉽게 관리할 수 있는 라이브러리이면서 Client State에 적합한 라이브러리이다.

  • Client State
    👉 세션 간 지속되지 않는 데이터, 동기적, 클라이언트가 소유하는 데이터를 말한다.
    ex) 컴포넌트의 state, 동기적으로 저장되는 redux store의 데이터

  • Server State
    👉 세션 간 지속되는 데이터, 비동기적, 여러 클라이언트에 의해 수정될 수 있는 공유되는 데이터를 말한다.
    ex) 비동기 요청으로 받아올 수 있는 백엔드 DB에 저장되어 있는 데이터

등장 배경

  1. React 자체가 데이터를 패칭해오거나 업데이트 하는 옵션을 제공하지 않기에 개발자 각각의 방식으로 http 통신을 구현하였다.

  2. Redux 같은 전역 상태관리 라이브러리는 클라이언트 상태값에 대해서는 잘 동작하지만, 서버 상태에 대해서는 유용하지 않다.

    • 서버 데이터는 항상 최신 상태가 아니다.
    • 명시적인 fetching이 수행되어야 최신으로 전환된다.
    • 네트워크 통신의 최소화를 구현하기 힘들다.

React Query는 서버에서 주기적으로 fetch하기에 이 데이터를 전역에서 사용하도록 만들어주고, Optimistic Update (데이터 변경을 요청 후 실제로 요청이 성공하기 전 미리 UI만 변경한 뒤, 서버의
응답에 따라 다시 롤백하거나 업데이트 된 상태로 UI를 놔두는 것) 기능도 제공한다.

장점

  • Redux, MobX를 사용할 때보다 서버 상태 관리에 용이하고 직관적인 API를 호출할 수 있다.
  • API 처리에 대해 각종 인터페이스와 옵션을 제공한다.
  • Client Store가 Front-End에서 정말 필요한 전역상태만 남기에 Store답게 사용될 수 있다.
  • Devtool 제공으로 원활한 디버깅이 가능하다.
  • Cache 전략이 필요할 때, 아주 좋다.

단점

  • Component가 상대적으로 비대해지는 문제가 생긴다.
  • Component 유착 최소화 및 사용처 파악이 필요하기에 좀 더 난이도가 높아지는 프로젝트를 설계 해야 한다.
  • 단순 API 통신 이상의 기능을 구현하기 위한 React Query를 생각해보아야 한다.

데이터 캐싱

서버에서 받아오려는 데이터가 캐시에서 이미 최신 상태라면, 서버에 도달하기 전에 브라우저가 이를 가로채서 캐시 메모리에 있는 데이터를 사용하는 것이다.

React Query의 hook인 useQuery로 API 데이터의 만료 시간, 리프레싱 주기, 데이터를 캐시에서 유지할 기간, 브라우저 포커스 시 데이터 리프레시 여부, 성공/에러 콜백 등의 기능을 제어할 수 있다.


✨ 사용 방법

설치 및 초기 설정

비동기 통신을 하기 위해서 axios과 같은 통신 관련 라이브러리는 미리 추가되어있어야 한다.

npm i react-query // npm 사용
or
yarn add react-query // yarn 사용

캐시를 관리하기 위한 Query Client인스턴스를 생성하고, QueryClientProvider를 통해 컴포넌트가 Query Client에 접근할 수 있도록 최상단에 추가한다.

import React from "react";
import ReactDOM from "react-dom";
import { QueryClient, QueryClientProvider } from "react-query";

const queryClient = new QueryClient();

const App = () => { 
  return (
    <QueryClientProvider client={queryClient}>
  		{/* 컴포넌트들 */}
    </QueryClientProvider>
  );
}

export default App;

React Query의 상태

  • fresh 새롭게 추가된 query / 만료되지 않은 query
    👉 component가 mount (최초 실행) / unmount (제거) 시에도 데이터를 요청하지 않는다.

  • fetching 요청 중인 query

  • stale 요청이 만료된 query
    👉 component가 mount (최초 실행) / unmount (제거) 시에 캐싱된 데이터를 요청한다.

  • inactive 비활성화된 query
    👉 특정 시간이 지나면 캐시가 가비지 컬렉터에 의해 제거된다.

자동 refetch

  1. 런타임에 stale인 특정 쿼리 인스턴스가 다시 만들어졌을 때
  2. window가 다시 포커스가 되었을때(옵션으로 끄고 키는게 가능)
  3. 네트워크가 다시 연결되었을 때(옵션으로 끄고 키는게 가능)
  4. refetch interval이 있을때 : 요청 실패한 쿼리는 디폴트로 3번 더 백그라운드단에서 요청하며, retry, retryDelay 옵션으로 간격과 횟수를 커스텀 가능하다.

💡 useQuery

GET 요청과 같은 CREATE 작업을 할 때 사용되는 훅이며. 데이터를 가져온다.

const requestData = useQuery( 쿼리 키, 쿼리 함수, 옵션);
  1. 쿼리 키 문자열 또는 배열
    👉 캐싱 처리에 중요하다.

    • 유일한 키 : fresh 상태가 되면 키 값으로 추정한다.
      [ 쿼리 이름, 프로미스 함수의 인자 이름 ]
  2. 쿼리 함수 Promise를 반환하는 함수
    👉 axios, fetch

  3. 옵션 useQuery 기능을 제어한다.
    👉 cacheTime, staleTime, refetchOnMount, refetchOnWindowFocus, refetchInterval, refetchIntervalInBackground, enabled, onSuccess, onError, select

  4. 반환 값
    isLoading, isError, isSuccess 등 Promise의 함수 상태를 status로 알 수 있다.

✅ querykey

기본적으로 배열이 들어가고, 순서에 따라 다른 키로 인식한다.

키의 종류

  • 문자열 자동으로 길이가 1인 배열로 변환한다.
    ex) "first" 👉 [ "first"]

  • 배열 문자열과 숫자가 함께 있으면 숫자로 구분한다.

  • Promise 함수의 인자 이름 배열의 마지막 요소이며 객체로 전달한다.

❗️ 쿼리 키가 다르면 캐싱을 별도로 관리한다 ❗️

만약, 호출하는 API가 같더라도 쿼리 키가 다르다면 캐싱은 별도로 진행된다.

const firstQuery = useQuery(["todos", "first"], fetchTodos);
const secondQuery = useQuery(["todos", "second"], fetchTodos);
const thirdQuery = useQuery(["todos", "third"], fetchTodos);

["todos", "first"] , ["todos", "second"], ["todos", "third"] 각각에 대해 캐싱을 진행한다.

✅ 옵션

  • cacheTime
    unmount 이후 cacheTime시간동안 메모리에 데이터를 저장하여 캐싱한다.
// cacheTime을 3초로 설정
const { data, isLoading } = useQuery('todos', fetchTodos, {
  cacheTime : 3000,
});
  • staleTime
    query가 fresh에서 stale로 전환되기까지 걸리는 시간이다.
    👉 기본값은 0이다.
    👉 fresh 상태에서는 재요청이 이루어지지 않으므로 API 요청 횟수를 줄일 수 있다.
    👉 보통 쉽게 변하지 않는 컴포넌트에서 사용한다.
// staleTime을 3초로 설정
const { data, isLoading } = useQuery('todos', fetchTodos, {
  staleTime : 3000,
});
  • refetchOnMount
    컴포넌트 mount시에 새로운 데이터의 패칭 유무를 나타낸다.
    👉 기본값은 true이다.
    👉 false이라면, 새로운 데이터를 가져오지 않는다.
// refetchOnMount을 3초로 설정
const { data, isLoading } = useQuery('todos', fetchTodos, {
  refetchOnMount : true,
});
  • refetchOnWindowFocus
    브라우저 클릭 시, 새로운 데이터의 패칭 유무를 나타낸다.
    👉 기본값은 true이다.
    👉 false이라면, 브라우저가 포커스 되어도 데이터를 가져오지 않는다.
const { data, isLoading } = useQuery('todos', fetchTodos, {
  refetchOnWindowFocus : true,
});
  • refetchInterval
    지정한 시간 간격으로 데이터를 패칭한다.
    👉 기본값은 0이다.
    👉 브라우저에 포커스가 없을 때는 실행되지 않는다.
// 2초 간격으로 데이터 패칭
const { data, isLoading } = useQuery('todos', fetchTodos, {
  refetchInterval: 2000,
});
  • refetchIntervalInBackground
    브라우저에 포커스가 없어도 refetchInterval에서 지정한 시간 간격으로 데이터를 패칭한다.
    👉 기본값은 false이다.
// 2초 간격으로 데이터 패칭
const { data, isLoading } = useQuery('todos', fetchTodos, {
  refetchInterval: 2000,
  refetchIntervalInBackground: true,
});
  • enabled
    컴포넌트가 mount되어도 데이터를 패칭하지 않는다.
    👉 기본값은 true이다.
    👉 useQuery의 반환값 중 refetch를 사용해 데이터 패칭을 진행한다.
const { data, isLoading, refetch } = useQuery('todos', fetchTodos, {
  enabled : false,
});
return (
  <button onClick={ refetch }>Fetch Button</button>
)
  • onSuccess
    데이터 패칭 성공을 의미한다.
const { data, isLoading } = useQuery('todos', fetchTodos, {
  onSuccess : (data) => {
    console.log('데이터 요청 성공', data);
  }
});
  • onError
    데이터 패칭 실패를 의미한다.
const { data, isLoading } = useQuery('todos', fetchTodos, {
  onError : (error) => {
    console.log('데이터 요청 실패', error);
  }
});
  • select
    데이터 패칭 성공시에 원하는 데이터 형식으로 변환하여 가져온다.
const { data, isLoading } = useQuery('todos', fetchTodos);
console.log(data.data);

/*[
	{ id : 1, content : 'React Query'},
    { id : 2, content : 'SWR'},
    { id : 3, content : '어렵다😭'},
]*/

👇 원하는 데이터로 가공시키면

const { data, isLoading } = useQuery('todos', fetchTodos, {
  select : (data) => {
    return data.data.map(todo => todo.content);
  }
});
console.log(data);	// [ 'React Query', 'SWR', '어렵다😭' ]

✅ 반환 값

  • data
    서버 요청에 대한 데이터

  • isLoading
    캐시가 없는 상태에서의 데이터 요청 중인 상태 👉 true / false

  • isFetching
    캐시의 유무 상관없이 데이터 요청 중인 상태 👉 true / false

  • isError
    서버 요청 실패에 대한 상태 👉 true / false

  • error
    서버 요청 실패 👉 object

const fetchTodos = () => {
  return axios.get('http://localhost:3000/todos');
};
// 방법1. 구조분해 X
const responseData = useQuery('todos', fetchTodos);
// 방법2. 구조분해 O
const { data, isLoading, isFetching, isError, error } = useQuery('todos', fetchTodos);

if (isLoading) {
  return <h2>Loading...</h2>
}

if (isError) {
  return <h2>{error.message}</h2>
}

console.log(responseData);

✨ 기본 문법

병렬 처리 ( Parallel )

데이터 패칭을 여러 개 실행하고 싶다면 useQuery를 병렬로 사용한다.

function App(){
  const userQuery = useQuery('users', fetchUsers);
  const todoQuery = useQuery('todos', fetchTodos);
  const projecstQuery = useQuery('projects', fetchProjects);
}

👇 쿼리 처리의 동시성을 극대화 시키기 위해 병렬 처리를 시도해보자.

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

렌더링이 계속해서 일어나는 동안 쿼리가 수행된다면, 이 로직이 hook에 위배될 수 있기에 useQueries를 사용하여 병렬 처리를 구현해야 한다.

동기적 실행

동기적으로 수행되어야 하는 작업을 위해서는 enabled 속성을 활용한다.

👇 useQuery에서 enabled 속성 값이 true일 때 실행되며, 코드를 살펴보자.

const fetchUserById = (id) => {
  return axios.get(`http://localhost:3000/users/${id}`);
};

const fetchTodosByTodoId = (todoId) => {
  return axios.get(`http://localhost:3000/todos/${todoId}`);
};

const DependentQueries = ({id}) => {
  const { data : user } = useQuery(['user', id], () => fetchUserById(id));
  const todoId = user?.data.todoId;
  
  useQuery(['todos', todoId], () => fetchTodosByTodoId(todoId), {
    enabled : !!todoId,
  });
  return <div>DependentQueries</div>
};
export defalut DependentQueries; 

id로 유저의 정보를 가져오는 함수, todoId로 할 일의 정보를 가져오는 함수를 만들고 동기적인 작업을 위한 DependentQueries에 이를 사용한다.

유저의 정보를 가져오게 된다면, todoId의 값이 존재하고, 그렇지 않다면 todoId에 undefined가 저장될 것 이다.

따라서 이를 이중 부정을 통해서 userId를 true, false로 판별하여 useQuery의 실행 유무를 판단한다.

재시도

useQuery 요청이 실패하는 경우, 지정한 최대 연속 요청 한계까지 재요청을 보낸다.
( 기본적으로 3번은 실행된다)

  • retry 재요청 횟수이다.
  • retryDelay 다음 재요청 까지의 딜레이 시간이다.
const userQuery = useQuery(['users',1], fetchUsers,
                           { retry : 10, retryDelay : 400});

💡 useMutation

POST, PUT, DELETE와 같은 변경 및 수정작업을 할 때 사용되는 훅이며. 데이터를 변경하고 삭제한다.

const requestData = useMutation(API 호출 함수, 콜백);
  • API 호출 함수
    특정 endpoint로 요청을 보내고 Promise를 반환하는 함수이다.
  • 콜백
    라이프사이클에 따라 로직 작성해야 한다.

mutate

👇 useMutation을 통해 mutation 객체를 생성하고, useQuery와 같은 반환값을 받으며, mutate메서드를 통해 API 요청 함수를 호출하여 요청한다.

import { useMutation } from 'react-query';

const AddTodo = () => {
  const addTodoList = (todo) => {
    return axios.post('http://localhost:3000/todos', todo);
  };
  
  const { mutate : addTodo, isLoading, isError, error } = useMutation(addTodoList);
  
  const handleAddTodoClick = () => {
    const todo = { 날짜, 할일 };
    addTodo(todo);
  };
  
  if (isLoading) {
    return <h2>Loading...</h2>
  }
  
  if (isError) {
    return <h2>{ error.message }</h2>
  }
};

❗️ useQuery vs useMutation ❗️

  • useQuery는 Server State와 Client State의 정확성을 유지하기 위해 사용한다.
  • useMutation으로 변경을 가하기 때문에 같은 서버 데이터를 공유하는 다른 클라이언트에도 영향을 미친다.
  • useMutation에서는 custom으로 retry 옵션을 따로 줄 수 있다.
    👉 한 번 요청에 실패하면 그대로 실패이다.

Invalidation

위의 코드에서 한 가지 문제점은 Add Todo를 수동으로 클릭하여 Fetch를 통해 화면에 보여진다는 게 불편하다.

👇 이를 해결하기 위해서 이 전에 캐싱된 쿼리가 있다면 이를 직접 useQueryClient로 무효화를 시키고, 데이터를 새로 패칭하도록 해야 한다.

import { useMutation, useQueryClient } from 'react-query';

const AddTodo = () => {
  // 쿼리 무효화를 위한 셋팅
  const queryClient = useQueryClient();
  const addTodoList = (todo) => {
    return axios.post('http://localhost:3000/todos', todo);
  };
  
  const { mutate : addTodo, isLoading, isError, error } = useMutation(addTodoList,
                                                                      {
    onSuccess : () => {
      // 캐시가 있는 모든 쿼리 무효화
      queryClient.invalidateQueries();
      // queryKey가 'to-dos'로 시작하는 모든 쿼리 무효화
      queryClient.invalidateQueries('to-dos');
    }
  });
  
  const handleAddTodoClick = () => {
    const todo = { 날짜, 할일 };
    addTodo(todo);
  };
  
  if (isLoading) {
    return <h2>Loading...</h2>
  }
  
  if (isError) {
    return <h2>{ error.message }</h2>
  }
};

👇 더불어 mutate가 실행되기 전에 성공 여부, 끝과 같은 라이프사이클에 따라 콜백함수를 작성할 수도 있다.

useMutation(addTodoList, {
  onMutate : (variables) => {
    // mutate 함수가 실행되기 전에 실행
    // addTodo에 들어가는 인자
    console.log(variables)
    
    // context 로 참조 가능
    return { date : "2022-04-23", content : "dance" };
  },
  onSuccess : (data, variables, context) => {
    // 성공
    console.log(`${context.date} - ${context.content}`);
  },
  onError : (error, variables, context) => {
    // 에러 발생
  },
  onSettled : (data, error, variables, context) => {
    // 성공 또는 실패 상관없이 실행
  },
})

🤔 언제 써야하는 걸까?

  • 수많은 전역상태가 API 통신과 엮여있어 비대해진 Store를 고민하고 있을 떄
  • API 통신 관련 코드를 보다 간단히 구현하고 싶을 때
  • Front-End에서 데이터 Caching 전략을 고민하고 있을 때

📚 학습할 때, 참고한 자료 📚

profile
즐기면서 정확하게 나아가는 웹프론트엔드 개발자 https://happhee-dev.tistory.com/ 로 이전하였습니다
post-custom-banner

0개의 댓글