Custom Instance
Axios 인스턴스를 사용하면 설정을 한 곳에서 관리. baseURL을 설정하여 서버 주소가 변경되더라도 한 곳에서 수정하면 모든 요청에 반영
// src > axios > api.js
import axios from "axios";
// axios.create의 입력값으로 들어가는 객체는 configuration 객체임
const api = axios.create({
baseURL: "http://localhost:4000",
});
export default api;
사용 예시
import "./App.css";
import { useEffect } from "react";
import api from "./axios/api";
function App() {
useEffect(() => {
api
.get("/cafe")
.then((res) => {
console.log("결과 => ", res.data);
})
.catch((err) => {
console.log("오류가 발생하였습니다!");
});
}, []);
return <div>axios 예제입니다.</div>;
}
export default App;
Interceptor
Interceptor는 HTTP 요청과 응답을 가로채서 특정 작업을 수행. 모든 요청에 공통적인 헤더를 추가하거나, 응답에 대한 공통적인 에러 처리 가능
Interceptor는 다음 두 상황에서 흐름을 가로챔
- 요청(request)이 전송되기 전(또는 요청을 보내기 전, 또는 요청이 출발하기 전)
- 응답(response)의 then(성공) 또는 catch(실패)가 처리되기 전
Interceptor를 사용하면 요청 및 응답 시 필요한 작업을 한꺼번에 처리 가능 ( 아래는 유용한 사례 )
- 요청 헤더 추가
- 인증 관리
- 로그 관련 로직 삽입
- 에러 핸들링
Interceptor 코드 예시
import axios from "axios";
const api = axios.create({
baseURL: "http://localhost:4000",
});
api.interceptors.request.use((config) => {
console.log("intercept");
return config;
});
api.interceptors.response.use(
(response) => {
console.log("response");
return response;
},
(error) => {
console.log("error");
return Promise.reject(error);
}
);
export default api;
import { useEffect, useState } from "react";
import axios from "axios";
import api from "./axios/api";
const App = () => {
const [todos, setTodos] = useState(null);
const [todo, setTodo] = useState({
title: "",
});
const [targetId, setTargetId] = useState("");
const [editTodo, setEditTodo] = useState({
title: "",
});
const onSubmitHandler = async (todo) => {
const { data } = await api.post("/todos", todo);
//끝나고 나면 -> await는 반드시 처리가 끝나기를 기다리고 실행
setTodos([...todos, data]);
};
// 만일 fetch를 사용했다면, 이렇게 JSON.stringify를 '직접' 해줘야 함
// await fetch("http://localhost:4000/todos", {
// method: "POST",
// headers: {
// "Content-Type": "application/json",
// },
// body: JSON.stringify(todo),
// });
const onDeleteHandler = async (id) => {
await api.delete("/todos/" + id);
setTodos(todos.filter((todo) => todo.id !== id));
};
const onEditHandler = async (targetId, editTodo) => {
await api.patch("/todos/" + targetId, editTodo);
const newTodos = todos.map((todo) => {
if (todo.id === targetId) {
return {
...todo,
title: editTodo.title,
};
}
return todo;
});
setTodos(newTodos);
};
useEffect(() => {
const fetchPost = async () => {
try {
// 구조 분해 할당으로 get 내부의 data 접근
const { data } = await api.get("/todos");
setTodos(data);
} catch (error) {
console.log("Error", error);
}
};
fetchPost();
}, []);
console.log(todos);
return (
<>
<form
onSubmit={(e) => {
e.preventDefault();
onSubmitHandler(todo);
}}
>
<div>
<input
type="text"
placeholder="수정하고싶은 Todo Id를 입력"
onChange={(e) => {
setTargetId(e.target.value);
}}
/>
<input
type="text"
placeholder="수정할 값 입력"
onChange={(e) => {
setEditTodo({ ...editTodo, title: e.target.value });
}}
/>
<button
type="button"
onClick={() => {
onEditHandler(targetId, editTodo);
}}
>
수정하기
</button>
</div>
<input
type="text"
onChange={(e) => {
setTodo({ ...todo, title: e.target.value });
}}
/>
<button type="submit">추가하기</button>
</form>
{/* 옵셔널 체이닝 -> null or undefined일 경우 undefined 리턴
null은 map 메서드가 없음으로 오류 발생 */}
{todos?.map((todo) => {
return (
<div key={todo.id}>
<span>{todo.title}</span>
<button
onClick={() => {
onDeleteHandler(todo.id);
}}
>
삭제
</button>
</div>
);
})}
</>
);
};
export default App;
일반 redux 비동기 처리의 문제점
- 상태 관리의 복잡성
- 중복된 코드
- 비즈니스 로직( 앱의 핵심 동작을 정의하는 코드 )의 분리 부족
- 서버 상태( 서버에서 가져오는 데이터를 포함 => 캐싱,동기화,재검증 등 ) 관리의 어려움
Redux 미들웨어 이용
- Redux Middleware란?
- Redux Middleware는 액션이 리듀서에 도달하기 전에 중간에서 가로채서 추가적인 작업을 수행할 수 있게 해주는 함수. 이를 통해 **비동기 로직을 처리(Redux Thunk, Redux Saga, Redux Observable …)**하거나, **로그를 기록(redux-logger)**하거나, 에러를 처리하는 등의 작업 가능
- 일반 redux의 처리 방식 vs redux thunk의 처리 방식
- 일반 redux의 처리 방식
- UI 컴포넌트에서 **dispatch**가 일어난다.
- **dispatch**가 일어날 때 action 객체를 **dispatch**한다.
- action 객체는 리듀서로 전달되어 Redux store를 업데이트한다.
- redux thunk의 처리 방식
- UI 컴포넌트에서 **dispatch**가 일어난다.
- **dispatch**가 일어날 때 thunk 함수를 **dispatch**한다.
- 여기 정말 중요하죠! 액션 객체라 아니라, 함수를 dispatch 한다는 것이 핵심. 그렇기 때문에 그 함수 안에서 여러가지 작업(비동기 작업 포함)을 할 수 있는 것.
- 전달된 thunk 함수에 의해 Redux Thunk 미들웨어가 이 함수를 호출하고, 함수 안에서 비동기 작업을 수행
- 비동기 로직이 수행된 이후 필요에 따라 액션 객체를 별도로 생성하여 **dispatch**한다.
- **dispatch**된 액션 객체는 리듀서로 전달되어 Redux store를 업데이트한다.
- Redux Thunk를 이용하면 이런 부분이 가능
- 상태 관리의 일관성: Redux Thunk를 사용하여 비동기 로직을 중앙에서 관리 가능.
- 비즈니스 로직의 분리: 비동기 로직을 액션 크리에이터로 분리하여 UI 로직과 비즈니스 로직을 분리하고, 유지보수성과 가독성을 향상.
- 로딩 및 에러 상태 관리의 용이성: Redux Thunk와 Redux Toolkit을 사용하여 **isLoading**과 isError 등의 상태를 중앙에서 일관되게 관리할 수 있어, 상태 변화에 따른 UI 업데이트가 용이.
redux thunk 예시 코드
import { useEffect } from "react";
import { useDispatch } from "react-redux";
import TodoListContainer from "./components/TodoListContainer";
import AddForm from "./components/AddForm";
import { fetchTodos } from "./redux/slices/todos"; // 비동기 액션 가져오기
const App = () => {
const dispatch = useDispatch();
useEffect(() => {
// 컴포넌트 마운트 시 todos를 가져오는 비동기 액션 디스패치
dispatch(fetchTodos());
}, [dispatch]);
return (
<div>
<AddForm />
<TodoListContainer />
</div>
);
};
export default App;
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
import axios from "axios";
// 비동기 작업을 위한 thunk 액션 생성
export const fetchTodos = createAsyncThunk("todos/fetchTodos", async () => {
const response = await axios.get("http://localhost:4000/todos");
return response.data;
});
export const addTodoAsync = createAsyncThunk("todos/addTodo", async (todo) => {
const response = await axios.post("http://localhost:4000/todos", todo);
return response.data;
});
export const deleteTodoAsync = createAsyncThunk(
"todos/deleteTodo",
async (id) => {
await axios.delete(`http://localhost:4000/todos/${id}`);
return id;
}
);
export const toggleTodoAsync = createAsyncThunk(
"todos/toggleTodo",
async (todo) => {
const response = await axios.patch(
`http://localhost:4000/todos/${todo.id}`,
{
isDone: !todo.isDone,
}
);
return response.data;
}
);
const todosSlice = createSlice({
name: "todos",
initialState: {
todos: [],
loading: false,
error: null,
},
reducers: {},
extraReducers: (builder) => {
builder
.addCase(fetchTodos.pending, (state) => {
state.loading = true;
state.error = null;
})
.addCase(fetchTodos.fulfilled, (state, action) => {
state.loading = false;
state.todos = action.payload;
})
.addCase(fetchTodos.rejected, (state, action) => {
state.loading = false;
state.error = action.error.message;
})
.addCase(addTodoAsync.fulfilled, (state, action) => {
state.todos.push(action.payload);
})
.addCase(deleteTodoAsync.fulfilled, (state, action) => {
state.todos = state.todos.filter((todo) => todo.id !== action.payload);
})
.addCase(toggleTodoAsync.fulfilled, (state, action) => {
state.todos = state.todos.map((todo) =>
todo.id === action.payload.id ? action.payload : todo
);
});
},
});
export default todosSlice.reducer;
redux thunk의 한계점
- 복잡성 증가 : 비동기 로직이 복잡해질수록 **액션 크리에이터**와 **리듀서**의 코드가 길어지고, 보일러플레이트 코드(반복적이고 틀에 박힌 코드)가 많아져 유지보수가 어려움.
- 테스트가 복잡 : 비동기 로직을 포함한 **액션 크리에이터**를 테스트하는 것이 복잡하며, 다양한 응답 상태와 비동기 작업을 시뮬레이션하기 위한 별도의 장치가 필요해 테스트 코드가 복잡해짐.
TanStack Query
개념
- 서버 상태 관리 라이브러리
- **TanStack Query**는 서버 상태를 관리하기 위한 라이브러리로, 데이터를 패칭하고 캐싱, 동기화, 무효화 등의 기능을 제공.
- 개발자는 이전에 비해 ‘훨씬’ 비동기 로직을 간편하게 작성하고 유지보수성 향상
- 주요 기능
- 데이터 캐싱: 동일한 데이터를 여러 번 요청하지 않도록 캐싱하여 성능을 향상.
- 자동 리페칭: 데이터가 변경되었을 때 자동으로 리페칭하여 최신 상태를 유지.
- 쿼리 무효화: 특정 이벤트가 발생했을 때 쿼리를 무효화하고 데이터를 다시 가져올 수 있음
설치 : yarn add @tanstack/react-query
useQuery
useQuery는 데이터를 가져오기 위해 사용되는 TanStack Query의 대표적인 훅. 쿼리 키와 비동기 함수(패칭 함수)를 인자로 받아 데이터를 가져오고, 로딩 상태, 오류 상태, 그리고 데이터를 반환함
useMutation
useMutation 은 데이터를 생성, 수정, 삭제하는 등의 작업에 사용되는 훅입니다. CUD에 대한 비동기 작업을 쉽게 수행하고, 성공 또는 실패 시에 추가적인 작업을 실행할 수 있기 때문에 useQuery와 함께 가장 대표적인 TanStack Query hook이라고 할 수 있음
비동기 작업을 쉽게 처리한다는 말 안에는 작업이 완료된 후에 관련된 쿼리를 무효화하는 과정이 포함되는데 이 역시도 TanStack Query의 핵심 개념. 최신 데이터를 유지하는 데에 필수적인 요소
invalidateQueries
invalidateQueries 는 특정 쿼리를 무효화하여 데이터를 다시 패칭하게 하는 함수입니다. 주로 useMutation과 함께 사용하여 데이터가 변경된 후 관련 쿼리를 다시 가져오도록 함. 이를 통해 데이터가 항상 최신 상태로 유지될 수 있도록 도와줌. 예를 들어, 새로운 할 일을 추가한 후 기존의 할 일 목록을 다시 가져오도록 할 수 있음.
예시코드
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App.jsx";
import "./index.css";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
const queryClient = new QueryClient();
ReactDOM.createRoot(document.getElementById("root")).render(
<React.StrictMode>
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>
</React.StrictMode>
);
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import axios from "axios";
import { useState } from "react";
function App() {
// main.js에서 만든 상수를 가져다 쓰는 것
const queryClient = useQueryClient();
const [todoItem, setTodoItem] = useState("");
const fetchTodos = async () => {
const response = await axios.get("http://localhost:4000/todos");
return response.data;
};
const addTodo = async (newTodo) => {
await axios.post("http://localhost:4000/todos", newTodo);
};
const {
data: todos,
isPending,
isError,
} = useQuery({
// data를 캐싱하는기준(서버에 요청x 가지고있는 기준은 queryKey)
queryKey: ["todos"],
queryFn: fetchTodos,
});
const { mutate } = useMutation({
mutationFn: addTodo,
onSuccess: () => {
alert("data!");
// query가 더이상 유효하지 않다고 알려주고 , 반드시 queryKey를 가짐
queryClient.invalidateQueries(["todos"]);
},
});
if (isPending) {
return <div>Loading...</div>;
}
if (isError) {
return <div>Error!!</div>;
}
return (
<div>
<form
onSubmit={(e) => {
e.preventDefault();
const newTodoObj = { title: todoItem, isDone: false };
mutate(newTodoObj);
}}
>
<input
type="text"
value={todoItem}
onChange={(e) => setTodoItem(e.target.value)}
/>
<button>추가</button>
</form>
<ul>
{todos.map((todo) => {
return (
<li
key={todo.id}
style={{
display: "flex",
alignItems: "center",
gap: "10px",
backgroundColor: "aliceblue",
}}
>
<h4>{todo.title}</h4>
<p>{todo.isDone ? "Done" : "Not Done"}</p>
</li>
);
})}
</ul>
</div>
);
}
export default App;