React Query 란?
🤔 React Query 란 무엇인가?
왜 React Query 를 알아보고 싶었는가?
프로젝트 개발을 진행하면서 프론트에서 다루는 데이터의 양보다 서버에서 가져온 데이터를 다루는 작업이 많아졌고 이를 효율적으로 다룰 수 있는 방법이 있지 않을까 생각하다 React Query 라는 라이브러리를 발견하게 되었다.
프로젝트에 사용하기 전에 이게 뭔지는 알아야 우리 프로젝트에 적합할지 아닐지를 판별 할 수 있을 것 같아서 기본 문법이랑 장점 등을 알아보려고 한다.
참고한 자료들은 카카오페이 프론트엔드 개발자들이 React Query를 선택한 이유, react-query 이 두 자료를 주로 사용했다.
👨🏻🏫 React Query 왜 사용하나요?
React Query 알아보기
우선 React Query 는 React Application 에서 서버의 상태를 불러오고, 캐싱하며, 지속적으로 동기화하고 업데이트 하는 작업을 도와주는 라이브러리이며, Hook 을 사용하여 React Component 내부에서 자연스럽게 서버의 데이터를 사용할 수 있는 방법을 제공한다.
말로만 들으니까 잘 이해가 안되는 부분들이 있다. 일단 대략적으로 서버와의 통신 작업을 도와주고 결과로 온 데이터들의 관리를 도와주는 것으로 보이는데 코드와 함께 알아보기로 한다.
그 전에 React Query 를 사용하면 어떠한 장점이 있다고 주로 하는지 살펴보면 아래와 같은 장점들이 있다고 한다.
「if(kakao)2021 - 카카오페이 프론트엔드 개발자들이 React Query를 선택한 이유」 세줄요약
- React Query는 React Application에서 서버 상태를 불러오고, 캐싱하며, 지속적으로 동기화하고 업데이트하는 작업을 도와주는 라이브러리입니다.
- 복잡하고 장황한 코드가 필요한 다른 데이터 불러오기 방식과 달리 React Component 내부에서 간단하고 직관적으로 API를 사용할 수 있습니다.
- 더 나아가 React Query에서 제공하는 캐싱, Window Focus Refetching 등 다양한 기능을 활용하여 API 요청과 관련된 번잡한 작업 없이 "핵심 로직"에 집중할 수 있습니다.
react-query 장점
- 캐싱
- get을 한 데이터에 대해 update를 하면 자동으로 get을 다시 수행한다. (예를 들면 게시판의 글을 가져왔을 때 게시판의 글을 생성하면 게시판 글을 get하는 api를 자동으로 실행 )
- 데이터가 오래 되었다고 판단되면 다시 get (
invalidateQueries
)- 동일 데이터 여러번 요청하면 한번만 요청한다. (옵션에 따라 중복 호출 허용 시간 조절 가능)
- 무한 스크롤 (Infinite Queries (opens new window))
- 비동기 과정을 선언적으로 관리할 수 있다.
- react hook과 사용하는 구조가 비슷하다.
대략 두 글에서 공통적으로 장점이라고 뽑는 부분을 보면 캐싱, API 요청이 쉽고 직관적, React Application 과의 호완이라고 볼 수 있다. 이제 코드를 보면서 위 장점이 어떻게 들어나는지 확인해보자
코드와 함께 알아보기
import axios from 'axios';
import {
QueryClient,
QueryClientProvider,
useMutation,
useQuery,
useQueryClient,
} from 'react-query';
// React Query는 내부적으로 queryClient를 사용하여
// 각종 상태를 저장하고, 부가 기능을 제공합니다.
const queryClient = new QueryClient();
function App() {
return (
<QueryClientProvider client={queryClient}>
<Menus />
</QueryClientProvider>
);
}
function Menus() {
const queryClient = useQueryClient();
// "/menu" API에 Get 요청을 보내 서버의 데이터를 가져옵니다.
const { data } = useQuery('getMenu', () => axios.get('/menu').then(({ data }) => data));
// "/menu" API에 Post 요청을 보내 서버에 데이터를 저장합니다.
const { mutate } = useMutation((suggest) => axios.post('/menu', { suggest }), {
// Post 요청이 성공하면 위 useQuery의 데이터를 초기화합니다.
// 데이터가 초기화되면 useQuery는 서버의 데이터를 다시 불러옵니다.
onSuccess: () => queryClient.invalidateQueries('getMenu'),
});
return (
<div>
<h1> Tomorrow's Lunch Candidates! </h1>
<ul>
{data.map((item) => (
<li key={item.id}> {item.title} </li>
))}
</ul>
<button
onClick={() =>
mutate({
id: Date.now(),
title: 'Toowoomba Pasta',
})
}
>
Suggest Tomorrow's Menu
</button>
</div>
);
}
아주 간단한 메뉴 관련 페이지의 내용이다. 내부 코드를 모두 이해할 필요는 없지만 간단하게 보면 우리가 평소에 사용하던 GET 은 useQuery 를 통해서 처리하고 있고, Post 요청은 useMutation 을 통해서 처리하고 있는 것을 볼 수 있다.
또한 눈에 들어오는 부분은 onSuccess 부분으로 queryClient 의 invalidateQueries 를 통해서 POST 가 이루어진 이후 재 GET 요청을 하는 것을 볼 수 있다. 이 포스트에서 모든 내용을 다룰 수 없더라도 GET, POST 부분을 대신 처리해주는 useQuery, useMutation 부분을 알아보려고 한다.
useQuery
// 1. 가장 기본적인 형태의 React Query useQuery Hook 사용 예시
const { data } = useQuery(
queryKey, // 이 Query 요청에 대한 응답 데이터를 캐시할 때 사용할 Unique Key (required)
fetchFn, // 이 Query 요청을 수행하기 위한 Promise를 Return 하는 함수 (required)
options, // useQuery에서 사용되는 Option 객체 (optional)
);
// 2. option 중의 하나인 enabled 를 사용한 예시
const { data: todoList, error, isFetching } = useQuery('todos', fetchTodoList);
const {
data: nextTodo,
error,
isFetching,
} = useQuery('nextTodos', fetchNextTodoList, {
enabled: !!todoList, // true가 되면 fetchNextTodoList를 실행한다
});
// 3. return 값을 나눠서 받을 수 있다.
const { isLoading, isError, data, error } = useQuery('todos', fetchTodoList, {
refetchOnWindowFocus: false, // react-query는 사용자가 사용하는 윈도우가 다른 곳을 갔다가 다시 화면으로 돌아오면 이 함수를 재실행합니다. 그 재실행 여부 옵션 입니다.
retry: 0, // 실패시 재호출 몇번 할지
onSuccess: (data) => {
// 성공시 호출
console.log(data);
},
onError: (e) => {
// 실패시 호출 (401, 404 같은 error가 아니라 정말 api 호출이 실패한 경우만 호출됩니다.)
// 강제로 에러 발생시키려면 api단에서 throw Error 날립니다. (참조: https://react-query.tanstack.com/guides/query-functions#usage-with-fetch-and-other-clients-that-do-not-throw-by-default)
console.log(e.message);
},
});
우선 useQuery 는 데이터를 GET 위한 api 입니다.
- 첫 번째 파라미터로는 query key (unique key) 가 들어갑니다.
- 넣어준 query key 를 통해서 다른 컴포넌트에서도 해당 키를 사용하면 호출이 가능합니다.
- query key 로는 string 과 배열을 받을 수 있습니다.
- string 으로 사용할 경우 기본적인 key 의 기능을 해주고,
- 배열로 넘기면 0번 인덱스 값은 string 으로 다른 컴포넌트에서 부를 값이 들어가고, 이후 인덱스의 값들은 query 함수 내부에 파라미터로 값이 전달됩니다.
- 두 번째 파라미터로는 요청을 수행하기 위해서 필요한 Promise 를 반환하는 함수, axios, fetch 등등의 함수를 넣어줄 수 있습니다.
- 세 번째 파라미터로는 option 객체를 넣어줄 수 있습니다.
- 간단하게 option 중 하나인 enable 에 대해서 알아보면, option 으로 enable 을 설정할 수 있는데 값이 true 인 경우에 useQuery 실행하게 만들 수 있습니다. 이를 통해서 위 예시는 비동기 함수인 useQuery 를 순차적으로 수행하게 만들었습니다.
- return 값으로는 api 의 성공, 실패 여부, api return 값을 포함한 객체를 반환합니다.
- 이는 한번에 1번 예제의 data 처럼 받을 수 있지만 나눠서 받을 수 있습니다. 3번 예제 코드의 리턴 값을 확인해보면 다음과 같습니다.
- isLoading: 현재 로딩 중인지 여부를 담는 값
- isError: 에러가 발생했는지 여부를 담는 값
- data: 결과로 받은 값을 가지고 있다.
- error: 어떠한 에러인지 Error 객체를 담고 있다.
- 이는 한번에 1번 예제의 data 처럼 받을 수 있지만 나눠서 받을 수 있습니다. 3번 예제 코드의 리턴 값을 확인해보면 다음과 같습니다.
이제 맨 위에서 본 코드 중 useQuery 부분을 이해할 수 있다. 이제 다음 알아볼 useMutation 을 볼 순서인데 그 전에 useQuery 를 여러번 사용할 때 귀찮은 경우를 줄여주는 useQueries 를 짧게 보고 넘어가보려 한다.
useQueries
// 3번의 useQuery 를 사용하는데 이 3개에 대한 로딩, 성공, 실패 처리를 모두
// 다루는 것은 귀찮은 일이다.
const usersQuery = useQuery('users', fetchUsers);
const teamsQuery = useQuery('teams', fetchTeams);
const projectsQuery = useQuery('projects', fetchProjects);
// 위와 같은 문제를 해결하는 방법으로 useQueries 를 통한 방법이 있다.
const result = useQueries([
{
queryKey: ['getRune', riot.version],
queryFn: () => api.getRunInfo(riot.version),
},
{
queryKey: ['getSpell', riot.version],
queryFn: () => api.getSpellInfo(riot.version),
},
]);
useEffect(() => {
console.log(result); // [{rune 정보, data: [], isSucces: true ...}, {spell 정보, data: [], isSucces: true ...}]
const loadingFinishAll = result.some((result) => result.isLoading);
console.log(loadingFinishAll); // loadingFinishAll이 false이면 최종 완료
}, [result]);
위 예제가 useQueries 를 사용하는 이유를 잘 보여주는 것 같다.
여러번의 useQuery 를 동시에 진행하려고 할때 한번에 loading 이나 error 처리를 하기 편하게 만들어주는 장점이 있는 것을 볼 수 있다.
또한 구조도 useQuery 와 비슷하게 queryKey (첫 번째 파라미터), queryFn (Promise 반환 함수) 로 되어 있는 것을 볼 수 있다.
useMutation
// 가장 기본적인 형태의 React Query useMutation Hook 사용 예시
const { mutate } = useMutation(
mutationFn, // 이 Mutation 요청을 수행하기 위한 Promise를 Return 하는 함수 (required)
options, // useMutation에서 사용되는 Option 객체 (optional)
);
이제 POST 역할을 해줄 수 있는 useMutation 을 알아보려 한다.
- useMutaion Hook으로 수행되는 Mutation 요청은 HTTP METHOD POST, PUT, DELETE 요청과 같이 서버에 Side Effect를 발생시켜 서버의 상태를 변경시킬 때 사용한다.
- useMutaion Hook의 첫번째 파라미터는 이 Mutation 요청을 수행하기 위한 Promise를 Return 하는 함수이며, useMutation의 return 값 중 mutate (또는 mutateAsync) 함수를 호출하여 서버에 Side Effect를 발생시킬 수 있다.
예제 코드와 함께 이해를 더 해보려 한다. 우선 처음 올린 예제와 함께 알아본다.
// "/menu" API에 Post 요청을 보내 서버에 데이터를 저장합니다.
const { mutate } = useMutation(
(suggest) => axios.post('/menu', { suggest }),
{
// Post 요청이 성공하면 위 useQuery의 데이터를 초기화합니다.
// 데이터가 초기화되면 useQuery는 서버의 데이터를 다시 불러옵니다.
onSuccess: () => queryClient.invalidateQueries('getMenu'),
},
);
...
<button
onClick={() =>
mutate({
id: Date.now(),
title: 'Toowoomba Pasta',
})
}
>
Suggest Tomorrow's Menu
</button>
우선 첫 번째 인자로 새롭게 입력된 메뉴 값 (suggest) 을 POST 로 처리하는 함수를 넣어준 것을 볼 수 있다. 이를 통해서 api 통신을 진행할 것이다.
이후 옵션을 보면 onSuccess 를 넣어주었는데 위에서 정의해준 POST 통신이 완료되면 어떠한 행동을 할지 정해준 것이다. 여기에서는 getMenu queryKey 의 재 전송을 요청한 것을 볼 수 있다.
또한 return 값으로 전달해준 mutate 를 button 의 onClick 에 연결하여 처리를 하는 것을 볼 수 있다.
대략적으로 어떻게 사용할지에 대한 감이 잡힌다.
위 두 기능 이외에도 React Query 가 지원하는 것이 많을건데 이는 시간이 여유로우면 더 추가할 예정이다. 뭔가 더 안쓸 것 같은 기분이..?!?
🧐 짧은 소감
글을 작성하게 된 이유가 프로젝트에 사용할지를 알아보기 위함이 컸는데 학습을 해보고 나니 POST 나 DELETE 를 처리하고 나서 GET 을 다시 요청하는 부분이나, 간단하게 서버 데이터를 활용할 수 있는 점이 매력적이라 모든 부분은 아니더라도 일부에 적용을 해봐야겠다는 생각이 들었다.
또한 이 외에도 지원하는 기능이 많을 것인데 좀 더 알아보고 싶다는 생각이 들었고 감사하게도 React Query와 함께 Concurrent UI Pattern을 도입하는 방법 이라는 좋은 자료가 있는 것 같아서 다음 글을 쓸 때는 위 내용을 학습하고 정리하는 시간을 가질 것 같다.
🛠 적용한 후기
적용하면서 ‘아 이 라이브러리 편하다!’ 라는 생각을 많이 했던 것 같다. 특히 코드가 많이 줄어들었고 에러처리나 로딩에 관련해서도 손쉽게 처리를 할 수 있어서 정말 좋았다.
적용한 파트중 하나이다. 좌측을 보면 처음에 마운트시 데이터를 로드하기위한 useEffect 부분, 버튼을
눌렀을 때 적용하기 위한 API 함수 부분이 장황하게 적힌 것을 볼 수 있다. React Query 를 적용한 우측을
보면 코드가 상당히 간단해진 것을 볼 수 있다. try-catch 구문을 굳이 사용하지 않아도 내부적으로 에러를
판단해 isError 에 넣어주기도 하고 추후 코드가 변경되면서 isError, error 를 받아오게 했는데 저기에
없네요… isLoading 을 통해서 로딩이 끝나는 시점도 제어할 수 있었다.
적용하면서 몇 번 이슈가 있었고, 그 이슈를 통해 학습을 했는데 하나는 아래 사진을 보면서 이야기하겠다.
eslint 를 사용하면서 단순하게 받아온 데이터를 사용하려고 하니 빨간불이 들어오면서 ‘멈춰!!!’ 를 시전했다. 이유를 알고보니 eslint 가 하는 말은 ‘아니 저거 잘 동작할지도 모르고 로딩일 때는 뭐 보여줄건데? 님 뭐함?’ 이였다. 말을 듣고보니 맞는 말이라 어떻게 처리를 하나 보니 isError 와 isLoading 을 통해서 중간에 리턴할 값을 지정해주는 방법이 있었다.
근데 막상 이렇게 하니 뭔가 이때까지 알고있던 컴포넌트는 이 모양이 아닌데.. 하면서 아쉬움이 남았고 이를 좀 예쁘게? 해줄 방법을 찾다보니 Suspense 를 통한 해결 방법이 있었다.
위 코드는 따로 return 으로 분기한게 아닌 매 조건을 따져가면서 Loading 과 Error 상황을 처리해준 상황을 Suspense 와 ErrorBoundary 를 통해서 처리해준 부분이다. 위 상황에서는 return 값을 위에서 처리해준 것이 아니기 때문에 뭐가 다르지 하는 생각이 들 수 있든데 이전 사진이 있던 코드를 리팩토링하고 차후 추가하겠다.
Suspense라는 React의 신기술을 사용하면 컴포넌트의 랜더링을 어떤 작업이 끝날 때까지 잠시 중단시키고 다른 컴포넌트를 먼저 랜더링할 수 있습니다. 이 작업이 꼭 어떠한 작업이 되어야 한다는 특별한 제약 사항은 없지만 아무래도 REST API나 GraphQL을 호출하여 네트워크를 통해 비동기로(asynchronously) 데이터를 가져오는 작업을 가장 먼저 떠오르게 됩니다. …
> Suspense는 어떤 컴포넌트가 읽어야 하는 데이터가 아직 준비가 되지 않았다고 리액트에게 알려주는 새로운 매커니즘인데요. Suspense를 통해 컴포넌트가 비동기 데이터를 읽어오는 방법을 표준화하고자 리액트 팀의 장기적인 계획도 엿볼 수 있습니다.
Suspense 는 한마디로 말하면 ‘잠시만! 나 뭐 로딩중인데 일단 이 화면 띄워줄래?’ 를 구현해주는 기능이다. 이번 글에 담기에는 너무 단독적으로 양이 많기 때문에 우선 링크를 걸어두겠습니다. React Suspense 소개(feat. React v18)
ErrorBoundary 는 하위 컴포넌트 트리의 어디에서든 자바스크립트 에러를 기록하며 깨진 컴포넌트 트리 대신 폴백 UI를 보여주는 React 컴포넌트입니다. 이 부분은 제가 아직 이해를 깊게 못했기 때문에 React 공식 문서를 넣어두겠습니다. 에러 경계(Error Boundaries)
또 하나의 이슈가 있었는데 이 또한 사진과 함께 알아보도록 한다.
위 사진이 이전, 아래 사진이 이후 코드이다. 크게 달라진 것은 없고 query key 값으로 단순 string
이 아닌 배열로 변경해줬다. 대략 어떠한 상황인지를 이야기해보면 target 아이디 라는 값을 props 를
통해서 받아오고 이를 통해서 useQuery 를 동작시키는 상황이였다. 하지만 이렇게 사용하다보니 A
페이지(임의로 저 컴포넌트가 있는 페이지를 A 라고 하겠다.) 에서 유저 1 에 대한 정보를 보다가 유저 2
에 대한 정보를 보려고 하면 기존에 있던 유저 1 에 대한 내용이 그대로 있었다.
리렌더 문제인지도 보고 저렇게 쓰는게 아닌가도 보고 여러 자료를 찾다가 대략적으로 짐작이 가던게 query key 에 대한 문제가 있는 것이 아닌가 였습니다. 이에 관한 글을 참고하고 한번 아래와 같이 변경해주니 잘 동작했습니다. 참고한 자료는 다음과 같습니다. react-query 캐싱 이슈 해결하기! 변경, 삭제할 때 쿼리 업데이트가 안된다?
무슨 문제인지 간단하게 이야기하면 react-query 의 캐싱 방식이 설정한 key 값이 변경되지 않는 이상 캐싱 데이터를 내려준다는 것이였습니다. 저의 경우 위 코드에서 단순하게 string 고정 값으로 전달해주고 있었고 이로 인해서 문제가 생겼던 것이였습니다.
정확하게 깊게 공부해서 적용한게 아니라 우여곡절이 있을 것이라 생각했지만 이 정도면 나쁘지 않게 잘 적용한게 아닌가 싶기도 하고 추가적인 React-Query 관련 글을 써보고 싶다는 생각이 들었습니다.