React-Query 가
Redux 를 대체하게 된
이유.mdx
상태 관리
프론트엔드와 상태관리
React 의 기본 기능을 통한 상태 관리
useState
useEffect
ContextAPI
useCallback
useReducer
으어어...
개발자
그래서 등장!!! Redux
그래서 편안해 졌나요?
하지만 API 통신이
출동하면 어떨까?
비동기 통신 상태 관리
긴 Boilerplate
코드
모든 기능이
Redux store 에 집중
Redux 에서 비동기 처리를 위한
미들웨어 설정 필요
미들웨어 및 에러 핸들링
직접 구현 필요
미들웨어 및 구현코드
테스트 필요
으어어...
개발자
그래서 등장!!! React-Query
그래서 React-Query 가 뭘 하는데요?
그래서 진짜 편안해 졌나요?
그럼 Redux 를 사용한 코드부터 봅시다!
React 의 영원한 예제 TodoList 코드
컴포넌트 코드
모든 로직이 컴포넌트에 집중
export default function Todo() {� const fetchTodo = async () => {
try {
dispatch(requestFetch());
const todoList = await getTodo();
dispatch(successFetch(todoList));
} catch (err) {
dispatch(errorFetch(err));
}
};
� const handleSubmit = async (e) => {
try {
dispatch(requestPost());
await addTodo(inputRef.current.value);
dispatch(successPost());
fetchTodo();
} catch (err) {
dispatch(errorPost(err));
}};
API 통신 파트 부분이
컴포넌트에 전부 포함되어 있는 모습
비동기 상태 관리를 위한 긴 코드들
export default function Todo() {� const dispatch = useDispatch();
const data = useSelector((state) => state.fetchTodo.data);
const fetchIsLoading = useSelector((state) => state.fetchTodo.isLoading);
const fetchError = useSelector((state) => state.fetchTodo.error);
� const postIsLoading = useSelector((state) => state.postTodo.isLoading);
const postError = useSelector((state) => state.postTodo.error);
if (fetchIsLoading || postIsLoading) return <h1>로딩 중</h1>;
� if (fetchError || postError) return <h1>에러 발생</h1>;
� if (data === undefined) return <h1>리스트 없음</h1>;
다양한 변수 선언 및
가독성 저해,
통일된 스타일 유지의 어려움
Store 코드
너무 긴 Boilerplate 코드
import { createStore } from "redux";
�// 초기 상태
const initialState = {
fetchTodo: {
data: [], isLoading: false, error: undefined,
},
postTodo: {
isLoading: false, error: undefined,
},
};
�// 액션 타입 정의
const REQUEST_FETCH = "REQUEST_FETCH";
const SUCCESS_FETCH = "SUCCESS_FETCH";
const ERROR_FETCH = "ERROR_FETCH";
�const REQUEST_POST = "REQUEST_POST";
const SUCCESS_POST = "SUCCESS_POST";
const ERROR_POST = "ERROR_POST";
간단한 상태 하나 추가에도
액션타입, 액션함수,
리듀서 설정 필요
너무 긴 Boilerplate 코드
// 액션 생성자
export const requestFetch = () => ({ type: REQUEST_FETCH });
export const successFetch = (data) => ({
type: SUCCESS_FETCH,
payload: data,
});
export const errorFetch = (error) => ({
type: ERROR_FETCH,
payload: error,
});
�export const requestPost = () => ({ type: REQUEST_POST });
export const successPost = () => ({ type: SUCCESS_POST });
export const errorPost = (error) => ({
type: ERROR_POST,
payload: error,
});
// 리듀서
const todoReducer = (state = initialState, action) => {
switch (action.type) {
case REQUEST_FETCH:
return {
...state,
fetchTodo: {
data: undefined,
isLoading: true,
error: undefined,
},
};
case SUCCESS_FETCH:
return {
...state,
fetchTodo: {
data: action.payload,
isLoading: false,
error: undefined,
},
};
case ERROR_FETCH:
return {
...state,
fetchTodo: {
data: undefined,
isLoading: false,
error: action.payload,
},
};
case REQUEST_POST:
return {
...state,
postTodo: {
isLoading: true,
error: undefined,
},
};
case SUCCESS_POST:
return {
...state,
postTodo: {
isLoading: false,
error: undefined,
},
};
case ERROR_POST:
return {
...state,
postTodo: {
isLoading: false,
error: action.payload,
},
};
default:
return state;
}
};
const store = createStore(todoReducer);�export default store;
Reducer 가 가지는
코드 가독성의 문제
리팩토링 또는 로직 수정의
어려움 등등
이렇게 된 이상...
이렇게 된 이상
통신 파트라도
분리한다
Redux-thunk
도입
Redux 는 사실...
비동기
통신이
뭐드라...
Redux 는 사실...
그래서 등장!!! Redux
Redux-thunk 적용하기
import { createStore, applyMiddleware } from "redux";
import { thunk } from "redux-thunk";
// 기존 코드
// 스토어 생성
const store = createStore(todoReducer, applyMiddleware(thunk));
API 통신 로직을 분리해 봅시다!
export const fetchTodo = () => {
return async (dispatch) => {
dispatch(requestFetch());
try {
const data = await getTodo();
dispatch(successFetch(data));
} catch (error) {
dispatch(errorFetch(error));
}
};
};
export const postTodo = (content) => {
return async (dispatch) => {
dispatch(requestPost());
try {
await addTodo(content);
dispatch(successPost());
dispatch(fetchTodo());
} catch (error) {
dispatch(errorPost(error));
}
};
};
이제 공통된 API 호출 파트를 불러서 사용!
export default function Todo() {� const dispatch = useDispatch();
const data = useSelector((state) => state.fetchTodo.data);
const fetchIsLoading = useSelector((state) => state.fetchTodo.isLoading);
const fetchError = useSelector((state) => state.fetchTodo.error);� const postIsLoading = useSelector((state) => state.postTodo.isLoading);
const postError = useSelector((state) => state.postTodo.error);
� useEffect(() => {
dispatch(fetchTodo());
}, [dispatch]);
� const handleSubmit = (e) => {
dispatch(postTodo(inputRef.current.value));
};
컴포넌트 코드가 많이 줄었습니다!
const fetchTodo = async () => {
try {
dispatch(requestFetch());
const todoList = await getTodo();
dispatch(successFetch(todoList));
} catch (err) {
dispatch(errorFetch(err));
}
};
� const handleSubmit = async (e) => {
e.preventDefault();
try {
dispatch(requestPost());
await addTodo(inputRef.current.value);
dispatch(successPost());
fetchTodo();
} catch (err) {
dispatch(errorPost(err));
}
};
� useEffect(() => {
fetchTodo();
}, [dispatch]);
useEffect(() => {
dispatch(fetchTodo());
}, [dispatch]);
� const handleSubmit = (e) => {
dispatch(postTodo(inputRef.current.value));
};
컴포넌트 담당자는
통신 로직과 상관 없이
개발이 가능!
그럼에도...
진짜 등장!!! React-Query
React-Query 적용하기!
import Todo from "./Todo";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
const queryClient = new QueryClient();
�function App() {
return (
<QueryClientProvider client={queryClient}>
<Todo />
</QueryClientProvider>
);
}
�export default App;
�
React-Query 적용하기! 데이터 받아오기
// React Query
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
export default function Todo() {
const { data, isLoading, isError } = useQuery({
queryKey: ["todo"],
queryFn: () => getTodo(),
});
if (isLoading) return <h1>로딩 중</h1>;
� if (isError) return <h1>에러 발생</h1>;
� if (data === undefined) return <h1>리스트 없음</h1>;
React-Query 적용하기! 데이터 보내기
export default function TodoQuery() {� const postTodoMutation = useMutation({
mutationFn: (todo) => {
addTodo(todo);
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["todo"] });
},
});��const handleSubmit = (e) => {
e.preventDefault();� postTodoMutation.mutate(inputRef.current.value, {
onSuccess: () => alert("Todo 등록 성공"),
onError: () => alert("Todo 등록 실패"),
});� inputRef.current.value = "";
queryClient.invalidateQueries();
};
그래서 우리가 얻은 것은!?
짧아진 코드!
import { createStore, applyMiddleware } from "redux";
import { thunk } from "redux-thunk";
import { getTodo, addTodo } from "./api"; // API 호출을 하는 함수들
�// 기존 코드
const initialState = {
fetchTodo: {
data: [],
isLoading: false,
error: undefined,
},
postTodo: {
isLoading: false,
error: undefined,
},
};
�// 액션 타입 정의
const REQUEST_FETCH = "REQUEST_FETCH";
const SUCCESS_FETCH = "SUCCESS_FETCH";
const ERROR_FETCH = "ERROR_FETCH";
�const REQUEST_POST = "REQUEST_POST";
const SUCCESS_POST = "SUCCESS_POST";
const ERROR_POST = "ERROR_POST";
�// 액션 생성자
export const requestFetch = () => ({ type: REQUEST_FETCH });
export const successFetch = (data) => ({
type: SUCCESS_FETCH,
payload: data,
});
export const errorFetch = (error) => ({
type: ERROR_FETCH,
payload: error,
});
�export const requestPost = () => ({ type: REQUEST_POST });
export const successPost = () => ({ type: SUCCESS_POST });
export const errorPost = (error) => ({
type: ERROR_POST,
payload: error,
});
�export const fetchTodo = () => {
return async (dispatch) => {
dispatch(requestFetch());
try {
const data = await getTodo();
dispatch(successFetch(data));
} catch (error) {
dispatch(errorFetch(error));
}
};
};
�export const postTodo = (content) => {
return async (dispatch) => {
dispatch(requestPost());
try {
await addTodo(content);
dispatch(successPost());
dispatch(fetchTodo());
} catch (error) {
dispatch(errorPost(error));
}
};
};
�// 리듀서
const todoReducer = (state = initialState, action) => {
switch (action.type) {
case REQUEST_FETCH:
return {
...state,
fetchTodo: {
data: undefined,
isLoading: true,
error: undefined,
},
};
case SUCCESS_FETCH:
return {
...state,
fetchTodo: {
data: action.payload,
isLoading: false,
error: undefined,
},
};
case ERROR_FETCH:
return {
...state,
fetchTodo: {
data: undefined,
isLoading: false,
error: action.payload,
},
};
case REQUEST_POST:
return {
...state,
postTodo: {
isLoading: true,
error: undefined,
},
};
case SUCCESS_POST:
return {
...state,
postTodo: {
isLoading: false,
error: undefined,
},
};
case ERROR_POST:
return {
...state,
postTodo: {
isLoading: false,
error: action.payload,
},
};
default:
return state;
}
};
�// 스토어 생성
const store = createStore(todoReducer, applyMiddleware(thunk));
�export default store;
�
export default function TodoQuery() {
const inputRef = useRef();
const queryClient = useQueryClient();
� const { data, isLoading, isError } = useQuery({
queryKey: ["todo"],
queryFn: () => getTodo(),
});
� const postTodoMutation = useMutation({
mutationFn: (todo) => {
addTodo(todo);
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["todo"] });
},
});
� const handleSubmit = (e) => {
e.preventDefault();
� postTodoMutation.mutate(inputRef.current.value, {
onSuccess: () => alert("Todo 등록 성공"),
onError: () => alert(" 등록 실패"),
});
� inputRef.current.value = "";
queryClient.invalidateQueries();
};
짧아진 코드!
쉬운 서버 상태 관리가 가능
아직도
React-query,
Zustand 안쓰니?