본문 바로가기

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;