menulogo

Tanstack query (React-query v4 도입하기)

@corinthioniaAugust 5, 2023

쉽고 빠른 약속 정하기 ⏰ 모두의 시간 🐰

내가 맡은 부분의 QA를 모두 해결하고 미뤄두었던 Tanstack query 도입을 진행했다. React query에 대해서 이전부터 많이 들어왔고, 또 많이 사용한다 하여 꼭 한번 사용해 보고 싶었는데, 다행히 여유 시간이 생겨 혼자 직접 도입해 볼 수 있었다!

⛓️ feat: tanstack-query 적용


1. React query?

React query 공식문서에 따르면, 전통적인 상태 관리 라이브러리들은 클라이언트 상태 관리에는 매우 유용하지만, 비동기 상태나 서버 상태를 제대로 관리하지 못한다고 한다.

서버 상태 (Server State)란,

  • 제어하거나 소유하지 않는 위치에서 원격으로 유지됨
  • fetching과 updating을 위해 비동기 API가 필요함
  • 공유 소유권을 의미하며 다른 사용자가 변경할 수 있음
  • 주의하지 않을 경우 애플리케이션에서 "오래된(out-of-date)" 상태가 될 수 있음

간단하게 말하자면 클라이언트 상태는 굳이 DB에 저장하지 않아도 되는 state 값들을 의미하고, 서버 상태는 DB에 저장하여 서버로부터 가져온 값을 의미한다.

따라서 react query를 사용한다면, 서버 상태 가져오기, 캐싱, 동기화 및 업데이트를 보다 쉽게 다룰 수 있다.


2. 프로젝트에 도입하기

1) React Query 도입 이전

React query 도입 이전에는, useEffect 내부에서 비동기 함수를 생성해 주고 get 요청을 통해 받아온 데이터를 state에 저장하는 방식을 사용했다.

useEffect(() => {
  const getRoomInfo = async () => {
    const { data } = await instance.get(`/api/room/${roomUUID}`);
    setRoom(data);
  };

  getRoomInfo();
}, []);

이와 같은 방법은 client state와 server state가 분리되지 않은 상태라고 할 수 있다. 왜냐하면 서버로부터 받아온 데이터를 client state인 room에 복사하여 저장하기 때문이다. 이럴 경우 예상치 못한 버그 등이 발생할 수 있기 때문에 더욱 react query의 도입이 필요했다.

2) 초기 세팅하기

yarn add @tanstack/react-query

설치가 완료되었으면, index.tsx 파일에서 초기 세팅을 진행한다.

// index.tsx

import { QueryClient, QueryClientProvider } from '@tanstack/react-query';

const queryClient = new QueryClient();

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

queryClient 인스턴스를 생성하고, QueryClientProvider를 이용해 App 컴포넌트를 감싸 준다. 이때 QueryClientProviderclient 값으로는 위에서 생성한 queryClient를 넣어준다.

3) 폴더 구분하기

먼저 나는 react query를 처음 사용해 보는 입장이었기 때문에, 보통 어떤 식으로 작성하는지 감을 잡기가 어려웠다. 따라서 여러 글들도 찾아보고, react query를 사용한 여러 프로젝트들의 코드를 살펴보면서 공통적으로 많이 사용하는 방식을 사용해 보았다.

  • /api query function - api 호출 함수들을 저장한다
  • /constants/QUERY_KEYS.ts query key들을 constant로 저장한다
  • /queries 커스텀 훅을 제작하여 useQuery 혹은 useMutation을 리턴한다

4) api 호출 함수 작성하기

이전에 axios instance를 이용하여 api를 호출했던 방식과 비슷하게 작성해 주면 된다.

import { instance } from './instance';
import { PostRoomTypes } from '@/types/roomInfo';

export const getRoomInfo = async (roomUUID: string) => {
  const { data } = await instance.get(`/api/room/${roomUUID}`);

  return data;
};

위 코드는 roomUUID의 정보를 가지고 해당 방의 정보를 가져오는 함수이다. 이때! parameter 값으로 roomUUID를 사용했다는 것을 기억해 두자.

5) Query key를 constant로 관리하기

추후 유지보수성을 높이기 위해 query key 값들은 constant로 관리해 주었다!

export const QUERY_KEYS = {
  ROOM: {
    GET_ROOM_INFO: 'get-room-info',
  },
};

6) 잠깐! useQuery와 useMutation

