본문 바로가기

Computer Programming/React

axios instance와 interceptor | toolkit + thunk로 API 비동기 처리

 

 

1. instance 생성

/.axios/api.js 에서 api 가공하기


  
import axios from "axios";
const instance = axios.create({
baseURL: process.env.REACT_APP_SERVER_URL,
});
export default instance;

 

이후 axios 를 사용하려면 Import 해와서

 


  
import api from "./axios/api";
const { data } = await api.get("/todos");

axios => api로 변경해주면 된다. 그리고 baseURL을 지정해놨기 때문에 /todos 까지만 작성해도 된다.

 

 

 

2. interceptor 생성


  
import axios from "axios";
const instance = axios.create({
baseURL: process.env.REACT_APP_SERVER_URL,
});
//req와 res 사이의 작업
//인자로 항상 콜백함수 2개가 들어감
instance.interceptors.request.use(
//요청을 보내기 전 수행되는 함수
function (config) {
console.log("인터셉터 요청 성공!");
return config;
},
//오류 요청을 보내기 전 수행되는 함수
function (error) {
console.log("인터셉터 요청 오류!");
return Promise.reject(error); //오류 처리
},
);
instance.interceptors.response.use(
//응답을 내보내기 전 수행되는 함수
function (response) {
console.log("인터셉터 응답 받았습니다!");
return response;
},
//오류 응답을 내보내기 전 수행되는 함수
function (error) {
console.log("인터셉터 응답 오류 발생!");
return Promise.reject(error);
},
);
export default instance;
//fetchTodos 의 get 요청이 실행되면서 axios를 콜하게되고, 인터셉터 요청과 응답이 실행됨.

 

 

timeout 속성을 이용해 1초가 지나면 error로 간주할 수 있게 설정해보자


  
const instance = axios.create({
baseURL: process.env.REACT_APP_SERVER_URL,
timeout: 1, // 0.001초가 지나면 error로 간주함
});

 

 

 

 

 

3. middleware (redux-thunk)

 

그 전에 기본적인 카운터 앱의 리덕스 툴킷을 사용한 구조를 살펴보자

 

index.jsx


  
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<Provider store={store}>
<App />
</Provider>,
);

 

config/configStore.jsx


  
import counter from "../modules/counter";
import { configureStore } from "@reduxjs/toolkit";
const store = configureStore({
reducer: { counter },
});
export default store;

 

modules/counter.js


  
import { createSlice } from "@reduxjs/toolkit";
const initialState = {
number: 0,
};
const counterSlice = createSlice({
name: "counter",
initialState,
reducers: {
plusN: (state, action) => {
state.number = state.number + action.payload;
},
minusN: (state, action) => {
state.number = state.number - action.payload;
},
},
});
export default counterSlice.reducer;
export const { plusN, minusN } = counterSlice.actions;

 

 

App.jsx


  
function App() {
const [num, setNum] = useState(0);
const globalNumber = useSelector((state) => state.counter.number);
const dispatch = useDispatch();
return (
<div className="App">
<button onClick={() => dispatch(plusN(+num))}>+</button>
<button onClick={() => dispatch(minusN(+num))}>-</button>
<input
type="number"
value={num}
onChange={(e) => setNum(+e.target.value)}
/>
현재 카운트: {globalNumber}
</div>
);
}

 

 

 

 

Thunk 사용하기

 

1. redux toolkit 내장 api인 createAsyncThunk 함수를 통해 thunk 함수 만들기

2. createSlice 안의 extraReducer를 수정해줌으로서 Thunk를 등록한다.

3. dispatch 하기

 

 

Thunk함수를 만들고


  
import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
//thunk함수 만들기
//2개의 INPUT (이름, 함수)
export const __addNumber = createAsyncThunk(
"ADD_NUMBER_WAIT",
(payload, thunkAPI) => {
//수행하고 싶은 동작 : 3초 기다리기
setTimeout(() => {
thunkAPI.dispatch(plusN(payload)); //component에서 넘겨받은 payload를 전달
}, 3000);
},
);
export const __minusNumber = createAsyncThunk(
"ADD_NUMBER_WAIT",
(payload, thunkAPI) => {
//수행하고 싶은 동작 : 3초 기다리기
setTimeout(() => {
thunkAPI.dispatch(minusN(payload)); //component에서 넘겨받은 payload를 전달
}, 3000);
},
);
const initialState = {
number: 0,
};
//...

