📕 0. 글을 작성하는 이유
: 최근에 리액트 쿼리 v5가 릴리즈 되었다. 지난번에 진행한 프로젝트에서 리액트 쿼리 v4를 사용해 본 경험이 있는데, 이번에 시작 예정인 토이 프로젝트에서는 리액트 쿼리 v5 도입을 시도할 예정이라 한 번 정리해보려고 한다.
해당 게시글은 리액트 쿼리 (TanStack Query V5)를 기준으로 작성되었습니다.
V4와 혼동하지 않도록 주의해주세요 !
📕 1. 리액트 쿼리(React-Query)란 무엇인가?
- 리액트 쿼리(React-Query)는 리액트 애플리케이션에서 서버 상태 가져오기, 데이터 캐싱, 동기화 및 업데이트를 보다 쉽게 다루기 위해 사용하는 라이브러리이다. 리액트 쿼리는 주로 서버 상태와 클라이언트 상태를 구분하기 위해 사용된다.
- 여기서 서버 상태와 클라이언트는 명확하게 구분되는 개념인데 각각의 예시를 들면, 클라이언트 상태는 웹페이지에서 입력받는 input값을 예시로 들 수 있고, 서버 상태는 DB에 저장되어 있는 데이터를 예시로 들 수 있다.
- React-Query는 기존 상태 관리 라이브러리인 redux 나 mobX가 클라이언트 상태 작업에 적합하지만, 비동기 또는 서버 상태 작업에서는 그다지 좋지 않다고 언급했다.
📕 2. 그렇다면 리액트 쿼리의 장점은 무엇인가?
1. 서버 상태(Server State)와 클라이언트 상태(Client State)라는 두 가지 상태를 각각 관리할 수 있게 해 준다.
: 리액트 쿼리의 도입 이전에 진행했던 프로젝트에서는 '서버 상태'와 '클라이언트 상태'를 혼용해서 관리했다. axios를 통해 서버와의 통신을 통해 받은 데이터인 '서버 상태'와 사용자가 화면에서 input창에 입력하는 값과 같은 '클라이언트 상태', 두 가지 모두 전역 상태 관리를 위한 '스토어'나 '로컬 스토리지'와 같은 저장소에 담아서 관리했었다. 하지만 리액트 쿼리를 사용하면 '서버 상태'를 '스토어' 같은 전역 State 없이 다룰 수 있다. '클라이언트 상태'는 기존과 같이 전역 State를 통해 관리하면 되므로, '서버 상태'와 '클라이언트 상태'를 분리하여 관리할 수 있게 된다.
2. 캐싱(Caching)을 통해 애플리케이션의 속도를 향상시킨다.
: 리액트 쿼리는 데이터 캐싱 기능을 제공한다. 간단하게 설명하면, 리액트 쿼리를 사용해 서버에 데이터를 요청하려면 '쿼리 키'라는 고윳값을 사용하여 요청하게 된다.
간략하게 예를 들면,
1. 사용자가 'A'라는 쿼리 키를 사용해 서버에 데이터를 요청한다.
2. 서버에서 받아온 데이터를 저장한다.
3. 또다시 사용자가 'A'라는 동일한 쿼리키를 사용해 여러 곳에서 동일한 데이터를 요청했을 때,
사용자가 설정한 시간이 지나지 않았다면, 서버에 데이터를 요청하지 않고
2번에서 저장해 둔 데이터를 사용자에게 보여주게 된다.
이렇게 되면 데이터가 자주 변경되지 않는 경우, 서버로 불필요한 네트워크 요청을 줄일 수 있다는 장점이 있다.
예를 들면, '이번 달 베스트셀러'를 보여주는 페이지의 경우, 실시간으로 데이터가 바뀔 필요가 없다. 많아봤자 하루에 한 번? 데이터가 바뀌면 될 것 같다. 이러한 경우 리액트 쿼리의 '캐싱' 기능을 사용하면 서버로의 불필요한 네트워크 요청을 줄일 수 있다. 네트워크 요청이 줄어들기에 애플리케이션의 속도를 향상할 수 있다.
리액트 쿼리에서는 수동으로 캐시를 무효화하고 refetch도 가능하므로 캐싱 기능을 보다 자유롭게 사용할 수 있다.
3. 동일한 데이터에 대한 중복 요청을 제거한다.
: 2번에서 설명한 내용과 이어지는 내용인데, 리액트 쿼리에서는 쿼리 키를 기반으로 동일한 데이터에 대한 중복된 요청을 방지할 수 있다.
먼저, 리액트 쿼리를 사용하지 않는다고 가정해 보자.
'유저 정보'라는 데이터가 A라는 컴포넌트에서도 필요하고, B라는 컴포넌트에서도 필요한 경우가 있다.
'유저 정보'를 A, B 컴포넌트에서 사용하기 위해서는 3가지 방법이 있을 것 같다.
1 ) props로 데이터 전송
: 이 경우는 A와 B 컴포넌트가 부모-자식 관계여야만 가능하므로 무조건 되는 방법이 아니다.
2 ) Store와 같은 전역 State 사용
: A나 B 컴포넌트에서 데이터를 받은 뒤, 스토어에 저장한다. 이후 필요한 컴포넌트에서 스토어에 있는 데이터를 꺼내 쓰는 방식을 사용할 수 있다. 이 경우도 많이 쓰이는 방법이지만, 서버 상태와 클라이언트 상태를 분리할 수 없다는 단점이 존재한다.
3 ) 각 컴포넌트에서 데이터 요청
: A 컴포넌트에서 서버로 데이터 요청하고, B 컴포넌트에서 서버로 데이터를 요청하면 된다. 가장 직관적인 방법이지만, 데이터가 필요한 컴포넌트마다 서버로 요청해야 하므로, 서버에 무리를 줄 수 있다.
이번엔 리액트 쿼리를 사용한다고 가정해 보자.
리액트 쿼리에서는 위의 3번과 비슷한 방식을 사용한다. 다시 말하면, 데이터가 필요한 컴포넌트마다 데이터를 요청하는 코드를 작성한다. 이때 데이터를 요청할 때, '쿼리 키'를 사용해 요청하게 되는데 중복된 요청이어도 쿼리 키를 확인하여 캐시 된 데이터가 있다면 서버로 요청하는 것이 아닌 캐시된 데이터를 반환한다. 그렇기 때문에 여러 컴포넌트에서 데이터를 요청해도 쿼리 결과는 한 번만 가져와지기 때문에 서버로의 중복 요청이 발생하지 않는다.
// 컴포넌트 A
const { data } = useQuery('쿼리 키', QueryFn);
// 컴포넌트 B
const { data } = useQuery('쿼리 키', QueryFn);
위의 코드를 예로 들면, A 컴포넌트에서 쿼리를 시작하고 B 컴포넌트에서는 서버로 데이터를 요청하는 것이 아닌, 캐시된 데이터를 사용한다는 것이다.
데이터를 공유하기 위해 스토어에 담는 과정도, 일일이 props를 하지 않아도 되므로 논리적으로 편하게 생각할 수 있다는 장점이 있다.
4. 보일러플레이트 형태의 코드가 적고, 가독성이 뛰어나서 이해하기 쉽고 추후 유지보수도 쉽다.
import React, { useEffect, useState } from 'react';
import axios from 'axios';
const MyComponent = () => {
const [data, setData] = useState(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchData = async () => {
try {
const response = await axios.get('https://api.example.com/data');
setData(response.data);
} catch (error) {
setError(error);
} finally {
setIsLoading(false);
}
};
fetchData();
}, []);
if (isLoading) {
return <p>Loading...</p>;
}
return (
<div>
<h1>Data:</h1>
</div>
);
};
export default MyComponent;
리액트 쿼리를 사용하지 않고, useEffect를 사용해 데이터를 불러오면 데이터를 저장할 State도 만들어야 하고, 에러인지 아닌지 확인하기 위해 State를 만들어야 하므로 보일러플레이트 형태의 코드가 증가한다.
import React from 'react';
import axios from 'axios';
import { useQuery } from 'react-query';
const fetchDataFunction = async () => {
const response = await axios.get('https://api.example.com/data');
return response.data;
};
const MyComponent = () => {
const { data, isLoading,isError } = useQuery('myQueryKey', fetchDataFunction);
if (isLoading) {
return <p>Loading...</p>;
}
if (isError) {
return <p>에러가 발생함.</p>;
}
return (
<div>
<h1>Data:</h1>
</div>
);
};
export default MyComponent;
하지만 리액트 쿼리를 사용하면 로딩 중이나 에러와 같은 예외처리도 간편해지고, 데이터를 담을 state를 따로 만들 필요가 없으므로 가독성이 높아진다는 장점을 가지고 있다.
로딩중이나 에러뿐만 아니라, 위에 해당하는 다양한 값을 쉽게 얻을 수 있다는 장점이 있다.
📕 3. 글을 마치며
이번 글에서는 리액트 쿼리의 개념과 장점에 대해 간략하게 알아보았다.
다음 글부터는 본격적인 리액트 쿼리의 사용법에 대해 자세하게 업로드할 예정이다.