[React] 리액트 쿼리

배성규·2023년 2월 28일

리액트

목록 보기
1/5

얼마전에 상태 관리 라이브러리인 recoil에 대해 학습했는데, 다른 라이브러리에 대해서도 궁금증이 생겼다. 그 중 react query도 있다는 것을 알게 되었고 카카오와 같은 회사들에서 적극 도입했다는 것을 들었고 다양한 회사에서 react query를 경험해본 사람을 선호한다는 내용을 보고 학습해보도록 했다.

리액트 쿼리

  • React Querydata fetching, 캐싱, 동기화, 서버 쪽 데이터 업데이트 등을 쉽게 만들어주는 리액트 라이브러리이다.
  • Redux, Recoil, Mobx는 클라이언트 데이터를 관리하기에는 적합하나 서베 데이터를 관리하기에는 적합하지 않다
    • Redux를 활용하여 프로젝트의 전역상태를 관리를 할때 서버 데이터를 활용하려면 Redux-saga, Redux-Thunk 혹은 RTK-Query같은 또 다른 미들웨어를 사용해야한다.

리액트쿼리가 만들어진 동기

  • 리액트 자체가 데이터를 패칭해오거나 업데이트 하는 옵션을 제공하지 않으므로 개발자들이 각자의 방식으로 http 통신 로직을 작성해야 했다.
  • 리덕스 같은 전역 상태관리 라이브러리들이 클라이언트 상태 값에 대해서는 잘 작동하나, 서버 상태에 대해서는 잘 작동하지 않는다.
    • 서버 데이터는 항상 최신 상태임을 보장하지 않는다. 명시적으로 fetching을 수행해야 최신 데이터로 전환된다.
    • 네트워크 통신은 최소한으로 줄이는 것이 좋으나, 복수의 컴포넌트에서 최신 데이터를 받아오기 위해 fetching을 여러번 수행하는 낭비가 발생할 수 있다.

리액트 쿼리의 라이프 사이클

  • fetching : 데이터 요청 상태
  • fresh : 데이터가 프레시한 상태 (만료되지 않은 상태)
    • 컴포넌트의 상태가 변경되더라도 데이터를 다시 요청하지 않음.
    • 새로고침하면 다시 fetching한다.
  • stale("케케묵은"이라는 형용사) : 데이터가 만료된 상태
    • 데이터가 만료되었다는 것 => 한번 프론트로 데이터를 주면 그 사이에 다른 유저가 데이터를 추가,수정,삭제 등을 할 수 있기 때문에 만료되었다고 한다. (최신화가 필요한 데이터)
    • 컴포넌트가 마운트, 업데이트되면 데이터를 다시 요청
    • fresh에서 stale로 넘어가는 시간 (기본값 : 0)
  • inactive : 사용하지 않는 상태
    • 일정 시간이 지나면 가비지 콜렉터가 캐시에서 제거한다. (기본값 : 5분)
  • delete 가비지 콜렉터에 의해 캐시에서 제거된 상태

React-query의 장점

  • 리액트 앱 내에서 데이터 패칭, 캐싱, 동기적, 서버의 상태 업데이트를 좀 더 용이하도록 만들어준다.
  • 프로젝트 구조가 기존보다 단순해져 어플리케이션의 유지 보수가 쉽고 새로운 긴으을 쉽게 구축할 수 있다.
  • 리액트 쿼리는 별도의 설정 없이 바로 사용이 가능하다.
  • 캐싱을 효율적으로 관리해준다.
  • 같은 데이터에 대한 여러번의 요청이 있을 시 중복을 제거한다.
  • 백그라운드에서 자동적으로 오래된 데이터를 업데이트해준다.
  • 페이징처리, 지연 로딩 데이터와 같은 성능 최적화를 해준다.
  • 서버 쪽 데이터를 가비지 컬렉션을 이용하여 자동으로 메모리를 관리한다.

예시 코드

import axios from 'axios';
import {
    QueryClient,
    QueryClientProvider,
    useMutation,
    useQuery,
    useQueryClient,
}from 'react-query';
/*
리액트 쿼리는 내부적으로 queryClient를 사용하여
각종 상태를 저장하고, 부가 기능을 제공한다.
*/
const queryClient = new QueryClient();

function App(){
    return(
        <QueryClientProvider client = {queryClient}>
            <Menus/>
        </QueryClientProvider>

    )
}

