[React] React Query의 동작원리

·2023년 12월 25일
2

React

목록 보기
7/7
post-thumbnail

🎯 일단 사용해보기

0. 설치

yarn add react-query

+ json-server 설치 및 실행

yarn add json-server
yarn json-server --watch db.json --port 3001
//db.json 파일을 db로 사용하고 3001번 포트에서 서버를 시작하겠다

+ axios 설치 및 실행

yarn add axios

1. QueryClientProvider

: 데이터를 읽어오는 기능(QueryClient)를 애플리케이션 전체에 주입하는 API

import React from "react";
import Router from "./shared/router";
import { QueryClient, QueryClientProvider } from "react-query";

const App = () => {
  <QueryClientProvider client={QueryClient}>
    return <Router />;
  </QueryClientProvider>;
};

export default App;

2. useQuery

import { useQuery } from 'react-query';
import { fetchTodoList } from '../api/fetchTodoList';

function App() {
	const info = useQuery('todos', fetchTodoList);
  	//const { isLoading, isError, data } = useQuery("todos", fetchTodoList);
  	...
}

useQuery('todos', fetchTodoList)

(1) useQuery의 첫번째 인자

  • Query Keys
    : refetching / caching 처리 / 애플리케이션 전체 맥락에서 이 쿼리를 공유하는 방법으로 사용된다. (여러 컴포넌트에 존재해도 같은 key면 같은 쿼리 및 데이터를 보장함)

  • Query Keys는 한 단어일수도, 배열일 수도 nested객체 일 수도 있다.

const query1 = useQuery('qk', api); // unique
const query2 = useQuery('qk2', api); // not unique
const query3 = useQuery('qk2', api); // not unique

  : key 이므로 고유한 값을 가져야 한다.

  • 한 단어로 이뤄진 Query Keys
useQuery('todos', ...)

위와 같은 코드가 있다면 내부적으로는 아래처럼 해석된다.

queryKey === ['todos'] 	// 배열 형태로 갖고 있다

  • 배열 형태로 이뤄진 Query Keys
    : 문자, 숫자, object 등등 여러가지를 조합한 배열 형태의 key도 사용 가능
// ID가 5인 todo 아이템 1개
useQuery(['todo', 5], ...)
// queryKey === ['todo', 5]

// ID가 5인 todo 아이템 1개인데, preview 속성은 true야
useQuery(['todo', 5, { preview: true }], ...)
// queryKey === ['todo', 5, { preview: true }]

// todolist 전체인데, type은 done이야
useQuery(['todos', { type: 'done' }], ...)
// queryKey === ['todos', { type: 'done' }]

(2) 두 번째 인자

  • Query Functions
    : promise 객체 리턴
    : promise 객체는 반드시 data resolve하거나 에러를 내야한다
    • resolve : 정상적으로 통신되었음을 의미
    • 오류가 발생한 경우에는 그에 맞는 적절한 오류 처리 관련 로직을 삽입해 처리해줘야한다. (axios, fetch, graphgl...)

(3) useQuery의 결과물

: useQuery를 통해 얻은 결과물은 객체이다.
: 객체 안에는 '조회'를 요청한 결과에 대한 거의 모든 정보가 들어있고, 그 과정에 대한 정보도 들어있다.

  • 조회를 시작하면 isLoadingtrue가 되고
  • 조회 결과 오류가 나면 isErrortrue가, isLoadingfalse가 된다. error 객체를 통해 더 상세한 오류 내용을 확인 할 수 있다.
  • 조회 결과 정상이 되면 isSuccesstrue가 , isLoadingfalse가 된다. data 객체를 통해 좀 더 상세한 조회 결과를 확인할 수 있다.



2. useMutation

: query와 다르게 mutation은 CUD에서 사용됨

  • 예제
// [출처] : 공식문서

