🧐 Tanstack Query란?
http 요청을 전송하고 프론트엔드 UI를 백엔드 데이터와 동기화된 상태로 유지하는데 이용하는 라이브러리
Tanstack Query가 반드시 필요한건 아니고, useEffect와 fetch로 충분히 구현할 수 있다.
하지만 Tanstack Query를 사용하면 훨씬 깔끔하고 수월하게 개발할 수 있다.
또한, 캐싱처리나 refetch트리거 등 고급 기능을 이용할 수 있다.
🔧 설치, 사용법, 이점
설치
npm install @tanstack/react-query
사용법
- tanstack query는 HTTP요청을 전송하는 로직이 내장되어 있지 않고, 요청을 관리하는 로직을 제공: 요청 데이터와 발생 가능한 오류를 추적하는 역할 등
- 모든 get요청에서 query key를 이용해 요청으로 생성된 데이터를 캐시처리한다.-> 나중에 동일한 요청을 할 경우, 이전 요청의 응답을 재사용할 수 있다.
const { data, isPending, isError, error, refetch } = useQuery({
queryKey: ['events'],
queryFn: fetchEvents,
});
export async function fetchEvents() {
const response = await fetch('~~');
if(!response.ok) {
const error = new Error('An error occurred while fetching the events');
error.code = response.status;
error.info = await response.json();
throw error;
}
const { events } = await response.json();
return events;
}
전달 프로퍼티
- queryKey: 배열
- 값은 배열이고, 이 배열을 리액트 쿼리는 내부적으로 저장한다.
- 유사한 값으로 이루어진 배열을 사용할 때마다 배열을 확인하고 기존 데이터를 재사용한다.
- 배열 내 데이터 유형은 문자열로 제한되지 않는다.
- queryFn: 쿼리함수로 실제 요청을 전송할 때 실행할 코드 정의로 프로미스를 반환하는 함수
반환 객체
- data, isPending, isError, error, refetch
프로바이더 래핑
const queryClient = new QueryCient();
function App(){
return (
<QueryClientProvider client={queryClient}>
<RouterProvider router={router} />
</QueryClientProvider>
);
}
이점
위 처럼 기본적인 구성으로만 사용해도, 페이지를 나갔다가 들어오면 refetch가 되서 최신 항상 최신 데이터를 보여줄 수 있다.
💰 쿼리 동작 이해 및 구성 -캐시 및 만료된 데이터
캐시 처리
- useEffet로 데이터를 가져올 때는 이미 응답된 데이터를 페이지 이동 등으로 다시 요청할 경우에 전체 데이터를 새로 받아왔지만, react query를 사용하면 캐시처리를 통해 데이터를 즉각적으로 가져온다.
- react query는 요청을 통해 얻은 응답 데이터를 캐시 처리하고 나중에 동일한 쿼리키를 가진 다른 useQuery가 실행되면 이 데이터를 재사용한다.
- 사용자가 페이지에 재진입하여 컴포넌트 함수가 다시 실행되면 리액트 쿼리는 이 쿼리 키가 이전에 이미 사용되었고, 이 키의 데이터를 캐시 처리한 것을 확인하여 데이터를 즉시 제공할 수 있다.
- 이와 동시에 내부적으로 해당 요청을 다시 전송하여 업데이트된 데이터가 있는지 확인하여 업데이트된 데이터로 이 데이터를 자체적으로 교체한다.
캐시 처리 실행 여부 동작 제어
staleTime
staleTime
: 캐시에 데이터가 있을 때 업데이트된 데이터를 가져오기 위한 요청을 자체적으로 전송하기 전에 기다릴 시간 설정- 0(default)일 경우 항상전송
- 5000일 경우 5000ms동안 기다린 후 추가요청: 5초가 지나기 전에 재요청하면 자체적 전송을 하지 않고 캐시 데이터를 사용하지만, 5초가 지난 후 재요청하면 자체적 전송
gcTime
- Garbage Collection Time(가비지 수집 시간)
- 데이터와 캐시를 얼마나 오랫동안 보관할지 제어
- 5분(default)
- 이 시간이 지나면 캐시가 날라가므로, 컴포넌트를 재렌더링 했을 때 데이터를 가져오기 위한 새로운 요청을 전송
const { data, isPending, isError, error } = useQuery({
queryKey: ['events'],
queryFn: fetchEvents,
staleTime: 5000,
// gcTime: 30000,
});
데이터를 보관하는 기간과 새 요청을 전송하는 시기를 제어할 수 있다!
🗝️ 동적 쿼리 함수 및 쿼리 키
쿼리함수가 파라미터를 받아 동적으로 구성될 경우,
쿼리 함수
export async function fetchEvents(searchTerm) {
let url = 'http://localhost:3000/events';
if(searchTerm) {
url += '?search=' + seatchTerm
}
const response = await fetch(url);
if(!response.ok) {
const error = new Error('An error occurred while fetching the events');
error.code = response.status;
error.info = await response.json();
throw error;
}
const { events } = await response.json();
return events;
}
useQuery
- 이 쿼리는 모든 이벤트를 가져오는게 아니라 검색된 결과만 가져오는 쿼리이므로, 전체를 반환하는 쿼리키와는 다른 쿼리키를 가져야 한다.
- 쿼리 키를 동적으로 구성함으로써 리액트 쿼리는 동일한 쿼리를 기반으로 서로 다른 키에 대해 서로 다른 데이터를 캐시하고 재사용할 수 있다.
- 아래 쿼리는 사용자가 입력한 값에 따라 결과가 달라지므로 동적인 쿼리키를 가져야한다.
❗️ 하지만 ref값을 fetchEvent와 key에 모두 이용하는 것은 바람직하지 않다. state와 달리 ref는 이 컴포넌트가 다시 실행되도록 할 수 없기 때문.const { data, isPending, isError, error } = useQuery({ queryKey: ['events', { search: searchElement.current.value }], queryFn: () => fetchEvents(searchElement.current.value), });
👇
const searchElement = useRef();
const [searchTerm, setSearchTerm] = useState('');
const { data, isPending, isError, error } = useQuery({
queryKey: ['events', { search: searchTerm }],
queryFn: () => fetchEvents(searchTerm),
});
function handleSubmit(event) {
event.preventDefault();
setSearchTerm(searchElement.current.value);
}
let content;
if (isPending) {
content = <LoadingIndicator />
}
if (isError) {
content = <ErrorBlock message={error.info?.message || '오류가 발생했습니다.' }/>
}
if (data) {
content = <ul>
{ data.map(event =>
<li key={event.id}>
<EventItem event={event} />
</li>
)}
</ul>
}
...
return (
...
<form onSubmit={handleSubmit}>
<input type="search" ref={searchElement} />
<button>Search</button>
</form>
{ content }
)
❌ 쿼리 구성 객체 및 요청 취소
- react query는 쿼리함수에 기본 데이터를 전달한다.
- 콘솔로 찍어보면, 코드상에서 어디에서도 전달한 적 없는 객체가 찍혀져 나온다.
- react query가 쿼리함수에 기본적으로 전달하는 데이터는 쿼리키와 신호(signal)에 대한 정보를 제공하는 객체이다.
- signal은 요청을 취소할 때 필요하다. (ex. 요청이 완료되기 전에 사용자가 페이지를 이탈하는 경우)
- 리액트는 자동으로 요청을 취소할 수 있는데 그 용도로 signal을 사용한다.
👉🏽 이러한 signal을 제공하고 데이터를 가져오는 함수에 필요한 쿼리키를 제공하기 위해 리액트 쿼리는 쿼리 함수에 이 객체를 전달한다.
그래서 파라미터 유무에따라 동적으로 url을 구성하고 싶을 경우, 아래와 같이 쿼리 함수를 작성하면 searchTerm
으로 기본 데이터가 들어가게 되므로 원하는대로 동작하지 않게된다.
export async function fetchEvents(searchTerm) {
let url = 'http://localhost:3000/events';
if(searchTerm) {
url += '?search=' + seatchTerm
}
const response = await fetch(url);
if(!response.ok) {
const error = new Error('An error occurred while fetching the events');
error.code = response.status;
error.info = await response.json();
throw error;
}
const { events } = await response.json();
return events;
}
수정
searchTerm 쓰는 곳
const { data, isPending, isError, error } = useQuery({
queryKey: ['events', { search: searchTerm }],
queryFn: ({signal}) => fetchEvents({signal, searchTerm}),
// 프로퍼티명을 쿼리 함수와 맞춰준다.
});
searchTerm 안쓰는 곳 (전체 데이터 받아올 때)
export async function fetchEvents({ signal, searchTerm })
// 요청이 취소된 것을 파악하기 위해 signal 받기
{
let url = 'http://localhost:3000/events';
if(searchTerm) {
url += '?search=' + seatchTerm
}
// fetch함수로 signal전달하여 취소할 수 있게 하기
// 👉🏽 브라우저 내부에서 signal을 받아 요청을 취소할 수 있다.
const response = await fetch(url, {signal: signal});
if(!response.ok) {
const error = new Error('An error occurred while fetching the events');
error.code = response.status;
error.info = await response.json();
throw error;
}
const { events } = await response.json();
return events;
}
쿼리 함수
🎛️ 쿼리 활성화 및 비활성화
검색어를 입력하기 전에는 결과 가져오지 않게 하기 (특정 조건에서 비활성화 처리): enabled: false
const [searchTerm, setSearchTerm] = useState();
const { data, isLoading, isError, error } = useQuery({
queryKey: ['events', { search: searchTerm }],
queryFn: ({signal}) => fetchEvents({signal, searchTerm}),
enabled: searchTerm !== undefined
});
- isLoading vs isPending
- isPending은 쿼리가 시도가 완료되지 않은 경우 (데이터 요청을 하지 않아도 enabled가 false이면 true가 된다.)
- isLoading은 데이터를 로드하고 있을 때 true가 된다. (=== isFetching && isPending)