React Query란?
client에서 상태관리 라이브러리를 사용하는 것은 클라이언트에서만 유효하다.
서버의 데이터를 요청하고 클라이언트의 전역 상태로 갱신하는 로직이 추가되면 클라이언트의 상태관리 라이브러리 코드가 복잡해지는 문제가 생긴다.
즉, 리액트 쿼리는 서버 데이터와 클라이언트 데이터를 구분하기 위해 사용한다.
useQuery
컴포넌트나 custom hook에서 query를 구독하기 위해서는 useQuery를 호출해야한다.
이 때, 유니크한 queryKey
와 promise를 반환하는 queryFn
가 있어야 한다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| import { useQuery } from "@tanstack/react-query";
function App() { const info = useQuery({ queryKey: ["todos"], queryFn: fetchTodoList }); }
function Todos() { const { isLoading, isError, data, error } = useQuery({ queryKey: ["todos"], queryFn: fetchTodoList, });
if (isLoading) { return <span>Loading...</span>; }
if (isError) { return <span>Error: {error.message}</span>; }
return ( <ul> {data.map((todo) => ( <li key={todo.id}>{todo.title}</li> ))} </ul> ); }
|
- useQuery의 반환값은 데이터를 사용하기 위한 모든 정보를 담고 있다.
- queryKey: query caching을 queryKey를 통해서 관리한다.
- 쿼리키는 최상위 레벨이어야 하고, string으로 구성된 배열이어야 한다.
- queryKey에는 쿼리함수에서 사용되는 모든 변수가 포함되어야 한다.
1 2 3 4 5 6
| function Todos({ todoId }) { const result = useQuery({ queryKey: ["todos", todoId], queryFn: () => fetchTodoById(todoId), }); }
|
- queryFn: 데이터를 resolve(분해)하거나 에러를 던지는 Promise를 반환하는 함수
- 쿼리가 에러를 가지고 있다는 것을 결정하려면, 쿼리함수가 throw Error 또는 rejected Promise를 반환해야한다.
useMutation
query와 달리 mutation은 데이털를 생성, 갱신, 삭제하거나 서버에서 사이드 이펙트를 수행할 때, 사용된다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| 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 중 error가 발생하여 data를 비우고 싶을 땐 reset을 사용할 수 있다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| const CreateTodo = () => { const [title, setTitle] = useState(""); const mutation = useMutation({ mutationFn: createTodo });
const onCreateTodo = (e) => { e.preventDefault(); mutation.mutate({ title }); };
return ( <form onSubmit={onCreateTodo}> {mutation.error && ( <h5 onClick={() => mutation.reset()}>{mutation.error}</h5> )} <input type="text" value={title} onChange={(e) => setTitle(e.target.value)} /> <br /> <button type="submit">Create Todo</button> </form> ); };
|
mutation의 사이드이펙트 처리
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| useMutation({ mutationFn: addTodo, onMutate: (variables) => {
return { id: 1 }; }, onError: (error, variables, context) => { console.log(`rolling back optimistic update with id ${context.id}`); }, onSuccess: (data, variables, context) => { }, onSettled: (data, error, variables, context) => { }, });
|
- onMutate가 현재 mutate가 발생했을 때, 실행하는 함수이다. 위 코드에서는 롤백할 때, 사용할 데이터를 반환하고 있다.
- 만약 에러가 발생하면, 해당 업데이트를 롤백하는 데이터가 context에 포함되어있다.
- onSettled는 성공하든 실패하든 호출되는 함수다.
만약, 콜백함수에서 Promise를 반환하면, 다음 콜백이 호출되기 전에 대기한다.
1 2 3 4 5 6 7 8 9
| useMutation({ mutationFn: addTodo, onSuccess: async () => { console.log("I'm first!"); }, onSettled: async () => { console.log("I'm second!"); }, });
|
- onSuccess가 먼저 발생하는 이유는 내부 로직때문이 아니라 단순히 코드상 위에 있기 때문이다.