새로운 기술을 도입하기 전에는, 이 기술이 어떤 문제를 해결하려고 하는 지에 대해 먼저 파악하는 것이 중요하다는 멘토님의 말씀을 따라 먼저 React Query를 사용하지 않은 기존의 Data Fetching 방식으로 연습을 진행했다.
/* db.json */
{
"superheroes": [
{
"id": 1,
"name": "Batman",
"alterEgo": "Bruce Wayne"
},
{
"id": 2,
"name": "Superman",
"alterEgo": "Clark Kent"
},
{
"id": 3,
"name": "Wonder Woman",
"alterEgo": "Princess Diana"
}
]
}
해당 JSON 데이터는 json-server 라이브러리를 통해 서버화한 파일이다. 로컬에서 임의로 만든 파일이지만, 서버로 내보내어 비동기 요청을 보낼 수 있게 하였다. 해당 작업은, package.json의 scripts 란에
"serve-json": "json-server --watch db.json --port 4000"
를 추가하여 명령어로 4000번 포트로 돌리면 해당 서버가 돌아갈 수 있도록 하였다.
import axios from 'axios';
/*
단순히 서버에서 데이터를 요청하고 저장하는 것임에도
두가지 훅을 받아와 사용해야 한다.
*/
import { useEffect, useState } from 'react';
export const SuperHeroesPage = () => {
const [isLoading, setIsLoading] = useState(true);
const [data, setData] = useState([]);
useEffect(() => {
const fetchSuperHeroes = async () => {
try {
const response = await axios.get('http://localhost:4000/superheroes');
const data = await response.data;
setData(data);
setIsLoading(false);
} catch (error) {
setError(error.message);
setIsLoading(false);
}
};
fetchSuperHeroes();
}, []);
/* isLoading 중이라면, 서버 데이터를 사용하는 로직 대신, Loading... 메시지를 보여준다. */
if (isLoading) {
return <h2>Loading...</h2>;
}
/* 에러가 발생하면, 상태값에 저장한 에러메시지를 보여준다. */
if (error) {
return <h2>{error}</h2>
}
return (
<div>
<h2>Super Heroes Page</h2>
{data.map((hero) => (
<div key={hero.name}>{hero.name}</div>
))}
</div>
);
};
재래적인 방식의 data fetching 로직이다.
단점은 아래와 같다.
아래는 이번에 학습한 React Query 훅을 사용해 Data Fetching 작업을 해보았다.
import axios from 'axios';
import { useQuery } from 'react-query';
export const RQSuperHeroesPage = () => {
/*
*/
const { data, isLoading, isError, error } = useQuery('super-heroes', () => {
return axios.get('http://localhost:4000/superheroes');
});
if (isLoading) {
return <h2>Loading...</h2>;
}
/* 에러가 발생하면 에러메시지를 보여주지만,
React Query 라이브러리가 자동으로 다시 fetching을 시도하기 때문에 로딩 상태가 더 길게 지속된다.
*/
if (isError) {
return <h2>{error.message}</h2>;
}
return (
<div>
<h2>RQ Super Heroes Page</h2>
{data?.data.map((hero) => (
<div key={hero.name}>{hero.name}</div>
))}
</div>
);
};
더 놀라운 점은, 기존의 useEffect 훅을 사용한 비동기 호출과 비교해서, React Query가 query cache 기능을 추가로 제공하기 때문에, 같은 데이터를 여러번 호출할 때에도 이미 캐싱된 데이터를 보여준다.
기본적으로 React Query로 수행한 모든 query fetching의 결과는, 5분간 cache를 지속하기 때문에 이러한 결과가 나오는 것이다.
(5분 이후에는 query cache는 가비지 컬렉팅 된다. 따라서 다시 loading state는 true로 바뀐다.)
따라서 로딩 페이지가 한번 API콜을 수행한 이후에는 보여지지 않아 성능의 향상을 보여준다.
해당 작업이 이루어지는 과정은 다음과 같다.
useQuery문이 인자로 넣어준 super-heroes key를 통해 최초로 실행되면 isLoading state가 true로 설정되며, 요청이 데이터를 받아오기 위해 보내진다.
요청이 완료되면, key로 설정한 "super-heroes"와 fetchSuperHeroes 함수를 unique identifier로 활용하여 cache 된다.
이후에 다시 같은 query call을 하면, react query는 cache 상에 해당 uniquer identifier가 존재하는 지 확인하고
존재한다면, isLoading state를 true로 설정하지 않고 해당 cache 된 데이터를 보여준다.
그러나, cache 된 데이터가 최신 데이터가 아닐 가능성도 있으므로, 같은 query 요청에 대해 React Query는 background refetch가 trigger되면서 새로 갱신된 데이터가 UI 화면 상에 업데이트도 해준다..!
cache 된 데이터가 재차 요청한 데이터와 동일하다면 UI 화면 상의 데이터는 그대로 유지된다.
여기서, background refetching이 실행될 때 사용되는 flag state는 isFetching이다.
이와 같은 작업이 수행되면서, DB 상의 데이터가 변경되면 기존 client단에서는 isLoading state는 false로 유지하되 isFetching이 true로 바뀌어 cache된 데이터를 UI 화면 상에 보여주다가, isFetching 값이 false가 되면 새로 업데이트된 데이터를 Loading 화면없이 업데이트 하여 보여주는 것이 가능하게 되는 것이다.
결과적으로 페이지 유저는 최초의 페이지 로딩을 제외하면, query call이 성공적으로 마무리 될 경우, 다시 로딩 페이지를 볼 일이 없어지는 것이다.
/*
인라인으로 작성했던 비동기 요청 함수를 다음과 같이 빼줄 수도 있다.
더 직관적인 코딩이 가능하다.
*/
const fetchSuperHeroes = () => {
return axios.get('http://localhost:4000/superheroes');
};
export const RQSuperHeroesPage = () => {
const { data, isLoading, isError, error, isFetching } = useQuery(
'super-heroes',
fetchSuperHeroes
);
다음 포스트에서는 React Query에서 제공하는 강력한 툴인 React Query DevTools 에 대해 다루어 보겠다.