function App() {
   const mutation = useMutation(newTodo => {
     return 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>
   )
 }
  • mutation.mutate(인자)
    : 인자는 반드시 한 개의 변수 또는 객체여야 한다.
    : 결과는 객체의 형태로 되어있고
    : 그 객체는 항상 (isIdle | isLoading | isError| isSuccess) 중 하나의 상태이다.



🎯 React Query란?

: 서버 상태 관리를 쉽게 할 수 있게 도와주는 라이브러리 이다.

서버 상태 관리: 서버에 요청하고 응답받는 모든 과정과 연관된 데이터들

  • fetching: 서버에서 데이터 받아오기
  • caching: 서버에서 받아온 데이터를 따로 보관해서 동일한 데이터가 단 시간 내에 다시 필요할 시 서버 요청없이 보관된 데이터에서 꺼내쓰기
  • synchronizing: 서버 상의 데이터와 보관 중인 캐시 데이터(서버 상태)를 동일하게 만들기(동기화)
  • updating: 서버 데이터 변경 용이 (mutation & invalidateQueries)

기존 미들웨어의 한계

: 서버와의 API통신과 비동기 데이터 관리를 위해 Redux-thunk, Redux-Saga등의 미들웨어를 사용할 수 있다. 하지만 문제가 있는데,,

  • 보일러 플레이트 코드의 양이 너무 많다.
  • Redux는 비동기 데이터 관리를 위한 전문 라이브러리가 아니다. (규격화 문제)

리액트 쿼리의 장점

  • 보일러 플레이트를 만들다가 오류날 일이 없다
  • 사용방법이 thunk에 비해 쉽고 직관적이다.

기존 미들웨어와의 차이

// React Query 미사용 시
const [todos, setTodos] = useState([]);
const [isLoading, setIsLoading] = useState(false);

const getTodos = async () => {
  setIsLoading(true);
	const data = await axios.get(`${API_URL}/todos`).then(res => res.data);
	setTodos(data);
  setIsLoading(false);
}
useEffect(() => {
	getTodos();
}, []);


// React Query 사용 시
const getTodos = () => axios.get(`${API_URL}/todos`).then(res => res.data);

const { data: todos, isLoading } = useQuery(["todos"], getTodos);

+읽어볼 만한 글) 카카오페이 프론트엔드 개발자들이 React Query를 선택한 이유


✔ 주요 키워드

  • Query
    : 어떤 데이터에 대한 요청
    : axios의 get요청과 유사

  • Mutation
    : 데이터 그룹 자체를 변경하는 것 (Create, Update, Delete)
    : axios의 post, put, patch, delete 요청과 유사

  • Query Invalidation
    : 쿼리 무효화
    : 기존에 가져온 Query는 서버 데이터이기 때문에 언제든지 변경이 있을 수 있다. 최신 상태가 아닐 수 있으므로 기존의 쿼리를 무효화한 후 최신화 해야 하는데 React Query는 이런 과정을 알아서 해준다.


✔ SWR 전략

stale-while-revalidate : 신규 데이터가 도착하는 동안 일단 기존 캐싱된 데이터를 사용하도록 하는 전략

서버의 헤더 응답 설정 Cache-control에서 아이디어 기원

⬆️ 클라이언트가 0~1초 사이에 다시 데이터를 요청하면, 서버 호출없이 캐시 데이터를 바로 사용

⬆️ 클라이언트가 1 ~ 60s 사이에 다시 데이터 요청하면, 일단 캐시 데이터를 사용하고 서버에서 신규데이터를 주면 그것으로 교체

✔ 캐시 데이터 저장

  • QueryClientProvider는 React Context API를 내부적으로 사용한다.
  • QueryCLientProvider의 자식으로 있는 모든 컴포넌트들은 캐시 데이터에 접근할 수 있다.
  • 페이지 컴포넌트 외부에 상태가 존재한다는 점에서 캐시 데이터는 전역 상태로 볼 수 있다.
//App.jsx
const queryClient = new QueryClient();

const App = () => {
  	<QueryClientProvider client = {queryClient}>
      	<Router/>
  	</QueryClientProvider>
	};
}

✔ useQuery에서 자주 사용하는 옵션들

① enabled

useQuery(["todos"], getTodos, {enable: true})

: boolean 타입 (true / false)
: true일 경우에만 queryFn 실행
: default 값은 true, useQuery 자동 실행됨

예제1 (Disabling / Pausing Queries): 이벤트 발생 시에만 수동 실행하고 싶을 때
const {data, refetch} = useQuery(["todos"], getTodos, {enabled: false});