이름 input은 의미가 없으니 컨벤션을 따라 적당히 지어준다

 

 

 

App.js에서 dispatch 보내기 (thunk함수에!)

 

 


  
const onClickAddNumberHandler = () => {
dispatch(__addNumber(+num));
};
const onClickMinusNumberHandler = () => {
dispatch(__minusNumber(+num));
};

 

 

리덕스 미들웨어를 사용하면 액션이 리듀서로 전달되기 전에 중간에 어떤 작업을 더 할 수 있다. ( = 함수를 넣을 수 있다)

즉 객체가 아닌 함수를 dispatch할 수 있게 해준다. <- thunk의 핵심이다.

 

 


 

4. Thunk 와 API 사용하기

 

아래는 module/todosSlice.js 기본 구조이다. 이제 여기에 thunk 함수를 추가하고, todosSlice의 createSlice안의 extrareducers에 전달해주면 미들웨어가 적용된다.

 


  
// src/redux/modules/todosSlice.js
import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
import axios from "axios";
//초깃값
const initialState = {};
//thunk 함수
// 액션생성 함수 + 리듀서
export const todosSlice = createSlice({
name: "todos",
initialState,
reducers: {},
extraReducers: {},
});
export const {} = todosSlice.actions;
export default todosSlice.reducer;

 

 

1. thunk 함수 구현

2. 리듀서 로직 구현 (createSlice의 reducers 에서 => extraReducer로!)

   - 진행에 관련된 state 관리할 수 있음 (loading, isError, error 등) + 물론 data도.

   - root reducer에는 추가되어있어야 함

3. 기능 확인 (network)

4. 화면 렌더링

 

이 순서로 하면 된다.

 


  
// src/redux/modules/todosSlice.js
import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
import axios from "axios";
//초깃값
const initialState = {
todos: [],
error: "",
isLoading: false,
isError: false,
};
//thunk 함수 (비동기 함수 처리 -> createAsyncThunk)
export const __getTodos = createAsyncThunk(
"getTodos",
async (payload, thunkAPI) => {
try {
const response = await axios.get("http://localhost:4000/todos");
return thunkAPI.fulfillWithValue(response.data);
} catch (e) {
return thunkAPI.fulfillWithValue(e);
}
},
);
// 액션생성 함수 + 리듀서
export const todosSlice = createSlice({
name: "todos",
initialState,
reducers: {},
extraReducers: {
[__getTodos.pending]: (state) => {
state.isLoading = true;
},
[__getTodos.fulfilled]: (state, action) => {
state.todos = action.payload;
state.isLoading = false;
},
[__getTodos.rejected]: (state, action) => {
state.isError = true;
state.error = action.payload;
},
},
});
export const {} = todosSlice.actions;
export default todosSlice.reducer;

 

 

 


  
//configStore.js
import { configureStore } from "@reduxjs/toolkit";
import todos from "../modules/todosSlice";
const store = configureStore({
reducer: {
todos,
},
});
export default store;

 

 


  
//App.js
import React from "react";
import { __getTodos } from "./redux/modules/todosSlice";
import { useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
const App = () => {
const dispatch = useDispatch();
const { isLoading, error, todos } = useSelector((state) => {
return state.todos;
});
//side effect -> mount 될 때 처리
useEffect(() => {
dispatch(__getTodos());
}, [dispatch]);
if (isLoading) {
return <div>Loading...</div>;
}
if (error) {
return <div>{error.message}</div>;
}
return (
<>
<h1>
{todos.map((todo) => (
<div key={todo.id}>{todo.title}</div>
))}
</h1>
</>
);
};
export default App;

 

 

 

 

 


*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*