function Menus(){
    const queryClient = useQueryClient();

    // /menu api에 get요청을 보내 서버 데이터를 가져온다.
    const { data } = useQuery('getMenu', () => {
        axios.get('/menu').then({data} => data),
    });

    // /menu api에 post 요청을 보내 서버데이터를 저장한다.
    const { mutate } = useMutation((suggest) => axios.post('/menu', {suggest}),{
        /*
        post 요청이 성공하면 위 useQuery의 데이터를 초기화한다.
        데이터가 초기화되면 useQuery는 서버의 데이터를 다시 불러온다.
        */
        onSuccess : () => queryClient.invalidateQueries('getmenu'),
    });

  return (
    <div>
      <h1> Tomorrow's Lunch Candidates! </h1>
      <ul>
        {data.map((item) => (
          <li key={item.id}> {item.title} </li>
        ))}
      </ul>
      <button
        onClick={() =>
          mutate({
            id: Date.now(),
            title: 'Toowoomba Pasta',
          })
        }
      >
        Suggest Tomorrow's Menu
      </button>
    </div>
  );
}

React query의 쿼리 요청

const { data } = useQuery(
  queryKey, // (required) query 요청에 대한 응답 데이터를 캐시할 때 사용할 유니크 키
  fetchFn, // (required) query 요청을 수행하기 위한 Promise를 return 하는 함수
  options
); // (optional)  useQuery에서 사용되는 옵션 객체
  • 리액트 쿼리는 이 유니크 키로 서버 상태를 로컬에 캐시하고 관리한다.
  • 데이터를 get하기 위한 api. post, update는 useMutation사용
  • useQuery는 비동기로 작동한다.
    • 한 컴포넌트에 여러 useQuery가 있다면 하나가 끝나고 다음 useQuery가 실행되는 것이 아닌 두 개의 useQuery가 동시에 실행된다. 단, 여러개의 비동기 쿼리가 있으면 useQueries가 더 좋다.
  • enabled를 사용하면 useQuery를 동기적으로 사용 가능하다.
const { status, data, error, isFetching, isPreviousData } = useQuery(
  ["projects", page],
  () => fetchProjects(page),
  { keepPreviousData: true, staleTime: 5000 }
);

// 예외처리는 reject 대신 무조건 throw Error 처리
const { error } = useQuery(["todos", todoId], async () => {
  if (somethingGoesWrong) {
    throw new Error("err");
  }
  return data;
});
  • unique key : 한번 fresh가 되었다면 계속 추적이 가능하다. 주로 배열을 사용하고, 배열 요소로 쿼리의 이름을 나타내는 문자열과 프로미스를 리턴하는 함수의 인자로 쓰이는 값을 넣음
  • useQuery 반환값 : 객체, 요청의 상태를 나타내는 몇가지 프로퍼티, 요청의 결과나 에러값을 갖는 프로퍼티도 포함
    • error, data, isFetching => 런타임간 무조건 요청이 1회 이상 발생했으면 값이 존재한다.
  • 쿼리 요청 함수의 상태를 표현하는 상태값은 4가지. status 프로퍼티에서는 문자열로, 상태 이름 앞에 is를 붙여 불리언값으로 해당 상태인지 아닌지 평가 가능하다.
    • idle : 쿼리 data가 하나도 없고 비었을 때, {enabled : false} 상태로 쿼리가 호출되었을 때 이 상태로 시작됨
    • loading : 로딩 중일 때
    • error : 에러 발생했을 때
    • success : 요청 성공했을 때
  • 주요 쿼리 옵션
    • enabled : True로 설정하면 자동으로 쿼리의 요청 함수가 호출되는 일이 없다.
    • keepPreviousData : success와 loading 사이 널뛰기 방지
    • placeholderData : mock 데이터 설정도 가능. 그러나 캐싱이 안된다.
    • initialData : 초기값 설정
    • 쿼리에 여러가지 옵션 설정을 해서 원하는대로 데이터를 관리할 수 있음

useQuery 동기적으로 실행

  • enabled옵션을 사용하면 useQuery를 동기적으로 사용 가능하다.
  • useQuery의 3번재 인자로 옵션값이 들어가는데, 그 옵션의 enabled에 값을 넣으면 값이 true일때 useQuery를 실행
const { data: todoList, error, isFetching } = useQuery("todos", fetchTodoList);
const {
  data: nextTodo,
  error,
  isFetching,
} = useQuery("nextTodos", fetchNextTodoList, {
  enabled: !!todoList, // true일 때 fetchNextTodoList 실행
});

useQueries

  • useQuery를 비동기로 여러개 실행하는 경우 변수를 다 기억해야하고, 3개 변수에 대한 로딩, 성공, 실패처리를 모두 해야한다.
