프로젝트 이모저모/HoduMarket 프로젝트

오픈마켓 프로젝트) createAsyncThunk 에서 React-Query로 리팩토링

Ella Seon 2023. 7. 18. 11:49

0. Redux-toolkit 의 createAsyncThunk -> React-Query 로 리팩토링

🔸리팩토링을 하게 된 배경

- 오픈 마켓 프로젝트에서 전역상태관리로 Redux-toolkit 을 쓰고, 서버 상태관리를 createAsyncThunk 로 관리하고 있었다.

🔸createAsyncThunk 
- Redux-Toolkit 에서 내부적으로 thunk 를 내장하고 있어서, 다른 미들웨어(ex: Recus-Saga) 를 사용하지 않고도 비동기 처리를 할 수 있다.
- 비동기 작업의 성공,실패 및 로딩 상태를 자동으로 처리하는 액션을 생성하므로, 서버 데이터를 편리하게 전역 상태로 관리할 수 있다. 
- 전역 상태로 관리되는 서버 데이터는 여러 컴포넌트에서 공유될 수 있으며, 필요한 곳에서 사용할 수 있다. 이를 통해 상태의 일관성을 유지하고, 컴포넌트 간의 데이터 공유 및 동기화를 할 수 있다. 

- 헌데, 오픈마켓 프로젝트에서 전역상태관리를 위해 Client Store을 사용하고 있는 건 맞지만 대부분이 비동기 통신을 위해 쓰이고 있다고 생각했다.  로그인, 회원가입, 장바구니 등 모든 페이지에서 사용되는 비동기통신의 상태를 다 전역상태관리로 사용하고 있었다.

🤔사실, redux-toolkit 을 쓰니까 굳이 API 폴더를 따로 나눠서 비동기 통신을 하기 보다는, 일관성을 위해 redux-toolkit에서 한번에 관리하자 라고 해서 createAsyncThunk 를 도입했었는데, 잘못된 방식 같았다. 

 

보통 프로덕트는 몇개에서 몇십개 혹은 몇백개의 API 동작마다 모두 AJAX 상태와 응답값을 바라보는 상태들이 생기게된다. (isError와 같은 더 많은 상태들이 있음) 이러한 모든 것들을 전역에 넣는다? 굉장히 비효율적이라고 생각이 들었다. 

또한, API 응답에서 받은 데이터의 조작과 가공까지 Store 에서 담당하고 있거나, 혹은 관련 코드를 호출하고 있을 것이다. 

 

아래 장바구니 postCartSlice.ts 만 봐도 상태를 관리하는 코드라기 보다는 API 통신을 담당하고 있는 코드라는 생각이 들것이다. 순수 Client 상태와 관련된 코드라기보다는 API 상태와 관련된 코드가 더 많았다. 

interface CartItem {
  my_cart?: number;
  cart_item_id?: number;
  product_id?: number;
  quantity?: number;
}

interface CartSliceProps {
  item: CartItem;
  status: string;
  error: string;
}

interface PostCartType {
  TOKEN: string;
  product_id: number;
  quantity: number;
  check: boolean;
}

const initialState: CartSliceProps = {
  item: {},
  status: "idle",
  error: "",
};

export const fetchPostCart = createAsyncThunk(
  "cart/fetchPostCart",
  async ({ TOKEN, product_id, quantity, check }: PostCartType) => {
    try {
      const config = {
        headers: {
          Authorization: `JWT ${TOKEN}`,
        },
      };
      const data = { product_id, quantity, check };
      const result = await axios.post(`${BASE_URL}/cart/`, data, config);
      // console.log(result.data);
      return result.data;
    } catch (error: any) {
      console.log(error);
    }
  }
);

export const postCartSlice = createSlice({
  name: "cart",
  initialState,
  reducers: {},
  extraReducers: (builder) => {
    builder
      .addCase(fetchPostCart.fulfilled, (state, action) => {
        state.item = action.payload;
        state.status = "succeeded";
        state.error = "";
      })
      .addCase(fetchPostCart.rejected, (state, action) => {
        state.status = "failed";
        state.error = action.error.message || "Something is wrong";
        state.item = {};
      });
  },
});

export default postCartSlice.reducer;

 

정리해보자면, 지금까지의 나의 코드는

1) 전역상태가 저장되고 관리되고 있는 공간으로서의 Redux-Toolkit을 사용했다기보다는, 비동기 통신을 위해 상태관리 라이브러리를 주로 사용하고, 부가적으로 전역상태관리를 사용하고 있었다. 

2) 따라서, 불필요한 상태나 로직들이 많아지면서 Store 가 비대해지고, 비효율이 발생했다.

 

🔸그렇다면 어떻게 해결해야할까?

1) Store 에서 비동기 통신을 걷어내고, 온전한 Client Side 전역상태로 탈바꿈

