[TIL] React Query

jeongjwon·2023년 9월 18일
0

이론

목록 보기
6/19

React Query

엄청 최근은 아니지만 redux 보다 recoil 보다 요즘은 react query 를 쓴다고 얼핏 듣기도 했고, 추천도 많이 받아서 이번 기회에 제대로 익혀보고자 한다.

「if(kakao)2021 - 카카오페이 프론트엔드 개발자들이 React Query를 선택한 이유」
1. React Query는 React Application에서 서버 상태를 불러오고, 캐싱하며, 지속적으로 동기화하고 업데이트하는 작업을 도와주는 라이브러리입니다.
2. 복잡하고 장황한 코드가 필요한 다른 데이터 불러오기 방식과 달리 React Component 내부에서 간단하고 직관적으로 API를 사용할 수 있습니다.
3. 더 나아가 React Query에서 제공하는 캐싱, Window Focus Refetching 등 다양한 기능을 활용하여 API 요청과 관련된 번잡한 작업 없이 “핵심 로직”에 집중할 수 있습니다.

Redux 의 불편했던 점

이전에서는 서버와의 API 통신과 비동기 데이터 관리에 Redux 를 사용했다. 서비스 특성에 따라 redux-thunk, redux-saga 등 다양한 미들웨어를 사용하기도 했는데, 나는 redux-toolkit 을 사용해본 경험이 있다.

Redux 는 무엇보다 매우 장황한 코드이다. 세 가지 기본 원칙 을 지키기 위해서는 많은 보일러 플레이트 코드가 요구된다. 이러한 이슈를 해결하기위해 redux-toolkit 이 등장했지만 아직까지 불필요하게 느껴지는 보일러플레이트 코드가 필요하다. 하나의 API 요청을 처리하기 위해 여러 개의 Action과 Reducer 가 필요하여 전체 코드가 잘 눈에 들어오지 않는다.

이는 Redux 가 비동기 데이터를 관리하기 위한 전문 라이브러리가 아니라, 범용적으로 사용할 수 있는 전역 상태 관리 라이브러리여서 생겨나는 현상이다. 미들웨어로 비동기 상태를 불러오고 그 값을 보관할 수는 있지만 내부적인 구현은 모두 개발자가 알아서 하다보니 상황에 따라 데이터를 관리하는 방식과 방법이 달라질 수 밖에 없다.


그래서 React Query 는?

위에서 언급한 Redux 의 불필요하고 방대한 코드의 양을 줄여줄 수 있고, API 요청과 비동기 데이터 관리의 불편함을 해소할 수 있다.

fetching, caching, 서버 데이터와의 동기화를 지원해주는 라이브러리

공식문서에서는 위과 같이 설명한다.

React Application 에서 서버의 상태를 불러오고, 캐싱하며, 지속적으로 동기화하고 업데이트하는 작업을 도와주는 라이브러리이다. 친숙한 Hook 을 사용하여 React Component 내부에서 자연스럽게 서버(또는 비동기적인 요청이 필요한 Source)의 데이터를 사용하는 방법을 제안한다.

사용법

  • React Query를 사용 하기 위해선 우선 사용하고자 하는 컴포넌트를 QueryClientProvider 컴포넌트로 감싸주고 QueryClient 값을 Props로 넣어줘야 합니다. 앱 전체에서 사용하고자하면 최상위 컴포넌트에 감싸주면됩니다.
  • react-query 를 이용해서 데이터 패칭을 할때는 useQuery hooks를 사용하면 됩니다.
    • useQuery 는 GET Method 에 대한 요청을 보낸다. 즉, '상태'를 불러와 사용할 때 사용한다.
    • useQuery 의 첫번째 인자에는 응답 데이터를 캐시할 때 사용할 Unique Key로 문자열 혹은 배열값인 queryKey 값을 받게 되어있는데, 해당 queryKey값으로 데이터를 캐싱하게 됩니다.
    • 두번째 인자는 요청수행을 하기 위한 Promise를 반환하는 함수이다.
    • 마지막인자는 옵션
  • POST, PUT, DELETE 와 같은 요청은 useMutation hook 을 사용한다.
    • 첫번째 인자로는 Promise 를 반환하는 함수, 두번째는 옵션으로 사용법은 useQuery 와 동일하다.
      // 다른 키로 취급합니다. 
      	useQuery(['post', 1], ...)
        useQuery(['[pst', 2], ...)
        
        // 객체 필드의 값이 달라도 다른 키로 취급합니다
      useQuery(['post', { new: true }], ...)
      useQuery(['post', { new: false }], ...)

예제

더미데이터를 받아오는 api 를 사용할 수 있는 JSONPlaceholder 의 post 데이터들을 react-query 를 이용해서 가져온다.

App.tsx

import { QueryClient, QueryClientProvider } from 'react-query';
import { useState } from 'react';
import Posts from './components/Posts';
import Post from './components/Post';