const usersQuery = useQuery("users", fetchUsers);
const teamsQuery = useQuery("teams", fetchTeams);
const projectsQuery = useQuery("projects", fetchProjects);
// useQuery를 하나로 묶을 수 있는데, 하나의 배열에 각 쿼리에 대한 상태 값이 객체로 들어온다.
// 아래 예시는 롤 룬과, 스펠을 받아오는 예시입니다.
const result = useQueries([
  {
    queryKey: ["getRune", riot.version],
    queryFn: () => api.getRunInfo(riot.version),
  },
  {
    queryKey: ["getSpell", riot.version],
    queryFn: () => api.getSpellInfo(riot.version),
  },
]);

useEffect(() => {
  console.log(result); // [{rune 정보, data: [], isSucces: true ...}, {spell 정보, data: [], isSucces: true ...}]
  const loadingFinishAll = result.some((result) => result.isLoading);
  console.log(loadingFinishAll); // loadingFinishAll이 false이면 최종 완료
}, [result]);

useMutation

  • 값을 바꿀때 사용하는 api(create, update, delete하며 서버상태에 사이드 이펙트를 일으키는 경우에 사용)
  • useMutation이 반환하는 객체 프로퍼티로 제공되는 상태 값은 useQuery와 동일하다.
  • mutation.reset : 현재의 error와 data를 모두 지울 수 있다.
import { useState, useContext, useEffect } from "react";
import loginApi from "api";
import { useMutation } from "react-query";

const Index = () => {
  const [id, setId] = useState("");
  const [password, setPassword] = useState("");

  const loginMutation = useMutation(loginApi, {
    onMutation: (variable) => {
      console.log("onMutate", variable);
      // variable : { loginId : 'xxx', password : 'xxx' }
    },
    onError: (error, variable, context) => {
      //error
    },
    onSuccess: (data, variables, context) => {
      console.log("success", data, variables, context);
    },
    onSettled: () => {
      console.log("end");
    },
  });

  const handleSubmit = () => {
    loginMutation.mutate({ loginId: id, password });
  };

  return (
    <div>
      {loginMutation.isSuccess ? "success" : "pending"}
      {loginMutation.isError ? "error" : "pending"}
      <input type="text" value={id} onChange={(e) => setId(e.target.value)} />
      <input
        type="password"
        value={password}
        onChange={(e) => setPassword(e.target.value)}
      />
      <button onClick={handleSubmit}>로그인</button>
    </div>
  );
};

export default Index;
  • react-query의 장점은 update 후에 get함수를 간단히 재실행할 수 있는 것이다.
  • mutation함수가 성공하면, unique key로 맵핑된 get함수를 invalidateQueries에 넣어주면 된다.
const mutation = useMutation(postTodo, {
  onSuccess: () => {
    // postTodo가 성공하면 todos로 맵핑된 useQuery api함수를 실행한다.
    queryClient.invalidateQueries("todos");
  },
});
  • 만약, mutation에서 return된 값을 이용하여 get함수의 파라미터를 변경해야할 경우, setQueryData를 사용한다.
const queryClient = useQueryClient();

const mutation = useMutation(editTodo, {
  onSuccess: (data) => {
    // data가 fetchTodoById로 들어간다
    queryClient.setQueryData(["todo", { id: 5 }], data);
  },
});

const { status, data, error } = useQuery(["todo", { id: 5 }], fetchTodoById);

mutation.mutate({
  id: 5,
  name: "nkh",
});

invalidation

  • stale 쿼리를 폐기한다.
  • 쿼리 데이터가 요청으로 서버에세 바뀌게 되면, 백그라운드에 남아있는 데이터는 과거 데이터가 되어 어플리케이션에서 쓸모 없어지는 상황이 발생할 수 있다.
  • invalidateQueries 메소드를 사용하면 개발자가 명시적으로 query가 stale되는 지점을 찝어줄 수 있음. 해당 메소드가 호출되면 쿼리가 바로 stale되고, 리패치가 진행된다.
  • 쿼리에 특정 키가 공통으로 들어가있으면 한번에 invalidation이 가능
queryClient.invalidateQueries();
queryClient.invalidateQueries("todos");
queryClient.invalidateQueries({
  predicate: (query) =>
    query.queryKey[0] === "todos" && query.queryKey[1]?.version >= 10,
});

마무리

recoiluseState와 비슷한 느낌이어서 쉽게 이해하고 적용할 수 있었는데, 생각보다 리액트 쿼리는 좀 더 복잡한 것 같다. 실제로 적용해보면서 부딪혀보는 것이 학습하는 것에 도움이 될 것이라고 생각한다.

참고자료

profile
FE 유망주🧑‍💻

0개의 댓글