React Query 잘 사용하기 - #2. React Query hook을 Custom hook으로 랩핑하기

YI·2024년 3월 21일
0

React Query

목록 보기
2/3
post-thumbnail

안녕하세요, 지난 글에 이어 이번에는 상용 프로젝트에서 React Query의 useQuery, useMutation 등과 같은 hook 어떻게 사용하였는 지에 대해 이야기해보고자 합니다.

React Query 도입 배경과 QueryKey 관리 방법이 궁금하다면, 1편 React Query 잘 사용하기 - #1. React Query 도입 이유와 QueryKey 관리 전략을 참고해주세요.

React Query 이렇게 사용했어요

사용 버전 : @tanstack/react-query : v4.35.3

들어가기 앞서..
아래의 내용이 React Query를 사용하는 Best Practice가 아닙니다. 많은 시행착오를 겪으며 나름대로 정립한 React Query 사용법을 공유하는 글 입니다. 건설적인 태클과 지적, 제안은 언제나 환영입니다!

1. Query Key 관리하기

2. Custom hook 사용하기

제가 React Query를 도입하며 가장 고민을 많이 한 점은 "어떻게 하면 일관성 있고, 편리하게 React Query의 Hook을 사용할 수 있을까?" 입니다.

일관성 있고 편리한 React Query 사용을 위해, React Query에서 제공하는 useQuery,useMutation,useInfiniteQuery과 같은 Hook인 등을 Custom Hook으로 감싸 모듈화하였습니다.

React Query의 hook을 Custom hook을 사용해 별도로 분리해 사용하는 방법을 보기 전, "왜" 모듈화가 필요한 지 먼저 알아보겠습니다.


① React Query의 hook을 Custom hook으로 모듈화 해야하는 이유

아래의 예시 코드를 통해 어떤 문제가 있고, 이를 어떻게 해결할 지 확인해봅시다.

먼저 이 코드는 특정 사용자의 프로필을 표시하고 관리하는 컴포넌트입니다.

이 컴포넌트에서는 useQuery hook을 사용해서 사용자 정보를 가져오고,
저장하기 버튼을 클릭하면 useMutation hook의 mutate 함수를 호출하여 서버에 PATCH 요청을 보내는 비동기 함수를 실행합니다.

export const UserProfile = (props) => {
  	const { userId } = props;
  
  	const queryClient = useQueryClient();
  
	const { data, isLoading, isError } = useQuery(queryKeys.user({ id: userId }), async ({ queryKey }) => {
		const [_, params] = queryKey;
  
      return await axios.get(
          "https://sample/api/v1/user",
          {
            params,
            headers: { Authorization: "my-access-token" },
          }
        );
 	},
    { staleTime: 60000 });
  
  	const { mutate } = useMutation(
      (userId) =>
        axios.patch(
          `https://sample/api/v1/user`,
          { status: "resolved" },
          { headers: { Authorization: "my-access-token" } }
        ),
      {
        onMutate: async (variables) => {
        	const { id } = variables;
          
          	queryClient.invalidateQueries(queryKeys.user({ id }));
        }
      });
  
	return (
  	  <UserProfileContainer>
      	<ProfileImageContainer />
      	<AccountSetting />
      	<button onClick={() => mutate({ id: userId })}>저장하기</button>
      </UserProfileContainer>
	);
}

이 컴포넌트는 UI와 관련된 코드와 API 관련 다양한 코드들이 복잡하게 결합되어 있습니다. 그리고 다른 컴포넌트에서도 작성된 유사한 코드들(Axios, header 토큰, onMutate 함수 등)이 불필요하게 반복되고 있습니다.이와 같은 형식은 코드의 유연성과 재사용성을 감소시키고, 유지보수를 어렵게 만듭니다. 그리고 다른 컴포넌트에서도 비슷한 API 호출을 반복해야하기 때문에 불필요한 중복이 많이 발생합니다.

우리가 React Query의 hook을 Custom hook으로 분리해 관리해야하는 이유가 바로 여기에 있습니다.

API 호출과 관련된 useQuery와 useMutation을 별도의 Custom hook으로 분리하면 컴포넌트의 관심사가 분리되어 유지보수가 용이하고, 다른 컴포넌트에서도 동일한 로직을 쉽게 재사용할 수 있게 됩니다.

아래는 Custom hook을 사용해 API 호출을 모듈화한 UserProfile 컴포넌트입니다.

export const UserProfile = ({ userId }) => {
  const { data, isLoading, isError } = useFetchUser({ userId });
  const { mutate } = useUpdateUser();

  return (
    <UserProfileContainer>
      <ProfileImageContainer />
      <AccountSetting />
      <button onClick={() => mutate({ id: userId })}>저장하기</button>
    </UserProfileContainer>
  );
};

② Custom Hook 구조 설계
이제 React Query hook들을 어떻게 Custom hook으로 분리할 지 살펴보겠습니다.

먼저 1개의 API endpoint에 대해 1개의 Custom hook 만듭니다.

예를 들어, GET /api/v1/user 를 호출하는 Custom hook useFetchUser.js 파일을 생성합니다. 이렇게 하면 각각의 API 엔드포인트마다 따로 Custom Hook을 만들어 사용할 수 있어 코드를 관리하기 쉬워지고, 각 API 호출에 대한 로직이 분리되어 재사용하기도 편리합니다.