2. Store 밖에서 서버와 관련된 상태 관리를 할 수 있어야하고, Server state 는 전역상태처럼 사용할 수 있어야함.

 

=> 이것들은 React-Query 로 해결할 수 있다. 

🔸리팩토링 방향성

1) 비동기 통신과 관련된 server state 는 React-Query 로 관리한다.

2) 인증 정보 관리, UI 상태관리 등 Client 전반에 전역으로 관리되는 상태는 Redux-toolkit 으로 관리한다.

 


1. React-Query 적용 후기

- createAsyncThunk 로 관리되던 로그인,로그아웃 기능을 react-query 로 변경하고 Client 전반에 전역으로 관리되는 상태 token 값과 userType 을 redux-toolkit 으로 관리했다.

 

🔸기존 코드

- 물론 내가 생각보다 createAsyncThunk 를 잘 관리 못해서 더 ... 복잡해보이는거 같기도하다 

import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
import axios from "axios";
import { BASE_URL } from "../constant/config";
import { setCookie, getCookie, removeCookie } from "../utils/Cookies";

const tokenItem = getCookie("token");
const typeItem = getCookie("userType");
const TOKEN = tokenItem === null ? null : tokenItem;
const USER_TYPE = typeItem === null ? null : typeItem;

interface LoginData {
  username: string;
  password: string;
  login_type: string;
}

interface LoginState {
  loginStatus: "loading" | "succeeded" | "failed";
  logoutStatus: "succeeded";
  error: string;
  token?: string | null;
  userType?: string;
}

const initialState: LoginState = {
  loginStatus: "loading",
  logoutStatus: "succeeded",
  error: "",
  token: TOKEN ? TOKEN : null,
  userType: USER_TYPE ? USER_TYPE : "BUYER",
};

export const fetchLogin = createAsyncThunk(
  "login/fetchLogin",
  async (
    { username, password, login_type }: LoginData,
    { rejectWithValue }
  ) => {
    try {
      const data = { username, password, login_type };
      const response = await axios.post(`${BASE_URL}/accounts/login/`, data);
      // console.log(response.data);

      if (response.data) {
        setCookie("token", response.data.token);
        setCookie("userType", response.data.user_type);
      }
      return {
        token: response.data.token,
        userType: response.data.user_type,
      };
    } catch (error: any) {
      console.log(error.response.data);
      return rejectWithValue(error.response.data.FAIL_Message);
    }
  }
);

export const fetchLogout = createAsyncThunk("login/fetchLogout", async () => {
  try {
    const response = await axios.post(`${BASE_URL}/accounts/logout/`);
    console.log(response);
    removeCookie("token");
    removeCookie("userType");
  } catch (error: any) {
    console.log(error.response.data);
  }
});

const loginSlice = createSlice({
  name: "login",
  initialState,
  reducers: {},
  extraReducers: (builder) => {
    // 로그인
    builder
      .addCase(fetchLogin.pending, (state) => {
        state.loginStatus = "loading";
      })
      .addCase(fetchLogin.fulfilled, (state, action) => {
        state.loginStatus = "succeeded";
        state.error = "";
        state.token = action.payload.token;
        state.userType = action.payload.userType;
      })
      .addCase(fetchLogin.rejected, (state, action) => {
        state.loginStatus = "failed";
        state.error =
          (action.payload as string | undefined) || action.error.message || "";
      })
      // 로그아웃
      .addCase(fetchLogout.fulfilled, (state) => {
        state.logoutStatus = "succeeded";
        state.loginStatus = "failed";
        state.token = null;
        state.userType = "BUYER";
        state.error = "";
      });
  },
});

export default loginSlice.reducer;

 

🔸react-query 로 비동기 코드 위임한 결과

- 확연히 코드가 가벼워졌다.

import { createSlice } from "@reduxjs/toolkit";
import { getCookie } from "../utils/Cookies";

const tokenItem = getCookie("token");

interface LoginState {
  token?: string | null;
  userType?: string;
}

const initialState: LoginState = {
  token: tokenItem ? tokenItem : null,
  userType: "BUYER",
};

const loginSlice = createSlice({
  name: "login",
  initialState,
  reducers: {
    updateToken: (state, action) => {
      state.token = action.payload;
    },
    changeUserType: (state, action) => {
      state.userType = action.payload;
    },
  },
});

export let { updateToken, changeUserType } = loginSlice.actions;

export default loginSlice.reducer;

참고자료

https://techblog.woowahan.com/6339/

 

Store에서 비동기 통신 분리하기 (feat. React Query) | 우아한형제들 기술블로그

오늘은 주문에서 사용하는 FE 프로덕트의 구조 개편을 준비하며 FE에서 사용하는 Store에 대해 개인적인 고민 및 팀원들과 검토하고 논의했던 내용을 소개합니다. 이 과정에서 생긴 여러 가지 의

techblog.woowahan.com