[RTK Query] 왜 RTK Query를 사용하였는가?

LMH·2023년 3월 30일
3
post-thumbnail

코드스테이츠 부트 캠프의 메인 프로젝트에서 데이터 패칭을 위해서 사용한 RTK Query를 사용한 이유와 사용하면서 느낀점을 간단하게 정리하겠습니다.

해당 기술을 적용하기 전에 사용한 기술

저희 프로젝트에서는 외부 상태관리 툴로 RTK(Redux-Tool-Kit)을 사용했으며 Redux를 사용해 비동기적인 작업을 처리하기 위해 Redux-saga 미들웨어를 도입했습니다.

이전 기술에서 겪은 불편함

Redux-saga를 사용하게 되면 기능을 추가할 때 마다 필요한 Action, Saga들을 새롭게 정의해줘야 했기 때문에 보일러 플레이트가(코드 볼륨)이 증가하는 단점이 있었습니다.

이 기술을 선택한 이유

저희 팀에서는 RTK Query와 React Query 도입을 검토했습니다. 특정 조건에서 비동기적으로 서버 데이터를 자동으로 패칭해 주는 기능은 동일 했으나, React Query라는 라이브러리를 추가적으로 사용하는 것 보다 RTK에 내장되어 있는 RTK Query를 사용하는 것이 번들링 사이즈를 줄이고 전체적인 프로젝트 구조를 잡는데 적합하다고 생각했습니다.

새롭게 도인한 기술을 사용하면서 느낀 좋았던 점

첫 번째로, Query와 Mutation을 통해서 캐싱된 서버 데이터를 간편하게 화면에 보여줄 수 있다는 것이 가장 큰 장점이었습니다. tag로 연결된 컴포넌트에서는 서버 데이터가 변경될 때문에 새로운 데이터를 자동으로 패칭하기 때문에 보다 간결한 코드를 작성할 수 있었습니다.

// postDetail.tsx

import { postsApi } from '../api/postApi';

// useGetPostQuery Hook을 사용해 데이터를 간단하게 패칭
const postDetailQuery = postsApi.useGetPostQuery({ postId });
const { data, isSuccess, isLoading, refetch } = postDetailQuery;

// Mutation Hook을 사용해 서버에 요청을 보내고 변경된 캐싱 데이터를 최신화
const [deletePost] = postsApi.useDeletePostMutation();
const [addThumbUp] = postsApi.useAddPostThumbUpMutation();
const [deleteThumbUp] = postsApi.useDeletePostThumbUpMutation();
const [addThumbDown] = postsApi.useAddPostThumbDownMutation();
const [deleteThumbDown] = postsApi.useDeletePostThumbDownMutation();
const [addBookmark] = postsApi.useAddBookmarkMutation();
const [removeBookmark] = postsApi.useRemoveBookmarkMutation();
const [deleteComment] = commentsApi.useDeleteCommentMutation();
const [deleteReply] = repliesApi.useDeleteReplyMutation();
const [sendReport] = reportApi.usePostReportMutation();

번째로, 하나의 fetchBaseQuery를 정의하고 추가적으로 tag와 Endpoint를 주입해 반복적인 코드를 줄일 수 있었습니다.

아래의 코드를 보면 apiSlice라는 파일에서 fetchBaseQuery에서 Access Token을 요청 헤더에 설정하고baseQueryWithReauth 라는 비동기 함수 내부에서 Access Token과 Refresh Token을 핸들링하는 하고 있는 것을 알 수 있습니다.

이렇게 특정 로직을 작성하고 createApi 메소드를 사용해 생성한 apiSlice에 Tag와 Endpoint를 주입해서 재사용이 가능합니다.

// apiSlice.ts

import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
import Cookies from 'js-cookie';

// Default baseQuery: 모든 요청에 access token을 포함시켜 요청
const baseQuery = fetchBaseQuery({
  baseUrl: process.env.REACT_APP_SERVER_ADDRESS,
  credentials: 'include',
  prepareHeaders: (headers) => {
    const accessToken = Cookies.get('Authorization');
    if (accessToken) {
      headers.set('Authorization', accessToken);
    }
    return headers;
  },
});

// 권한 관련 오류와 access token 이 만료되었을때 실행하는 refresh token flow
export const baseQueryWithReauth = async (
  args: any,
  api: any,
  extraOptions: any,
) => {
  let result = await baseQuery(args, api, extraOptions);

  type ErrorResHeader = {
    status: number;
    message: string;
  };

  // 접근 권한이 없는 경우 (일반 유저가 관리자 페이지로 들어갔을때)
  if (
    result?.error?.status === 403 &&
    (result?.error?.data as ErrorResHeader)?.message === 'User unauthorized'
  ) {
    alert('접근할 수 없는 페이지입니다.');
  }

  // 기타 인증과정 오류 발생 (내가 쓴 글이 아닌데 삭제나 수정할때)
  if (
    result?.error?.status === 403 &&
    (result?.error?.data as ErrorResHeader)?.message === 'Authorized Fail'
  ) {
    alert('요청을 수행할 수 없습니다.');
  }

  // access token이 만료되었다는 status와 메세지를 받으면, 새로운 access token 발급을 위해 refresh token 보내기
  if (
    result?.error?.status === 401 &&
    (result?.error?.data as ErrorResHeader)?.message === 'Access token expired'
  ) {
    const name = localStorage.getItem('name');

    const refreshResult = await baseQuery(
      {
        url: `/auth/refresh/${name}`,
        method: 'POST',
        headers: {
          Refresh: Cookies.get('Refresh'),
        },
      },
      api,
      extraOptions,
    );

    // response headers로 온 새로운 access token에 접근하기
    const headers = refreshResult?.meta?.response?.headers;
    const accessToken = headers?.get('Authorization');

    // accessToken이 있다면 쿠키에 저장하고 다시 api 요청하기
    if (accessToken) {
      Cookies.set('Authorization', accessToken!);
      result = await baseQuery(args, api, extraOptions);
    } else {
      //만약 refresh token도 만료되어 acces token이 없다면 로그아웃 시키기.
      Cookies.remove('Authorization');
      Cookies.remove('Refresh');
      localStorage.clear();

      alert('재로그인 해주세요.');
      window.location.href = '/';
    }
  }
  return result;
};

// Wrap baseQuery with the baseQueryWithReauth function
export const apiSlice = createApi({
  baseQuery: baseQueryWithReauth,
  endpoints: (builder) => ({}),
});
//postApi.ts

import { apiSlice } from './apiSlice';

// apiSlice에 태그와 엔드포인트 주입
// RecomendedPosts 정보를 가져오는 query에 Token 핸들링 로직을 포함 시킨다.
export const recommendedPostsApi = apiSlice
  .enhanceEndpoints({ addTagTypes: ['RecomendedPosts'] })
  .injectEndpoints({
    endpoints: (builder) => ({
      getRomendedPosts: builder.query({
        query: ({ recommend }) => `posts/${recommend}`,
        providesTags: ['RecomendedPosts'],
      }),
    }),
  });

기술을 사용하면서 느낀 불편했던 점

React Query에 비해서 레퍼런스가 부족했고 공식문서에 의존해서 로직을 작성하는데 생각보다 더 많은 시간이 소요되는 점이 불편했습니다.

더 나아질 수 있는 방향

프로젝트를 진행하는 과정에서 기본으로 제공되는 fetchBaseQuery만을 사용했는데 커스터 마이징을 통해서 반복된 코드를 줄이고 기능을 확장 시킬 수 있을 것이라고 생각합니다.

profile
새로운 것을 기록하고 복습하는 공간입니다.

0개의 댓글