React에서 전역상태를 정할 때, 두 가지 분류가 있습니다.
하나는 서버 쪽(API 응답) 데이터, 하나는 클라이언트 쪽 상태입니다.
페이지
👉List
, List
👉List Item
으로 state를 props로 전달해야 합니다. 이러한 Props Chaining
은 코드 가독성이 매우 떨어지기 때문에, 우리는 API 응답을 전역 상태로 관리하게 됩니다.React Query는 전역 상태 중 서버 쪽(API 응답) 데이터를 전담하는 라이브러리입니다.
Zustand는 전역 상태 관리 라이브러리인데, Redux
나 다른 라이브러리들에 비해 사용하기에 쉽고 가볍습니다. React Query가 이미 서버 쪽 데이터를 전담하므로, Zustand를 사용해서 클라이언트 쪽 상태만을 관리합니다.
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
const queryClient = new QueryClient();
const App = () => {
return (
<QueryClientProvider client={queryClient}>
<ReactQueryDevtools initialIsOpen={true} buttonPosition="bottom-right" />
</QueryClientProvider>
)
}
state를 사용해야 하는 최상단에서 ( 저는 App.jsx에서 ) QueryClientProvider
를 설정해줍니다.
그리고 reactQueryDevtools
도 추가로 사용합니다.
아래 이미지는 DevTools의 UI입니다.
어떤 Query가 실행되었는지, 해당 Query의 상태가 어떤 상태인지, state의 세부적인 내용들도 devtools를 통해 쉽게 확인할 수 있습니다.
위에 올려드린 예제에서 API(=service) 코드는 아래와 같이 api폴더에 모아두었습니다.
useQuery
, useMutation
코드도 Hook으로 분리하고 싶기 때문에, src 하위에 queries 폴더를 생성하고, 안에 react query hook 코드를 작성했습니다.
import { useMutation } from "@tanstack/react-query";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { TodoApi } from "../api/TodoApi";
export const useTodoListQuery = () => {
return useQuery({
queryKey: ['todo','list'],
queryFn: async () => {
const res = await TodoApi.getAllTodoList();
return res;
},
});
};
export const useTodoItemQuery = (id) => {
return useQuery({
queryKey: [todo,item,{id}],
queryFn: async () => {
const res = await TodoApi.getTodoItem(id);
return res;
},
cacheTime: 0,
});
};
export const useUpdateTodoMutation = (...) => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (params) => TodoApi.updateTodoItem(params),
onSuccess: () => {
// Mutation 에 성공한 뒤 실행됨
queryClient.invalidateQueries({
queryKey: ['todo','list'], // fetchAllTodoList 의 데이터를 무효화 시킴.
}); ...
},
...
});
};
이제 작성한 query hook을 사용해서 데이터를 불러오겠습니다.
//src/pages/TodoPage.jsx
import TodoListItem from "./components/TodoListItem";
import classes from './TodoPage.module.scss';
import {useTodoListQuery} from "../../query/useTodoQuery";
function TodoPage() {
const { isLoading, error, data } = useTodoListQuery();
if (isLoading) return "Loading...";
if (error) return "An error has occurred: " + error.message;
return (
<div className={classes.pagewrapper}>
<h1>오늘 할 일</h1>
{
data.data.map((data, idx)=><TodoListItem key={idx} item={data}/> )
}</div>
);
}
export default TodoPage;
// src/pages/todoPage/components/TodoEditModal.jsx
import React, {useEffect, useState} from "react";
import classes from "./TodoEditModal.module.scss";
import {UiStore} from "../../../store/UiStore";
import {useTodoItemQuery, useUpdateTodoMutation} from "../../../query/useTodoQuery";
const TodoEditModal = ({id}) => {
const modalClose = UiStore(state => state.actions.modalClose);
const [inputValue, setInputValue] = useState({
id: id,
userId: 0,
title: '',
completed: false,
});
const {isLoading, error, data} = useTodoItemQuery(id);
const updateData = useUpdateTodoMutation(handleCloseClick);
/* updateData.mutate : updateData.mutate() => mutationFn을 실행한다.
updateData.mutateAsync : .mutate와 같으나 promise를 반환한다. */
async function handleUpdateClick() {
updateData.mutate({
id: id,
userId: data.data.userId,
title: inputValue.title,
completed: inputValue.completed,
});
}
function handleCloseClick() {
modalClose();
}
useEffect(() => {
if (data?.data) {
setInputValue({
id: id,
userId: data.data.userId,
title: data.data.title,
completed: data.data.completed,
})
}
}, [data]);
if (isLoading) return "Loading...";
if (error) return "An error has occurred: " + error.message;
return (
<div className={classes.wrapper}>
<div className={classes.inputWrapper}>
<label htmlFor="id">id : </label>
<input
id="id"
value={id}
onChange={() => {
}}
disabled
className={classes.input}
/>
</div>
...
</div>
);
};
export default TodoEditModal;
위 코드를 보면, queryKey를 직접 배열에 작성해 주었습니다.
['todo','list']
[todo,item,{id}]
그런데, useUpdateTodoMutation
을 보면 Mutation 성공 후 queryKey를 이용해서 invalidateQueries
를 실행해 주려고 하는데,
useQuery Hook이 많아질 수록 queryKey를 찾아서 일일이 타이핑 해주기 어렵겠죠.
그래서 query key factory 라이브러리도 사용했습니다.
// src/queries/useTodoQuery.js
import { useMutation } from "@tanstack/react-query";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { TodoApi } from "../api/TodoApi";
import { createQueryKeys } from "@lukemorales/query-key-factory";
const todoQueryKeys = createQueryKeys("todo", {
list: () => [],
detail: (id) => [{ id }],
export const useTodoListQuery = () => {
return useQuery({
queryKey: todoQueryKeys.list().queryKey,
queryFn: async () => {
const res = await TodoApi.getAllTodoList();
return res;
},
});
};
export const useTodoItemQuery = (id) => {
return useQuery({
queryKey: todoQueryKeys.detail(id).queryKey,
queryFn: async () => {
const res = await TodoApi.getTodoItem(id);
return res;
},
cacheTime: 0,
});
};
export const useUpdateTodoMutation = (...) => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (params) => TodoApi.updateTodoItem(params),
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: todoQueryKeys.list().queryKey,
}); ...
},
...
});
};
todoQueryKeys
를 생성했고, 쿼리 키가 필요할 때는 todoQueryKeys
를 이용해서 접근합니다.
앞서 todo list, todo detail, update todo query를 작성했습니다.
todo API 말고도 user API도 react query hook으로 만들어 보겠습니다.
// src/queries/useUserQuery.js
import { useQuery } from "@tanstack/react-query";
import { UserApi } from "../api/UserApi";
import { createQueryKeys } from "@lukemorales/query-key-factory";
export const userQueryKeys = createQueryKeys("user", {
detail: (id) => [{ id }],
});
export const useUserItemQuery = (id) => {
return useQuery({
queryKey: userQueryKeys.detail(id).queryKey,
queryFn: async () => {
const res = await UserApi.getUserInfo(id);
return res;
},
});
};
이렇게 여러개의 useQuery Hook이 생겨나다보니 query key가 js 파일별로 늘어났습니다.
query key를 한 번에 모아두고 싶어지네요.
src/queries/queryKeys.js를 생성하고, mergeQueryKeys
를 이용해 한 번의 import로 query key들을 가져오게끔 수정하겠습니다.
// src/queries/queryKeys.js
import {
createQueryKeys,
mergeQueryKeys,
} from "@lukemorales/query-key-factory";
export const todoQueryKeys = createQueryKeys("todo", {
list: () => [],
detail: (id) => [{ id }],
});
export const userQueryKeys = createQueryKeys("user", {
detail: (id) => [{ id }],
});
export const queryKeys = mergeQueryKeys(todoQueryKeys, userQueryKeys);
이제 useTodoQuery.js, useUserQuery.js에서는 queryKeys.js의 queryKeys
로부터 키 값을 가져옵니다.
// src/queries/useTodoQuery.js
import { useMutation } from "@tanstack/react-query";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { TodoApi } from "../api/TodoApi";
import { queryKeys } from "./queryKeys";
export const useTodoListQuery = () => {
return useQuery({
queryKey: queryKeys.todo.list().queryKey,
queryFn: async () => {
const res = await TodoApi.getAllTodoList();
return res;
},
});
};
...
// src/queries/useUserQuery.js
import { useQuery } from "@tanstack/react-query";
import { UserApi } from "../api/UserApi";
import { queryKeys } from "./queryKeys";
export const useUserItemQuery = (id) => {
return useQuery({
queryKey: queryKeys.user.detail(id).queryKey,
queryFn: async () => {
const res = await UserApi.getUserInfo(id);
return res;
},
});
};
react query 로 서버 상태 관리를 하도록 구현했습니다.
이제 zustand로 클라이언트 상태관리를 해보겠습니다.
예제에서는 isModalOpen
, modalContents
상태를 관리할 것입니다.
먼저 src 하위에 store 폴더를 생성하고, uiStore.js에 아래와 같이 작성했습니다.
import { create } from "zustand";
import { devtools } from "zustand/middleware";
/*
immer를 쓰면 setState((prev) => {...prev, todoList: newList} 처럼 하지 않아도 된다.
*/
import { immer } from "zustand/middleware/immer";
const uiStore = (set, get) => ({
isModalOpen: false,
modalContents: <div></div>,
actions: {
modalClose: () =>
set((state) => {
state.isModalOpen = false;
}),
setModalInfoAndOpen: (newModalContents) =>
set((state) => {
state.isModalOpen = true;
state.modalContents = newModalContents;
}),
},
});
const store = immer((set, get) => uiStore(set, get));
export const useUIStore = create(devtools(store, { name: "uiStore" }));
1. zustand는 provider와 같은 설정이 따로 필요하지 않습니다.
zustand로 state를 관리할 때는, provider같은 설정은 따로 필요하지 않습니다.
2. zustand/middleware의 devtools 사용하기
zustand/middleware의 devtools를 사용하면 Chrome의 redux devtools 확장 프로그램을 사용해서 zustand 값들을 확인할 수 있습니다. 만약 redux devtools 확장 프로그램을 설치하지 않으셨다면, 아래 링크에서 설치하세요.
아래 이미지처럼 개발자도구에 redux 메뉴가 생겼다면 정상적으로 설치된 것입니다.
redux devtools 확장 프로그램 설치하기 : https://chromewebstore.google.com/detail/redux-devtools/lmhkpmbekcpmknklioeibfkpmmfibljd