return (
	<div>
  		<button onClick = {() => refetch()}>
  			데이터 불러오기
        </button>
	</div>
);
예제2 (Dependent Queries): useQuery가 2개 이상이고 실행순서 설정이 필요할 때
//Get user
const {data: user} = useQuery({queryKey: ['user', email], queryFn: getUserByEmail});

const userId = user?.id;

//Get user's projects
const { status, fetchStatus, data: projects }
	= useQuery({ queryKey: ['projects: userId'], queryFn: getProjectsByUser, enabled: !!userId});
// userId가 존재하기 전까지 이 쿼리는 실행되지 않음
// !!userId === Boolean(userId)

② select

queryFn에 의해 리턴된 값을 변형시킨 후에 useQuery의 리턴 data로 넘겨줌
(단, cache data는 queryFn에서 리턴 받은 값 그대로 이다.)

import {useQuery} from "react-query"

function User(){
  const { data } = useQuery( ['user'], fetchUser, { select: (user) => user.username, });
  return <div>Username: {data}</div>;
}

✔ react Query의 데이터 흐름

"오래된 것 먼저, 리렌더링 되면서 새 것으로 교체"



📌Tanstack Query ?

: React Query v4 부터 라이브러리 이름이 Tanstack Query로 변경되었다. React 뿐 아니라 Vue 같은 다른 SPA 프레임워크에도 적용!!

// "react-query": "^3.39.3"
yarn add react-query

// "@tanstack/react-query": "^4.29.19"
yarn add @tanstack/react-query

+ v4부터는 query key를 반드시 배열 형태로 써야 함


📌React Query LifeCycle

  • fresh: 새거가 필요하지 않은 상태
  • state: 새거가 필요한 상태
  • 기본 설정 (default config)
기본설정의미
staleTime: 0useQuery 또는 useInfiniteQuery에 등록된 QueryFn을 통해 fetch해 온 데이터는 항상 stale data 취급
refetchOnMount: trueuseQuery 또는 useInfiniteQuery가 있는 컴포넌트가 마운트 시 stale data를 refetch 자동 실행
refetchOnWindowFocus: true실행중인 브라우저 화면은 focus할 때마다 stale data를 refetch 자동 실행
refetchOnReconnect : trueNetwork가 끊겼다가 재연결되었을 때 stale data를 refetch 자동 실행
cacheTime: 5분 (1000 60 5ms)useQuery 또는 useInfiniteQuery가 있는 컴포넌트가 언마운트 되었ㅇ르 때 inactive query라고 부르며, inactive 상태가 5분 경과 후 GC(Garbage Collector)에 의해 cache data 삭제 처리
retry: 3useQuery 또는 useInfiniteQuery에 등록된 queryFn이 API 서버에 요청을 보내서 실패하더라도 바로 에러를 띄우지 않고 총 3번까지 재요청을 자동으로 사용
  • staleTime: 얼마의 시간이 흐른 뒤에 stale취급할 것인가 (default: 0)

    staleTime > 0이면, fresh data
    staleTime = 0이면, stale data

  • cacheTime: inactive된 이후로 메모리에 얼마만틈 있을 것인가 (default: 5분, cacheTime이 0이 되면 삭제처리됨)



📌isLoading vs isFetching

  • isLoading: 새로운 캐시 데이터를 서버에서 받고 있는가
  • isFetching: 서버에서 데이터를 받고 있는가

: 캐시 데이터가 있는 경우 isLoadingfalse, isFetchingtrue이다.



📌useQuery 실행 시cacheTime > 0인 것과 queryFn

  • useQuery 실행 시 cacheTime > 0인 것과 queryFn 실행은 관계 없다. (stateTime과 관련 있음)
  • cacheTime > 0이면 캐시 데이터가 존재하고, 이 경우 useQuery를 실행하면 stale data를 우선 받고 cacheTime이 0이면 queryFn을 실행한 리턴 값으로 리렌더링하면서 바꿔준다.
  • cacheTime이 0이 되면 캐시 데이터가 삭제되기 때문에 useQuery로 data 호출 시 undefined 값을 우선 받고 queryFn을 실행한 리턴 값으로 리렌더링 하면서 바꿔준다.

0개의 댓글