[RTK] axios 대신 fetchBaseQuery

강동욱·2024년 1월 27일
0

RTK

목록 보기
1/1
post-thumbnail

상황

JWT 토큰 방식을 사용해서 회원가입과 로그인 기능을 구현하려고 했다. 처음이라 많이 어려웠지만 그중에서도 가장 이해가 어려웠던 부분은 상태관리 라이브러리에서 비동기를 처리하는 방법이였다. 지금하고 있는 weShare 프로젝트에서는 RTK를 사용하고 있으므로 비동기 처리는 axios를 사용해 RTK 자체에서 지원해주는 createAsyncThunk 메서드를 활용해 비동기를 처리할 생각이였다. 하지만 과연 이 방법만 있을까라는 생각이 들었고 또 다른 방법을 찾던 와중 RTK Query를 발견했다.

RTK Query란

RTK Query는 문서에 따르면 강력한 데이터 fetching, cashing 툴이라고 설명한다. 우리가 작성하는 fetching과 cashing에 필요한 코드를 간단하게 만들어 주는것이 핵심적인 기능이다.

왜 RTK Query를 사용했나?

솔직한 이유는 그저 여러가지의 방법을 사용해보고 싶었다.하지만 단지 이런 이유로는 이 기술이 왜 필요한가에 대한 설명으로는 한없이 부족하다고 생각해 RTK Query 이전에 axios와 함께 createAsyncThunk를 사용하려던 방식과 RTK Query를 방식을 비교해 보려고 한다.

번들 사이즈

공식 문서에 따르면 RTK Query는 RTK를 사용한 경우 최대 9kb이고 RTK를 사용하지 않은 경우 리액트를 사용한 경우에는 19kb + Reacy-Redux이고 리액트를 사용하지 않은 경우에는 17kb이다.

기존에 RTK를 쓰고 있었고 axios를 사용하면 RTK Query 보다 번들 사이즈보다 더 늘어나게 되므로 RTK Query를 사용하게 되었다.

  • If you are using RTK already
    ~9kb for RTK Query and ~2kb for the hooks.
  • If you are not using RTK already
    Without React
    17 kB for RTK+dependencies+RTK Query
    With React
    19kB + React-Redux, which is a peer dependency

간편한 로딩, 에러 상태관리

UX에 있어서 데이터를 불러올때는 로딩 상태 창 그리고 데이터 어떠한 이유로 불러오지 못했을 때는 error 창을 띄우는 것이 필수이다.

기존에 생각했던 방식으로 createAsyncThunk를 사용해 구현을 했더라면 error 상태와 loading의 상태를 해당하는 슬라이스에 추가해줘 extraReducer를 통해 처리해줘야 한다. 코드는 아래와 같다.

// 통신 에러 시 보여줄 에러 메세지의 타입
interface MyKnownError {
  errorMessage: string
}

// 통신 성공 시 가져오게 될 데이터의 타입
interface TodosAttributes {
  id: number;
  text: string;
  completed: boolean
}

// 비동기 통신 구현
const fetchTodos = createAsyncThunk<
  // 성공 시 리턴 타입
  TodosAttributes[],
  // input type. 아래 콜백함수에서 userId 인자가 input에 해당
  number,
  // ThunkApi 정의({dispatch?, state?, extra?, rejectValue?})
  { rejectValue: MyKnownError }
>('todos/fetchTodos', async(userId, thunkAPI) => {
  try { 
    const {data} = await axios.get(`https://localhost:3000/todos/${userId}`);
    return data;
  } catch(e){
    // rejectWithValue를 사용하여 에러 핸들링이 가능하다
    return thunkAPI.rejectWithValue({ errorMessage: '알 수 없는 에러가 발생했습니다.' });
  }
})

const todosSlice = createSlice({
  name: 'todos',
  initialState,
  reducers: {
    ...
  },
  extraReducers: (builder) => {
    builder
      // 통신 중
      .addCase(fetchTodos.pending, (state) => {
        state.error = null;
        state.loading = true;
      })
      // 통신 성공
      .addCase(fetchTodos.fulfilled, (state, { payload }) => {
        state.error = null;
        state.loading = false;
        state.todos = payload;
      })
      // 통신 에러
      .addCase(fetchTodos.rejected, (state, { payload }) => {
        state.error = payload;
        state.loading = false;
      });
  },
})

RTK query를 이용하면 아래와 같다

// todoApiSlice.ts
interface TodosAttributes {
  id: number;
  text: string;
  completed: boolean;
}

// baseurl을 지정하고 endpoints프로퍼티에 사용할 endpoint를 설정한다
export const todoApiSlice = createApi({
  reducerPath: "hi",
  baseQuery: fetchBaseQuery({ baseUrl: "https://localhost:3000" }),
  endpoints: (builder) => ({
    getUser: builder.query<TodosAttributes[], void>({
      query: (userId) => `/todos/${userId}`,
    }),
  }),
});

// 엔드 포인트에 따라 자동적으로 rtk-query가 훅을 생성해준다.
export const { useGetUserQuery } = todoApiSlice;


// todosSlice.ts

// createAsyncThunk를 사용하지 않아서 extraReduer는 따로 필요하지 않다.
const todosSlice = createSlice({
  name: "todos",
  initialState: {},
  reducers: {},
});

