개발 경험 및 효율성 증대를 위해 클라이언트와 비동기 및 서버 상태 관리를 분리하여 개발하려고 하고, 비동기 및 서버 상태 관리를 맡아주는 라이브러리 설치하려고 함.
서버에서 비동기적으로 가져올 수 있는 데이터들을 관리하기 위해 로직을 따로 구현해도 되지만 라이브러리를 사용해 서버 데이터 감시 및 관리를 편하게 하기 위해 라이브러리를 사용하고자 함.
React 애플리케이션에서 비동기 로직을 관리하는 데 사용되는 라이브러리. 즉, 서버 상태와 비동기 상태들을 관리해주는 관리자임.
비동기 또는 서버 상태 작업을 용이하게 하기 위해 서버 상태 가져오기 및 비동기 상태 처리, 캐싱과 서버 상태와의 동기화, 업데이트를 도와줌.
가장 큰 이유는 클라이언트 전용 상태 관리 라이브러리는 비동기 데이터들을 관리하는데 한계가 명확함. 데이터가 오래됐음에도(Stale) Refetch하지 않는다든지, 데이터 혼합이 발생한다는지 등.
카카오페이 기술 블로그를 참조했다.
// features/todos/todos.slice.ts
// API 상태를 관리하기 위한 Redux State
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { TodoItem } from 'types/todo';
export interface TodoListState {
data?: TodoItem[];
isLoading: boolean;
error?: Error;
}
const initialState: TodoListState = {
data: undefined,
isLoading: false,
error: undefined,
};
export const todoListSlice = createSlice({
name: 'todoList',
initialState,
reducers: {
requestFetchTodos: (state) => {
state.isLoading = true;
},
successFetchTodos: (state, action: PayloadAction<TodoItem[]>) => {
state.data = action.payload;
state.isLoading = false;
state.error = undefined;
},
errorFetchTodos: (state, action: PayloadAction<string>) => {
state.data = undefined;
state.isLoading = false;
state.error = action.payload;
},
},
});
export const { requestFetchTodos, successFetchTodos, errorFetchTodos } =
todoListSlice.actions;
export default todoListSlice.reducer;
// features/todos/todos.saga.ts
import { PayloadAction } from '@reduxjs/toolkit';
import axios from 'axios';
import { call, put, takeEvery } from 'redux-saga/effects';
import { TodoItem } from '../../types/todo';
import {
errorFetchTodos,
errorPostTodos,
requestFetchTodos,
requestPostTodos,
successFetchTodos,
successPostTodos,
} from './todos.slice';
async function getTodoList() {
const { data } = await axios.get<TodoItem[]>('./todos');
return data;
}
function* requestFetchTodoTask() {
try {
const data: TodoItem[] = yield call(getTodoList);
yield put(successFetchTodos(data));
} catch (e) {
yield put(errorFetchTodos(e.message));
}
}
async function postTodoList(contents: string) {
await axios.post('/todos', { contents });
}
function* requestPostTodoTask(action: PayloadAction<string>) {
try {
yield call(postTodoList, action.payload);
yield put(successPostTodos());
} catch (e) {
yield put(errorPostTodos(e.message));
}
}
function* successPostTodoTask() {
// 서버에 새로운 Todo 추가 요청 성공 시
// 서버에서 Todo 목록을 다시 받아오기 위해 Action Dispatch
yield put(requestFetchTodos());
}
function* todoListSaga() {
yield takeEvery(requestFetchTodos.type, requestFetchTodoTask);
yield takeEvery(requestPostTodos.type, requestPostTodoTask);
yield takeEvery(successPostTodos.type, successPostTodoTask);
}
export default todoListSaga;
// quires/QTdeeoorssuuy.ts
// API 상태를 불러오기 위한 React Query Custom Hook
import axios from 'axios';
import { useQuery } from 'react-query';
import { TodoItem } from 'types/todo';
// useQuery에서 사용할 UniqueKey를 상수로 선언하고 export로 외부에 노출합니다.
// 상수로 UniqueKey를 관리할 경우 다른 Component (or Custom Hook)에서 쉽게 참조가 가능합니다.
export const QUERY_KEY = '/todos';
// useQuery에서 사용할 `서버의 상태를 불러오는데 사용할 Promise를 반환하는 함수`
const fetcher = () => axios.get<TodoItem[]>('/todos').then(({ data }) => data);
const useTodosQuery = () => {
return useQuery({ queryKey: QUERY_KEY, queryFn: fetcher });
};
export default useTodosQuery;
// 코드를 받는 최상위 파일 : _app.tsx, layout.tsx 등
import {
useQuery,
useMutation,
useQueryClient,
QueryClient,
QueryClientProvider,
} from '@tanstack/react-query';
import { getTodos, postTodo } from '../my-api';
/// QueryClient 만들기
// 1. 그냥 선언
const queryClient = new QueryClient();
// 2. useState 선언 + 기본 설정
const [client] = useState(
new QueryClient({
defaultOptions: {
queries: {
refetchOnWindowFocus: false, // 윈도우가 다시 포커스되었을때 데이터를 refetch
refetchOnMount: false, // 데이터가 stale 상태이면 컴포넌트가 마운트될 때 refetch
retry: 1, // API 요청 실패시 재시도 하는 옵션 (설정값 만큼 재시도)
},
},
}),
);
function App() {
return (
// 우리 App 루트에 Provider 설정(필수 설정)
<QueryClientProvider client={queryClient}>
<Todos />
</QueryClientProvider>
);
}
function Todos() {
// queryClient 접근
const queryClient = useQueryClient();
// useQuery
const query = useQuery({ queryKey: ['todos'], queryFn: getTodos });
// Mutations
const mutation = useMutation({
mutationFn: postTodo,
onSuccess: () => {
// Invalidate 및 refetch
queryClient.invalidateQueries({ queryKey: ['todos'] });
},
});
return (
<div>
<ul>{query.data?.map((todo) => <li key={todo.id}>{todo.title}</li>)}</ul>
<button
onClick={() => {
mutation.mutate({
id: Date.now(),
title: 'Do Laundry',
});
}}
>
Add Todo
</button>
</div>
);
}
render(<App />, document.getElementById('root'));