https://www.youtube.com/watch?v=MArE6Hy371c
상태관리란?
주어진 시간에 시스템을 나타내는 것
Redux VS SWR VS React Query
셋 다 상태관리다
Redux - Saga 혹은 Thunk가 있어야지 비동기 처리 가능, 코드가 길다
SWR 하고 React Query는 위를 해결하고자 나왔다
1. SWR이 React Query보다 훨씬 가볍다
2. SWR은 가비지컬렉션이 없다.
3. SWR은 mutation hook을 지원하지 않는다.
React Query
렌더링 퍼포먼스를 높이기 위해 데이터를 최적화한 점과 자동으로 가비지 컬렉션을 지원
server state가 가지는 많은 차이점들을 대응할 수 있도록, 잘 정리된 내장 함수들과 훌륭한 캐싱 기능을 제공
이들은 api 통신 로직 자체를 다루지는 않습니다. 이들이 제공하는 정확한 기능은 "비동기 함수와 그것의 리턴 값"에 대한 데이터 캐싱 등의 기능이고, 실제로 Promise를 리턴하는 axios.get('https://...') 등의 코드는 개발자가 직접 구현
꼭 기억하기
Quary: CRUD에서 R을담당, 데이터 fetching용
- 쿼리 key
- 리액트 쿼리는 Query Key에 따라 query caching을 관리
- 쿼리 function
- Promise를 반환하는 함수, 데이터를 resolve, error throw 함
- useQuery는 뭘 반환?
- data, error, isFetching, status(isLoading, isSuccess등) refetch, remove 등등등 을 반환한다.
Mutation: CRUD에서 CUD를 담당
React-Query가 주장하는 Global State 개념
- Global State라는 말을 쓰지 말자 : 전역 state는 Client와 Server로 분류할 수 있고, 이 두 state는 다른 방식으로 다뤄져야 효율적인 앱을 만들 수 있다.
- Server-State : 서버에서 가져오는 데이터들도 하나의 상태
- Server-State과 Client-State의 구분
- Client State : 세션간 지속적이지 않는 데이터, 동기적, 클라이언트가 소유, 항상 최신 데이터로 업데이트(렌더링에 반영)
- ex) 리액트 컴포넌트의 state, 동기적으로 저장되는 redux store의 데이터
- Server State : 세션간 지속되는 데이터, 비동기적, 세션을 진행하는 클라이언트만 소유하는게 아니고 공유되는 데이터도 존재하며 여러 클라이언트에 의해 수정될 수 있음, 클라이언트에서는 서버 데이터의 스냅샷만을 사용하기 때문에 클라이언트에서 보이는 서버 데이터는 항상 최신임을 보장할 수 없음.
- ex) 리액트 앱에서는 비동기 요청으로 받아올 수 있는, 백엔드 DB에 저장되어있는 데이터
- Client State : 세션간 지속적이지 않는 데이터, 동기적, 클라이언트가 소유, 항상 최신 데이터로 업데이트(렌더링에 반영)
- Redux 같은 전역 상태관리 라이브러리들이 클라이언트 상태값에 대해서는 잘 작동하지만, 서버 상태에 대해서는 그렇게 잘 작동하지 않는다. Server State는 Client State와 완전 다르기 때문이다
- 서버 데이터는 항상 최신 상태임을 보장하지 않는다. 명시적으로 fetching을 수행해야만 최신 데이터로 전환된다.
- ex) 매번 게시물 데이터 가져올때마다 useEffect()로 가져올때
- 네트워크 통신은 최소한으로 줄이는게 좋은데, 복수의 컴포넌트에서 최신 데이터를 받아오기 위해 fetching을 여러번 수행하는 낭비가 발생할 수 있다.
- 서버 데이터는 항상 최신 상태임을 보장하지 않는다. 명시적으로 fetching을 수행해야만 최신 데이터로 전환된다.
중요한 기본 사항
- Query들은 4개의 상태를 가지며, useQuery가 반환하는 객체의 프로퍼티로 어떤 상태인지 확인이 가능하다.
- fresh : 새롭게 추가된 쿼리 인스턴스 → active 상태의 시작, 기본 staleTime이 0이기 때문에 아무것도 설정을 안해주면 호출이 끝나고 바로 stale 상태로 변한다. staleTime을 늘려줄 경우 fresh한 상태가 유지되는데, 이때는 쿼리가 다시 마운트되도 패칭이 발생하지 않고 기존의 fresh한 값을 반환한다.
- fetching : 요청을 수행하는 중인 쿼리
- stale : 인스턴스가 존재하지만 이미 패칭이 완료된 쿼리. 특정 쿼리가 stale된 상태에서 같은 쿼리 마운트를 시도한다면 캐싱된 데이터를 반환하면서 리패칭을 시도한다.
- inactive : active 인스턴스가 하나도 없는 쿼리. inactive된 이후에도 cacheTime 동안 캐시된 데이터가 유지된다. cacheTime이 지나면 GC된다.
- *어떻게 inactive가 되는가? : pagenation 관련한 예제를 보니, 페이지네이션을 할 때마다 컴포넌트가 재랜더링 되면서 새로운 쿼리가 만들어지고, 저번 랜더링에서 호출했던 쿼리들은 inactive된다. 렌더링간에 다시 호출되지 않고 언마운트되는 쿼리들은 inactive가 되는 듯 보인다.
- 다음 4가지 경우에 리패칭이 일어난다
- 런타임에 stale인 특정 쿼리 인스턴스가 다시 만들어졌을 때
- window가 다시 포커스가 되었을때(옵션으로 끄고 키는게 가능)
- 네트워크가 다시 연결되었을 때(옵션으로 끄고 키는게 가능)
- refetch interval이 있을때 : 요청 실패한 쿼리는 디폴트로 3번 더 백그라운드단에서 요청하며, retry, retryDelay 옵션으로 간격과 횟수를 커스텀 가능하다
아래처럼 재호출을 할수 있다.
// 재호출 횟수를 옵션으로 커스텀해줄 수 있다.
const result = useQuery(['todos', 1], fetchTodoListPage, {
retry: 10, // 에러를 display할 때까지 10번을 더 호출한다.
})
다른데서는 5가지의 경우라고 하기도 한다.
React Query를 통해 관리하는 쿼리 데이터는 라이프사이클에 따라
fetching, fresh, stale, inactive, delete 상태를 가진다.
- fetching - 요청 중인 쿼리
- fresh - 만료되지 않은 쿼리. 컴포넌트가 마운트, 업데이트되어도 데이터를 다시 요청하지 않는다
- stale - 만료된 쿼리. 컴포넌트가 마운트, 업데이트되면 데이터를 다시 요청한다.
- inactive - 사용하지 않는 쿼리. 일정 시간이 지나면 가비지 컬렉터가 캐시에서 제거한다
- delete - 가비지 컬렉터에 의해 캐시에서 제거된 쿼리
React-query 사용법 (useQuery 훅)
- 데이터 캐싱이란?
고속 데이터 스토리지 - 이전에 검색하거나 계산한 데이터를 효율적으로 재사용
- query, mutation, query invalidation 세 개념에 집중하자.
- 타입스크립트로 구현되어 있다. 만약 커스텀 훅을 만들고 싶으면, 결과와 에러 타입을 분명하게 써 줘야 한다.
기본값
- useQuery, useInfiniteQuery 의 staleTime
- 옵션에서 조작 가능하며 default는 5분이다.
설치
npm i react-query
기본 세팅
import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import App from "./App";
import reportWebVitals from "./reportWebVitals";
import { QueryClient, QueryClientProvider } from "react-query";
import { ReactQueryDevtools } from "react-query/devtools";
// 캐시를 관리하기 위해 QueryClient 인스턴스를 사용
const queryClient = new QueryClient();
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<React.StrictMode>
{/* useQuery 훅 안에서 QueryClient 인스턴스에 접근할 수 있도록
QueryClientProvider를 컴포넌트 트리 상위에 추가 */}
<QueryClientProvider client={queryClient}>
<App />
<ReactQueryDevtools initialIsOpen />
</QueryClientProvider>
</React.StrictMode>
);
reportWebVitals();
Api 세팅
import axios from "axios";
const todoApi = axios.create({
baseURL: "http://localhost:3500",
});
export const getTodos = async () => {
const response = await todoApi.get("/todos");
return response.data;
};
export const addTodo = async (todo) => {
return await todoApi.post("/todos", todo);
};
export const updateTodo = async (todo) => {
return await todoApi.patch(`/todos/${todo.id}`, todo);
};
export const deleteTodo = async ({ id }) => {
return await todoApi.delete(`/todos/${id}`, id);
};
export default todoApi;
import React, { useState } from "react";
import { useQuery, useMutation, useQueryClient } from "react-query";
import { getTodos, addTodo, updateTodo, deleteTodo } from "../../api/todosApi";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faTrash, faUpload } from "@fortawesome/free-solid-svg-icons";
const TodoList = () => {
const [newTodo, setNewTodo] = useState("");
const queryClient = useQueryClient();
// 기본적으로 stale 상태입니다. useQuery로 가져온 데이터
const {
isLoading,
isError,
error,
data: todos,
} = useQuery("todos", getTodos);
// 서버 데이터를 가져오는 것은 reactive하게 동작하는 useQuery를 사용하면 되겠지만,
// 서버 데이터 업데이트는 그런 방식으로 사용하기에는 적절하지 않다.
// 데이터 생성/수정/삭제에는 useMutation 훅을 사용하면 된다.
const addTodoMutation = useMutation(addTodo, {
onSuccess: () => {
// 캐시에 있는 모든 쿼리를 무효화한다. - stale 데이터 처리
queryClient.invalidateQueries("todos");
},
onError: () => {
// 에러처리
},
onSettled: () => {
// finally
},
});
const updateTodoMutation = useMutation(updateTodo, {
onSuccess: () => {
// 캐시에 있는 모든 쿼리를 무효화한다.
queryClient.invalidateQueries("todos");
},
});
const deleteTodoMutation = useMutation(deleteTodo, {
onSuccess: () => {
// 캐시에 있는 모든 쿼리를 무효화한다.
queryClient.invalidateQueries("todos");
},
});
const handleSubmit = (e) => {
e.preventDefault();
addTodoMutation.mutate({ userId: 1, title: newTodo, completed: false });
setNewTodo("");
};
const newItemSection = (
<form onSubmit={handleSubmit}>
<label htmlFor="new-todo">Enter a new todo item</label>
<div className="new-todo">
<input
type="text"
id="new-todo"
value={newTodo}
onChange={(e) => setNewTodo(e.target.value)}
placeholder="Enter new todo"
/>
</div>
<button className="submit">
<FontAwesomeIcon icon={faUpload} />
</button>
</form>
);
let content;
if (isLoading) {
content = <p>Loading....</p>;
} else if (isError) {
content = <p>{error.message}</p>;
} else {
content = todos.map((todo) => {
return (
<article key={todo.id}>
<div className="todo">
<input
type="checkbox"
checked={todo.completed}
id={todo.id}
onChange={() =>
updateTodoMutation.mutate({
...todo,
completed: !todo.completed,
})
}
/>
<label htmlFor={todo.id}>{todo.title}</label>
</div>
<button
className="trash"
onClick={() => deleteTodoMutation.mutate({ id: todo.id })}
>
<FontAwesomeIcon icon={faTrash} />
</button>
</article>
);
});
}
console.log(todos);
return (
<main>
<h1>Todo List</h1>
{newItemSection}
{content}
</main>
);
};
export default TodoList;
만약 회원가입/ 이메일체크/ 로그인 같은데 사용하면?
const queryClient = useQueryClient();
const userSignUpMutation = useMutation(__signUpQuery, {
onSuccess: () => {
// 캐시에 있는 모든 쿼리를 무효화한다.
queryClient.invalidateQueries("users");
},
onError: () => {
// 에러처리
},
onSettled: () => {
// finally
},
});
const userEmailCheckMutation = useMutation(__emailCheckQuery, {
onSuccess: () => {
// 캐시에 있는 모든 쿼리를 무효화한다.
queryClient.invalidateQueries("users");
},
onError: () => {
// 에러처리
},
onSettled: () => {
// finally
},
});
const onIdDupCheckHandler = () => {
userEmailCheckMutation.mutate({
email: email,
});
};
const onSubmitHandler = () => {
userSignUpMutation.mutate({
nickName: nickName,
email: email,
number: number,
location: location,
pw: pw,
gender: gender,
birth: birth,
fileImage: register_imageRef.current.url,
});
};
'Web > React' 카테고리의 다른 글
React - CORS 오류 클라에서 처리 - proxy 설정 (0) | 2022.06.25 |
---|---|
React - 회원가입 (0) | 2022.06.25 |
React - Emotion (0) | 2022.06.25 |
React - Loadable Components를 통한 코드 스플리팅 (0) | 2022.06.25 |
React - Prettier, Eslint (0) | 2022.06.24 |