Custom hook을 작성하기 이전에 useQueryuseMutation부터 알고 가는 게 좋다. 간단히 설명하자면, useQuery는 데이터를 받아올 때(get) 사용하고, useMutation은 값을 수정할 때(post, patch, put, ...) 사용한다.

더 간단히 설명하자면, CRUD 작업 중 useQuery는 R 작업에 사용하고, useMutation은 CUD 작업에 사용한다.

보다 자세한 설명은 아래에서 하겠다.

7-1) useQuery - Custom hooks 만들기

queries 라는 새로운 폴더를 만들어 api 호출을 위한 custom hooks를 모아두었고, 각각 useQuery 또는 useMutation을 리턴한다.

import { useQuery } from '@tanstack/react-query';
import { getRoomInfo } from '../../api/room';
import { QUERY_KEYS } from '../../constants/QUERY_KEYS';

export const useGetRoomInfo = (roomUUID: string) => {
  return useQuery([QUERY_KEYS.ROOM.GET_ROOM_INFO, roomUUID], () => getRoomInfo(roomUUID));
};

먼저, useQuery의 첫 번째 파라미터에는 unique한 query key 값들이 들어가고, 두 번째 파라미터로 api 호출을 위한 비동기 함수가 들어간다. 이때 query key 값들로 비동기 함수의 parameter 값들까지 넣어 주어야 한다! 따라서 위에서 언급했던 대로 roomUUID도 query key 값으로 넘겨 준다.

또, useQuery는 비동기로 동작하기 때문에, 만약 여러 개의 비동기 쿼리가 존재한다면 useQueries를 사용하는 것이 더 좋다. 만약 동기적으로 동작하게 하고 싶다면, useQuery의 세 번째 인자로 option 값을 넘겨 줄 수 있는데, 여기에 enabled를 설정해 주면 된다.

7-2) useMutation - Custom hooks 만들기

import { useMutation } from '@tanstack/react-query';
import { createRoom } from '@/api/room';
import { PostRoomTypes } from '@/types/roomInfo';

export const useCreateRoom = () => {
  return useMutation((payload: PostRoomTypes) => createRoom(payload));
};

useQuery와 크게 다를 건 없지만, 아무래도 useMutation는 값을 변경할 때 사용하다 보니 payload 값을 전달해 주어야 한다. 따라서 위와 같이 작성해 준다.

8-1) useQuery - Custom hooks 사용하기

const { data, error, isLoading, isError, isSuccess } = useGetRoomInfo(roomUUID);

위에서 작성한 useGetRoomInfo hook을 불러오고, 비구조화 할당을 통해 바로 변수에 값을 저장한다.

  • data: TData 말 그대로 서버로부터 받아온 데이터를 의미한다.
  • error: null | TError 에러 발생 시 error object를 의미한다.
  • isLoading: boolean 캐시된 데이터가 없거나 query 요청이 아직 끝나지 않았을 경우 true를 리턴한다.
  • isSuccess: boolean 성공적으로 데이터를 받아온 경우 true를 리턴한다.
  • isError: boolean 데이터를 받아오는 도중 에러가 발생했을 시 true를 리턴한다.

8-2) useMutation - Custom hooks 사용하기

const { mutate, data, isError, isSuccess } = useCreateRoom();

우선은 useQuery를 사용할 때와 마찬가지로 비구조화 할당을 통해 변수에 바로 할당해 준다.

useEffect(() => {
  if (isChecked) {
    mutate(room);

    if (isError) {
      alert('오류가 발생했습니다.\n처음부터 다시 시도해 주세요');
    }

    if (isSuccess) {
      navigate(`/`);
    }
  }
}, [room, isError, isSuccess]);

그리고 useEffect를 이용하여 isError, isSuccess인 상태에 대해 처리를 해 주었다. 위에서처럼 mutate() 함수에 payload를 전달해 주면 된다.


후기

React query를 도입하고 나니, 데이터 요청이 필요한 컴포넌트에서는 확연하게 코드 수를 줄일 수 있다는 장점이 있었다. 하지만 아직 React query의 모든 기능을 알지 못하기에 조금 더 공부해 보고 익숙해지려고 한다.

마지막으로 혼자 직접 react query를 도입하고 프로젝트의 관련된 모든 부분을 변경했던 흔적,, ✨


Reference

← 이전 글모바일 환경에서 애니메이션 사용 시 잔상이 생길 때
다음 글 →CRA에서 Vite로 전환할 결심