본문 바로가기

카테고리 없음

Study_240613 ( axios ( Interceptor ) , redux Thunk ,TanStack Query )

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는 다음 두 상황에서 흐름을 가로챔

  1. 요청(request)이 전송되기 전(또는 요청을 보내기 전, 또는 요청이 출발하기 전)
  2. 응답(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의 처리 방식
      1. UI 컴포넌트에서 **dispatch**가 일어난다.
      2. **dispatch**가 일어날 때 action 객체를 **dispatch**한다.
      3. action 객체는 리듀서로 전달되어 Redux store를 업데이트한다.
    • redux thunk의 처리 방식
      1. UI 컴포넌트에서 **dispatch**가 일어난다.
      2. **dispatch**가 일어날 때 thunk 함수를 **dispatch**한다.
      3. 여기 정말 중요하죠! 액션 객체라 아니라, 함수를 dispatch 한다는 것이 핵심. 그렇기 때문에 그 함수 안에서 여러가지 작업(비동기 작업 포함)을 할 수 있는 것.
      4. 전달된 thunk 함수에 의해 Redux Thunk 미들웨어가 이 함수를 호출하고, 함수 안에서 비동기 작업을 수행
      5. 비동기 로직이 수행된 이후 필요에 따라 액션 객체를 별도로 생성하여 **dispatch**한다.
      6. **dispatch**된 액션 객체는 리듀서로 전달되어 Redux store를 업데이트한다.
  • Redux Thunk를 이용하면 이런 부분이 가능
    1. 상태 관리의 일관성: Redux Thunk를 사용하여 비동기 로직을 중앙에서 관리 가능.
    2. 비즈니스 로직의 분리: 비동기 로직을 액션 크리에이터로 분리하여 UI 로직과 비즈니스 로직을 분리하고, 유지보수성과 가독성을 향상.
    3. 로딩 및 에러 상태 관리의 용이성: 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의 한계점

  1. 복잡성 증가 : 비동기 로직이 복잡해질수록 **액션 크리에이터**와 **리듀서**의 코드가 길어지고, 보일러플레이트 코드(반복적이고 틀에 박힌 코드)가 많아져 유지보수가 어려움.
  2. 테스트가 복잡 : 비동기 로직을 포함한 **액션 크리에이터**를 테스트하는 것이 복잡하며, 다양한 응답 상태와 비동기 작업을 시뮬레이션하기 위한 별도의 장치가 필요해 테스트 코드가 복잡해짐.

 

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;