그 다음 아래와 같이 API 레이어를 분리했습니다.

  • React Query hook
  • API 호출 함수 분리
  • 글로벌 Axios 인스턴스 사용

Step 1 : React Query hook 분리

사용자 정보를 fetching하는 useQuery hook을 아래와 같이 Custom hook으로 분리합니다. 이렇게 Custom hook으로 쿼리 호출을 분리하면, 사용자 정보를 필요로 하는 곳 어디에서나 useFetchUser hook을 호출하기만 하면 되기 때문에 되므로 코드를 더욱 간결하게 만들 수 있습니다.

// useFetchUser.ts
export const useFetchUser = (props) => {
  	const { userId } = props;
  
	return useQuery(queryKeys.user({ id: userId }), async { queryKey }) => {
	const [_, params] = queryKey;
     
    return await axios.get("https://sample/api/v1/user",{
          params,
          headers: { Authorization: "my-access-token" },
     });
    }, { staleTime: 60000 });
}

Step 2: API 호출 함수 분리
이 예시의 경우, API를 실제로 호출하는 함수가 매우 간단하지만, 상황에 따라 복잡하고 길이가 긴 함수를 호출해야할 때가 있습니다. 이런 경우에 함수가 useQuery hook의 파라미터 형태로 작성되어있다면 가독성이 매우 떨어집니다.

이러한 단점을 해결하기 위해, 아래와 같이 API 호출 함수를 분리했습니다.

// useFetchUser.ts
const fetchUser = async { queryKey }) => {
	const [_, params] = queryKey;
  
	return await axios.get(
		"https://sample/api/v1/user",
        {
          params,
          headers: { Authorization: "my-access-token" },
        }
      );
 };

export const useFetchUser = (props) => {
  	const { userId } = props;
  
	return useQuery(queryKeys.user({ id: userId }), fetchUser, { staleTime: 60000 });
}

제 경우, useQuery hook과 API 호출 함수를 따로 각각의 파일로 분리하지 않고 useFetchUser.js hook에서 한 번에 관리하고 있습니다. 두 개를 하나의 파일에서 통합해서 관리하는 것이 응집성과 유지보수 측면에서 더 유리하다고 생각했기 때문입니다.

하지만 이 방법이 정답은 아닙니다. 상황에 따라 혹은 개인의 취향에 따라 어떻게 관리할 지 선택하시면 됩니다!

Step 3: 글로벌 Axios 인스턴스 사용
다음은 반복적으로 사용되는 Axios 관련 코드들을 리팩토링해보겠습니다. 매번 api를 요청할 때마다 반복적으로 사용되는 API 기본 URL, header 등을 분리하는 것이 이상적입니다.

새로운 api 버전이 바뀌거나, base url이 변경되거나, production과 develop 버전의 base url이 바뀌는 등의 상황이 흔하기 때문에 글로벌 Axios 인스턴스를 만들어 사용하는 것이 좋습니다.

아래와 같이 api.js 파일을 생성하고 baseUrl, axios 관련 코드, 사용자 정의 HTTP 메서드 등을 정의합니다.

// api.js
const apiUrl = '/api/v1/';

export { apiUrl };


/.. axios 관련 다른 코드들 ../

export const get = (
  path,
  params = {},
  baseUrl = "",
) => {
  const instance = createAxiosInstance(baseUrl);

  return instance
    .get<ServerResponseDto<T>>(path, {
      params,
    })
    .then((response) => {
      return Promise.resolve(response.data.data);
    })
    .catch((err) => {
      console.log(`Error in :: GET :: ${path} :: Failed!`);
      return Promise.reject(err);
    });
};

이렇게 정의한 Axios 인스턴스를 적용해 우리의 useFetchUser 훅을 수정하면 아래와 같습니다.

// useFetchUser.ts
const fetchUser = async { queryKey }) => {
	const [_, params] = queryKey;
  
	return await axios.get("user", params);
};

export const useFetchUser = (props) => {
  	const { userId } = props;
  
	return useQuery(queryKeys.user({ id: userId }), fetchUser, { staleTime: 60000 });
}

이어지는 포스트

이번 포스트에서는 Custom hook을 활용하여 React Query의 Hook들을 모듈화해야하는 이유와 방법에 대해 알아보았습니다.최대한 풀어 쓰려 노력하였지만, 혹시 이해가 되지 않거나 궁금한 점이 있다면 언제든 편하게 질문해주세요.

다음에는 "React Query 이렇게 사용했어요 - 폴더 구조" 편으로 돌아오겠습니다. 긴 글 읽어주셔서 감사합니다!

Reference

https://profy.dev/article/react-architecture-api-layer#the-final-state-of-separation
https://medium.com/hcleedev/web-react-query-custom-hooks%EB%A1%9C-%EB%8D%94-%EC%9E%98-%EC%82%AC%EC%9A%A9%ED%95%B4%EB%B3%B4%EA%B8%B0-2ea47fb358c3

profile
Junior Frontend Developer

0개의 댓글