엄청 최근은 아니지만 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 요청과 관련된 번잡한 작업 없이 “핵심 로직”에 집중할 수 있습니다.
이전에서는 서버와의 API 통신과 비동기 데이터 관리에 Redux 를 사용했다. 서비스 특성에 따라 redux-thunk, redux-saga 등 다양한 미들웨어를 사용하기도 했는데, 나는 redux-toolkit 을 사용해본 경험이 있다.
Redux 는 무엇보다 매우 장황한 코드이다. 세 가지 기본 원칙 을 지키기 위해서는 많은 보일러 플레이트 코드가 요구된다. 이러한 이슈를 해결하기위해 redux-toolkit 이 등장했지만 아직까지 불필요하게 느껴지는 보일러플레이트 코드가 필요하다. 하나의 API 요청을 처리하기 위해 여러 개의 Action과 Reducer 가 필요하여 전체 코드가 잘 눈에 들어오지 않는다.
이는 Redux 가 비동기 데이터를 관리하기 위한 전문 라이브러리가 아니라, 범용적으로 사용할 수 있는 전역 상태 관리 라이브러리여서 생겨나는 현상이다. 미들웨어로 비동기 상태를 불러오고 그 값을 보관할 수는 있지만 내부적인 구현은 모두 개발자가 알아서 하다보니 상황에 따라 데이터를 관리하는 방식과 방법이 달라질 수 밖에 없다.
위에서 언급한 Redux 의 불필요하고 방대한 코드의 양을 줄여줄 수 있고, API 요청과 비동기 데이터 관리의 불편함을 해소할 수 있다.
fetching, caching, 서버 데이터와의 동기화를 지원해주는 라이브러리
공식문서에서는 위과 같이 설명한다.
React Application 에서 서버의 상태를 불러오고, 캐싱하며, 지속적으로 동기화하고 업데이트하는 작업을 도와주는 라이브러리이다. 친숙한 Hook 을 사용하여 React Component 내부에서 자연스럽게 서버(또는 비동기적인 요청이 필요한 Source)의 데이터를 사용하는 방법을 제안한다.
QueryClientProvider
컴포넌트로 감싸주고 QueryClient
값을 Props로 넣어줘야 합니다. 앱 전체에서 사용하고자하면 최상위 컴포넌트에 감싸주면됩니다.useQuery
hooks를 사용하면 됩니다.// 다른 키로 취급합니다.
useQuery(['post', 1], ...)
useQuery(['[pst', 2], ...)
// 객체 필드의 값이 달라도 다른 키로 취급합니다
useQuery(['post', { new: true }], ...)
useQuery(['post', { new: false }], ...)
더미데이터를 받아오는 api 를 사용할 수 있는 JSONPlaceholder 의 post 데이터들을 react-query 를 이용해서 가져온다.
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;
//types/post.ts
export interface Post {
id: number;
title: string;
body: string;
}
//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', postId]
로 지정//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);
};
posts
로 지정//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;
//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;
- 보일러플레이트의 코드의 감소
- API 요청 수행을 위한 규격화된 방식 제공
- 사용자 경험 향상을 위한 기능 제공