// store.ts

const store = configureStore({
  reducer: {
    //스토어에 다음과 같이 추가한다. 
    [todoApiSlice.reducerPath]: todoApiSlice.reducer
  },

  // 미들웨어에 등록함으로써 todoApiSlice가 rtk-query의 기능을 하게된다.
  middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(todoApiSlice.middleware)
})


// Todo.tsx

const Todo  = () => {
  // rtk-query가 자동으로 생성한 훅을 통해 로딩 상태나 에러 상태를 관리할 수 있다.
  const { data, error, isLoading} = useGetUserQuery('dong')
  return <div></div>
} 

RTK Query는 이와 같이 하나의 훅을 통해 로딩 상태나 에러 상태를 관리할 수 있다. 또한 하나의 baseUrl을 둔 상태로 각각의 다른 endpoint를 관리할때 todoApiSlice의 endpoints프로퍼티에 추가만 해주면 되서 쉽게 관리할 수 있다.

🚨 주의

In most cases, you should use this once per app, with "one API slice per base URL" as a rule of thumb.

-RTK-Query 공식문서

하나의 베이스 url에서는 하나의 api slice를 생성해야 한다. 아래의 예시를 참고해 보자

// ❌ BAD
const apiSlice = createApi({
  baseQuery: fetchBaseQuery({baseUrl:'https://localhost:3000/auth'}),
  endpoints: () => ({})
})

const apiSlice2 = createApi({
  baseQuery: fetchBaseQuery({baseUrl:'https://localhost:3000/todos'}),
  endpoints: () => ({})
})

// ✅ GOOD
const apiSlice = createApi({
  baseQuery: fetchBaseQuery({baseUrl:'https://localhost:3000'}),
  endpoints: (builder) => ({
    getAuth: builder.query({
      query: (name) => `/auth/${name}`
    }),
    getTodos: builder.query({
      query:  () => `/todos`
    })
  })
})

간편한 Interceptor 기능

액세스 토큰이 만료가 되었을 떄 리프레시 토큰을 통해 재발급을 받아야 하는 상황일 때 axios에서는 interceptor의 기능을 사용해서 다음과 같이 사용할 수 있다.

import useAuth from "./useAuth";

const useAxiosPrivate = () => {
    const refresh = useRefreshToken();
    const { auth } = useAuth();

    useEffect(() => {

        const requestIntercept = axiosPrivate.interceptors.request.use(
            config => {
                if (!config.headers['Authorization']) {
                    config.headers['Authorization'] = `Bearer ${auth?.accessToken}`;
                }
                return config;
            }, (error) => Promise.reject(error)
        );

        const responseIntercept = axiosPrivate.interceptors.response.use(
            response => response,
            async (error) => {
                const prevRequest = error?.config;
                if (error?.response?.status === 403 && !prevRequest?.sent) {
                    prevRequest.sent = true;
                    const newAccessToken = await refresh();
                    prevRequest.headers['Authorization'] = `Bearer ${newAccessToken}`;
                    return axiosPrivate(prevRequest);
                }
                return Promise.reject(error);
            }
        );

        return () => {
            axiosPrivate.interceptors.request.eject(requestIntercept);
            axiosPrivate.interceptors.response.eject(responseIntercept);
        }
    }, [auth, refresh])

    return axiosPrivate;
}

export default useAxiosPrivate;

responseintercept를 보면 응답을 받기전에 interceptor메서드를 사용해 액세스 토큰을 재발급 받고 헤더를 설정해 준다.

RTK Query에서는 다음과 같이 에러를 처리해준다.

const baseQuery = fetchBaseQuery({
  baseUrl: " http://localhost:5174",
  credentials: "include",
  prepareHeaders: (headers, { getState }) => {
    const token = (getState() as RootState).auth.token;

    if (token) {
      headers.set("authorization", `Bearer ${token}`);
    }
    return headers;
  },
});

const baseQueryWithReauth: BaseQueryFn = async (arg, api, extraOption) => {
  let result = await baseQuery(arg, api, extraOption);
  console.log(result);

  if (result?.error && result?.error?.status === 403) {
    const refreshResult = await baseQuery(
      "/auth/reissue-token",
      api,
      extraOption,
    );

    if (refreshResult.data) {
      const user = (api.getState() as RootState).auth.user;
      const token = (refreshResult.data as ResponseAT).accessToken;

      api.dispatch(setCredentials({ token, user }));
      result = await baseQuery(arg, api, extraOption);
    } else {
      await baseQuery(
        { url: "/auth/logout", method: "POST" },
        api,
        extraOption,
      );
      api.dispatch(logout());
    }
  }

  return result;
};

baseQueryWithReauth를 보면 별다른 메서드 없이 403이 발생할 때setCredential 액션 함수를 이용해서 재발급 토큰을 설정해준다. 그러면 basequery에도 보다시피 토큰이 있을 경우 헤더에 토큰을 설정해준다. 그래서 axios와 같이 따로 커스텀훅을 만들지 않고 그냥 RTK Query에서 자동으로 생성해주는 훅을 사용하면 자동으로 헤더에 토큰을 실어준다.

출처
https://redux-toolkit.js.org/rtk-query/overview#apis
https://bundlephobia.com/package/axios@1.6.7

profile
차근차근 개발자

0개의 댓글