원티드 프리온보딩 챌린지 - 리팩토링 (4)

KINA KIM·2022년 8월 18일
1
post-thumbnail

1. React-Query

리팩토링 과제로 받은 리액트 쿼리를 적용하기 전 리액트 쿼리를 사용할 필요성이 있는지, 기존에 사용하고 있었던 상태 관리 라이브러리인 Redux보다 나은 점이 무엇인지 궁금했다.

React-query는 비동기 처리를 쉽게 도와주고, Client State와 관리에 적합한 라이브러리이다. 서버 데이터를 가져오고, 동기화 및 업데이트, 캐싱을 쉽게 만들어준다.

단순히 말하면 axios 등으로 처리하는 비동기 통신과 관련된 함수들과, 비동기 통신에 사용되는 응답과 상태, Client와 Server에서 사용하는 state까지 한 번에 관리할 수 있는 기능을 모두 제공하는 라이브러리다.

React-Query가 관여하는 State에는 두 종류가 있다.

  • Clinet State
    - 컴포넌트에서 관리하는 state, Redux Store 데이터
  • Sever State
    - 비동기 요청으로 받아오는 DB 데이터

기존에 사용한 Redux는 Server sate를 비동기 통신으로 받아와 dispatch를 통해 Redux Store에서 관리하는 state에 세팅했다.

Redux같은 전역 상태관리 라이브러리는 비동기 통신을 통해 서버 데이터를 받아와야지만 (명시적인 fetching을 통해서만) 데이터를 최신으로 갈아끼울 수 있다. 따라서 Store에서 저장된 데이터는 항상 최신 상태가 아니다. 또 비동기 통신으로 불러온 서버 데이터는 서버를 떠나 store에 저장된 이후 부터 원본 데이터가 아닌 복사본이 되버린다.

또한 Redux는 'API 통신 및 비동기 상태 관리를 위한 라이브러리'가 아니다. API Loading State, Error State, Success State등 API와 관련된 모든 응답을 개발자가 하나하나 결정하고 구현해야 한다. 자연스럽게 개발자마다 상황별로 데이터를 관리하는 방식이 달라질 수밖에 없고, 프로젝트의 규모가 커지거나 팀원들이 늘어날수록 관리 방법도 늘어날 것이다.

하지만 React-Query는 API 상태와 응답을 관리할 수 있는 규격화된 방식을 제공하기 때문에 유지보수와 협업에도 유용하다.

React-Query는 Server State에 변화가 생겼을 때 주기적으로 fetch하기 때문에 데이터를 항상 최신 상태로 유지할 수 있다. 또한 데이터 캐싱 기능 덕분에 서버에서 받아오려는 데이터가 이미 최신 버전이라면 캐시 메모리에 있는 데이터를 사용하여 리소스를 절약할 수 있다.

React-Query는 비동기 통신 로직 자체를 다루지는 않는다. React-Query가 다루는 정확한 기능은 '비동기 함수와 서버에서 받아온 데이터'에대한 캐싱과 패칭 기능 등이다. Promise를 리턴하는 axios.get('..')과 같은 코드는 개발자가 직접 구현해야 한다.

요약하자면 기존의 전역 상태관리 라이브러리는 클라이언트 상태값에는 잘 작동하지만, 서버 상태와 API 통신에 대해서는 유용하지 않았다. React-Query는 이러한 문제점을 보완할 수 있는 라이브러리다.

(1) 설치 및 초기 설정

비동기 통신을 위한 axios 라이브러리는 이미 설치된 상태이기 때문에 react-query만 설치했다.

yarn add react-query

캐싱을 사용하기 위한 QueryClientProvieder를 최상단 컴포넌트에 설정하여 Query Client에 접근할 수 있도록 세팅해준다.

//index.tsx
import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import App from "./App";
import { QueryClient, QueryClientProvider } from "react-query";
const queryClient = new QueryClient();
const root = ReactDOM.createRoot(
  document.getElementById("root") as HTMLElement
);

root.render(
  <QueryClientProvider client={queryClient}>
    <App />
  </QueryClientProvider>
);

(2) UseQuery와 Mutation

React-Query는 API 요청을 QueryMutation이라는 두 가지 유형으로 나누어 사용한다.

  • useQuery()
    useQuery hook으로 사용하는 query 요청은 'GET' 요청과 같이 서버에 저장되어 있는 데이터를 '가져올 때' 사용한다. useQuery는 사용할 때 'query key'를 필요로 하는데, React-Query는 query key를 통해서 데이터를 캐싱하고 관리한다.

useQuery('query-key', axios 요청(Promise))

  • useMutation()
    useMutation hook으로 사용하는 mutation 요청은 'POST, PUT, DELETE'와 같이 서버의 데이터를 수정할 때 사용된다.

useMutation(axios 요청(Promise), mutation option (onSucess, onError 등)

  • mutation option
    - onMutate(variables): mutation 요청 직전 처리되는 부분으로 여기서 반환하는 값은 하단 함수들의 context로 사용
    - onSucess(data, variables, context): 성공 시 처리
    - onError(data, varaibles, context): 에러 시 처리
    - onSttled(data, error, variables, context): 성공 여부와 관계 없이 작업이 끝날 시 처리

(3) React-Query 적용

  • hooks/query/todo.tsx에 query와 관련된 함수들을 작성했다. useQuery hook을 사용한 useGetTodos와 useMutation hook을 사용한 useCreateTodo 코드이다.
  • useCreaetTodo의 invalidateQuries는 'todos' query-key를 가진 데이터에 변화가 일어나면 새로운 상태로 refetch하겠다는 의미를 가진다. (업데이트 시 새로운 데이터를 받아옴)
export const useGetTodos = () => {
  return useQuery("todos", todoService.callGetTodosApi, {
    refetchOnWindowFocus: false,
  });
};

export const useCreateTodo = () => {
  const queryClient = useQueryClient();
  return useMutation(todoService.callCreateTodoApi, {
    onSuccess: () => {
      queryClient.invalidateQueries("todos");
    },
  });
};
  • 전체 Todos를 받아오는 TodoList.tsx의 React-query 적용 전 후 코드이다. dispatch를 사용하는 getTodoList 함수와 store에서 todos state를 받아오는 부분이 사라져 한결 코드가 깔끔해졌다.
  • mutate hook을 사용하는 useGetTodos로 한 줄만에 서버의 데이터를 받아와서 바로 사용할 수 있다.
  • 또한 기존 Redux의 원칙을 준수하기 위해 사용했던 다양한 보일러플레이트 코드를 더이상 사용할 필요가 없어졌다. 투두와 관련된 API나 기능이 추가되고 앱이 확장될수록 코드도 길어졌을 것이다.
// react-query 적용 전 TodoList.tsx
function TodoList() {
  const todoList: Todo[] = useSelector(
    (state: RootState) => state.todoReducer.todos
  );
  const dispatch = useDispatch();
  const navigate = useNavigate();
  const { "*": currentUrl } = useParams();

  //투두리스트 불러오기
  const getTodoList = async () => {
    const response = await callGetTodosApi();
    dispatch(getTodos(response?.data.data));
  };
// react-query 적용 후 TodoList.tsx
function TodoList() {
  const { data: todos } = useGetTodos();
  const navigate = useNavigate();
  const { "*": currentUrl } = useParams();

0개의 댓글