
✳︎ 주의
tanstac-query를 처음 사용하는분들이라면, 해당 글은 v5 기준으로 작성되었다는 것을 알아주셨으면 좋겠습니다!
리엑트 쿼리는 웹 어플리케이샨을 위한 데이터 가져오기 라이브러리로 설명되지만, 보다 기술적인 측면에서 보면 웹 어플리케이션에서 서버 상태를 쉽게 가져오기, 캐싱, 동기화 및 업데이트 할 수 있게 해준다.
대부분의 핵심 웹 프레임워크는 전체 데이터를 가져오거나 업데이트하는 방식에 대해 의견이 일치하지 않는다. 이 때문에 개발자는 데이터 가져오기에 대한 엄격한 의견을 캡슐화하는 메타 프레임워크를 구축하거나 데이터를 가져오기 위한 재체적인 방법을 개발하게 된다. 이는 일반적으로 컴포넌트 기반 상태와 side-effect들이 조합된 상태이다. (Redux를 예시로 들수 있다.)
서버 상태란??
서버 상태라는 존재를 알게 되면, 고려해야 할 것들이 많다는 것을 알 수 있다.
이 목록들이 어렵지 않게 다가온다면 이미 모든 서버 상태 문제를 해결했다는 뜻이다. 하지만 대다수의 사람들과 마찬가지로 아직 이러한 문제를 전보 또는 대부분 해결하지 못했거나 이제 경우 표면만 이해하고 있을 뿐이다.
React-Query는 서버 상태 관리를 위한 최고의 라이브러리 중 하나이다. config 필요없이 바로 사용할 수 있으며, 애플리케이션이 성장함에 따라 원하는대로 커스터마이징할 수 있다.
쿼리는 고유 키에 연결된 비동기/ 데이터 소스에 대한 선언적 종속성이다. 쿼리는 서버에서 데이터를 가져오기 위해 모든 프로미스 기반 메서드와 함께 사용 가능하다. 메서드가 서버에 수정을 가하는 경우는, mutation을 활용하는 것이 훨씬 좋다.
컴포넌트 혹은 custom hook에서 쿼리를 구독하려면, useQuery 훅을 사용한다.
useQuery에는
제공한 unique key는 애플리케이션 전체에서 쿼리를 다시 가져오고, 캐싱하고, 공유하는데 내부적으로 사용된다. useQuery가 반환하는 쿼리 결과에는 템플릿 작성 및 기타 데이터 사용에 대한 모든 정보가 포함되어 있다.
const result = useQuery({queryKey: ['todos'], queryFn: fetchTodoList})
결과 객체에는 3가지 상태값이 올 수 있다.
이러한 기본 상태 외에도 쿼리 상테에 따라 더 많은 정보가 올 수 있다.
대부분의 쿼리에서는 일반적으로 isPending 상태를 확인한 다음, isError 상태를 확인한 다음 마지막으로 데이터를 사용할 수 있다고 가정하고 성공 상태를 렌더링하는 것으로 충분하다. -> (req, res, next) 받은 express.js 처럼
function Todos() {
const { isPending, isError, data, error } = useQuery({
queryKey=["todos"],
queryfn= fetchTodoList,
})
//isError보다 isPending 상태를 먼저 확인하기
if (isPending) {
return <div>Loading</div>
}
if (isError) {
return <span> Error: {error.message}</span>
}
return (
<ul>
{data.map((todo) => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
)
}
상태 필드 외에도 다른 옵션이 포함된 추가 fetchStatus 속성이 제공된다.
background refresh 및 stale-while-revalidate 로직은 상태와 fetchStatus에 대한 모든 조합을 가능하게 한다. 예를 들어, 성공 상태의 쿼리는 일반적으로 idle fetchStatus에 있지만, 백그라운드 리프레쉬가 진행중이면 fetching 상태일 수도 있다.
mount 되고 데이터가 없는 쿼리는 일반적으로 status: pending에 있고, fetchStatus: fetching에 있을것이다. 하지만 네트워크 연결이 없는 경우 paused 상태일 수도 있다.
고로, status는 데이터에 대한 정보. fetchStatus는 Queryfn에 대한 정보를 제공한다.
React-Query는 쿼리 키를 기반으로 쿼리 캐싱을 관리하는 것이 핵심이다.
==쿼리 키는 최상위 수준에서 배열이어야 하고==, 단일 문자열이 포함된 배열처럼 단순할 수도, 여러 문자열과 중첩된 객체가 포함된 배열처럼 복잡할 수도 있다. 쿼리 키가 직렬화 가능하고 쿼리 데이터에 고유한 것이면 사용할 수 있다.
객체의 키 순서에 관계없이 다음 쿼리는 모두 동일한 것으로 간주된다.
useQuery({queryKey: ['todos', status, page],...})
useQuery({queryKey: ['todos', page, status],...})
useQuery({queryKey: ['todos', undefined, status, page],...})
위의 키들이 동일한 것으로 간주되는 이유는 hash 구조로 저장되기 때문이다.
쿼리 키는 가져오는 데이터를 고유하게 설명하므로 쿼리 함수에서 변경되는 모든 변수를 쿼리 키에 포함해야 한다.
function Todos({todoId}) {
const result = useQuery({
queryKey: ['todos', todoId],
queryFn: () =>
fetchTodoById(todoId);
})
}
쿼리 키는 쿼리 함수에 대한 종속성 역할을 한다.
쿼리 키에 종속 변수를 추가하면, 쿼리가 독립적으로 캐시되고, 변수가 변경될 때마다 쿼리가 자동으로 다시 가져오도록 할 수 있다.
쿼리 함수는 promise를 반환하는 모든 함수가 될 수 있다. 반환되는 프로미스는 데이터를 해결하거나 오류를 발생시켜야 한다.
React-query에서 쿼리에 오류가 발생했다고 판단하려면, 쿼리 함수가 거부된 promise를 던지거나 반환해야 한다. 쿼리 함수에서 발생하는 모든 오류는 쿼리의 ‘error’ 상태에 유지된다.
axios나 graphql-request 등은 자동으로 성공적이지 못한 HTTP 요청에 에러를 던지지만, fetch와 같은 일부 기능들은 오류를 발생시키지 않는다. 이 경우 사용자가 직접 오류를 발생 시켜야 한다.
예시)
useQuery({
queryKey: ['todos', todoId],
queryFn: async () => { const response = await fetch('/todos/' + todoId)
if (!response.ok) { throw new Error('Network response was not ok') }
return response.json()
},
})
리엑트 쿼리가 필요한 이유와, 데이터 fetching 하는 법에 대해 알아봤다. 다음 글에는 CRUD 중 R을 제외한, Create, Updat, Delete에 필요한 mutate에 대해 작성해볼 예정이다.