const queryClient = new QueryClient();
function App() {
  const [postId, setPostId] = useState(-1);
  return (
    <QueryClientProvider client={queryClient}>
      <div className="App">
        {postId > -1 ? <Post postId={postId} setPostId={setPostId} /> : <Posts setPostId={setPostId} />}
      </div>
    </QueryClientProvider>
  );
}

export default App;
  • React Query 를 사용하겠다고 QueryClientProvider 로 컴포넌트를 감싸준다.
  • 최상위 컴포넌트 App.tsx 에서 postId 를 state 에 저장하고 해당 값으로 포스트를 조회하는 방식으로 구성했다.
  • postId 를 이용해서 하나의 post 를 가져오는 API 와 Post 전체를 가져오는 API를 useQuery 를 이용해서 커스텀 훅으로 생성한다.

Post.ts

//types/post.ts
export interface Post {
  id: number;
  title: string;
  body: string;
}

usePost.tsx

//hooks/usePost.tsx
import axios from 'axios';
import { useQuery } from 'react-query';
import { Post } from '../types/Post';

const getPostById = async (id: number): Promise<Post> => {
  const { data } = await axios.get(`https://jsonplaceholder.typicode.com/posts/${id}`);
  return data;
};

export const usePost = (postId: number) => {
  return useQuery(['post', postId], () => getPostById(postId), {
    enabled: !!postId
  });
};
  • 하나의 Post 를 가져오는 useQuery 커스텀 훅
  • usePost hooks 의 useQuery 는 ['post', postId] 로 지정
  • 배열의 키 값에 따라 데이터가 캐싱되어 postId 별로 가져온 데이터를 따로 캐싱할 수 있다.

usePosts.tsx

//hooks/usePosts.tsx
import axios from 'axios';
import { useQuery } from 'react-query';
import { Post } from '../types/Post';

const getPosts = async (): Promise<Array<Post>> => {
  const { data } = await axios.get('https://jsonplaceholder.typicode.com/posts');
  return data;
};

export const usePosts = () => {
  return useQuery('posts', getPosts);
};
  • 전체 Post 를 가져오는 useQuery 커스텀 훅
  • usePosts hooks 의 useQuery의 queryKey는 posts 로 지정

Post.tsx

//components/Post.tsx
import React, { useCallback } from 'react';
import { usePost } from '../hooks/usePost';

interface Props {
  postId: number;
  setPostId: React.Dispatch<React.SetStateAction<number>>;
}
const Post = ({ postId, setPostId }: Props) => {
  const { status, data, error, isFetching } = usePost(postId);

  const renderByStatus = useCallback(() => {
    switch (status) {
      case 'loading':
        return <div>Loading...</div>;
      case 'error':
        if (error instanceof Error) {
          return <span>Error: {error.message}</span>;
        }
        break;
      default:
        return (
          <>
            <h1>{data?.title}</h1>
            <div>
              <p>{data?.body}</p>
            </div>
            {isFetching && <div>Background Updating...</div>}
          </>
        );
    }
  }, [status, isFetching]);

  return (
    <div>
      <a onClick={() => setPostId(-1)} href="#">
        Back
      </a>
      {renderByStatus()}
    </div>
  );
};

export default Post;

Posts.tsx

//components/Posts.tsx
import React, { useCallback } from 'react';
import { useQueryClient } from 'react-query';
import { usePosts } from '../hooks/usePosts';

interface Props {
  setPostId: React.Dispatch<React.SetStateAction<number>>;
}

const Posts = ({ setPostId }: Props) => {
  const queryClient = useQueryClient();
  const { status, data, error } = usePosts();

  const renderByStatus = useCallback(() => {
    switch (status) {
      case 'loading':
        return <div>Loading...</div>;
      case 'error':
        if (error instanceof Error) {
          return <span>Error: {error.message}</span>;
        }
        break;
      default:
        return (
          <div>
            {data?.map((post) => (
              <p key={post.id}>
                <a
                  onClick={() => setPostId(post.id)}
                  href="#"
                  style={
                    queryClient.getQueryData(['post', post.id])
                      ? {
                          fontWeight: 'bold',
                          color: 'green'
                        }
                      : {}
                  }
                >
                  {post.title}
                </a>
              </p>
            ))}
          </div>
        );
    }
  }, [status]);

  return (
    <div>
      <h1>Posts</h1>
      {renderByStatus()}
    </div>
  );
};

export default Posts;
  • usePosts hooks의 리턴값이 useQuery 객체이므로, 그 중 status 값은 데이터 패칭 상태를 불러올 수 있고, data 는 해당 데이터 패칭함수 리턴하는 데이터, error 는 에러 발생시 에러값을 리턴받을 수 있다.
  • 데이터 패칭이 성공하면 해당 data 를 이용해 map 함수로 리스트를 불러오는데 inline style 로 설정한 부분에서 queryClient 에서 값을 받아오는 부분은 이미 데이터 캐싱 완료여부에 따라 폰트와 색상을 다르게 지정한다.

정리하자면

  • 보일러플레이트의 코드의 감소
  • API 요청 수행을 위한 규격화된 방식 제공
  • 사용자 경험 향상을 위한 기능 제공

0개의 댓글