Query - React Query(1)

bloom74·2024년 5월 30일

✳︎ 주의
tanstac-query를 처음  사용하는분들이라면, 해당 글은 v5 기준으로 작성되었다는 것을 알아주셨으면 좋겠습니다!

리엑트 쿼리는 웹 어플리케이샨을 위한 데이터 가져오기 라이브러리로 설명되지만, 보다 기술적인 측면에서 보면 웹 어플리케이션에서 서버 상태를 쉽게 가져오기, 캐싱, 동기화 및 업데이트 할 수 있게 해준다.

대부분의 핵심 웹 프레임워크는 전체 데이터를 가져오거나 업데이트하는 방식에 대해 의견이 일치하지 않는다. 이 때문에 개발자는 데이터 가져오기에 대한 엄격한 의견을 캡슐화하는 메타 프레임워크를 구축하거나 데이터를 가져오기 위한 재체적인 방법을 개발하게 된다. 이는 일반적으로 컴포넌트 기반 상태와 side-effect들이 조합된 상태이다. (Redux를 예시로 들수 있다.)

서버 상태란??

  • 사용자가 제어하거나 소유하지 않는 위치에서 원격으로 유지된다.
  • 가져오기 및 업데이트에 비동기 API가 필요하다
  • 공유 소유권을 의미하며 사용자 모르게 다른 사람이 변경할 수 있다.
  • 주의하지 않으면 애플리케이션에서 잠재적으로 오래된 상태가 될 수 있다.

서버 상태라는 존재를 알게 되면, 고려해야 할 것들이 많다는 것을 알 수 있다.

  • 캐싱
  • 동일한 데이터에 대한 여러 요청을 단일 요청으로 중복 제거하기
  • 백그라운드에서 오래된 데이터 업데이트하기
  • 데이터가 오래된 시기 파악하기 -> stale, cache
  • 가능한 한 빨리 데이터에 업데이트 반영하기
  • 페이지 매김 및 지연 로딩 데이터와 같은 성능 최적화
  • 서버 상태의 메모리 및 가비지 컬렉션 관리
  • 구조적 공유를 통한 쿼리 결과 메모화

이 목록들이 어렵지 않게 다가온다면 이미 모든 서버 상태 문제를 해결했다는 뜻이다. 하지만 대다수의 사람들과 마찬가지로 아직 이러한 문제를 전보 또는 대부분 해결하지 못했거나 이제 경우 표면만 이해하고 있을 뿐이다.

React-Query는 서버 상태 관리를 위한 최고의 라이브러리 중 하나이다. config 필요없이 바로 사용할 수 있으며, 애플리케이션이 성장함에 따라 원하는대로 커스터마이징할 수 있다.

Queries

쿼리는 고유 키에 연결된 비동기/ 데이터 소스에 대한 선언적 종속성이다. 쿼리는 서버에서 데이터를 가져오기 위해 모든 프로미스 기반 메서드와 함께 사용 가능하다. 메서드가 서버에 수정을 가하는 경우는, mutation을 활용하는 것이 훨씬 좋다.

컴포넌트 혹은 custom hook에서 쿼리를 구독하려면, useQuery 훅을 사용한다.
useQuery에는

  • unique key for the query
  • a function that returns a promise that: resolved the data, or throws and error

제공한 unique key는 애플리케이션 전체에서 쿼리를 다시 가져오고, 캐싱하고, 공유하는데 내부적으로 사용된다. useQuery가 반환하는 쿼리 결과에는 템플릿 작성 및 기타 데이터 사용에 대한 모든 정보가 포함되어 있다.

const result = useQuery({queryKey: ['todos'], queryFn: fetchTodoList})

결과 객체에는 3가지 상태값이 올 수 있다.

  • pending -> 데이터가 아직 없음
  • isError -> 에러가 발생
  • isSuccess -> 성공

이러한 기본 상태 외에도 쿼리 상테에 따라 더 많은 정보가 올 수 있다.

  • error -> 상태가 isError 상태인 경우 오류 속성을 통해 오류를 사용할 수 있다.
  • data -> 쿼리가 isSuccess 상태인 경우 데이터 속성을 통해 데이터를 사용할 수 있다.
  • isFetching -> 어떤 상태에서든 쿼리가 언제든지 불러오는 중이면 isFetching이 true가 된다.

대부분의 쿼리에서는 일반적으로 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

상태 필드 외에도 다른 옵션이 포함된 추가 fetchStatus 속성이 제공된다.

  • fetchStatus === "fetching" -> 쿼리를 가져오는 중이다.
  • fetchStatus === "paused" -> 쿼리를 가져오려고 하지만 일시 중지되었다.
  • fetchStatus === "idle" -> 쿼리가 현재 아무 작업도 수행하지 않고 있다.

왜 두개의 다른 상태가 있을까?

background refresh 및 stale-while-revalidate 로직은 상태와 fetchStatus에 대한 모든 조합을 가능하게 한다. 예를 들어, 성공 상태의 쿼리는 일반적으로 idle fetchStatus에 있지만, 백그라운드 리프레쉬가 진행중이면 fetching 상태일 수도 있다.
mount 되고 데이터가 없는 쿼리는 일반적으로 status: pending에 있고, fetchStatus: fetching에 있을것이다. 하지만 네트워크 연결이 없는 경우 paused 상태일 수도 있다.

고로, status는 데이터에 대한 정보. fetchStatus는 Queryfn에 대한 정보를 제공한다.

Query Keys

React-Query는 쿼리 키를 기반으로 쿼리 캐싱을 관리하는 것이 핵심이다.
==쿼리 키는 최상위 수준에서 배열이어야 하고==, 단일 문자열이 포함된 배열처럼 단순할 수도, 여러 문자열과 중첩된 객체가 포함된 배열처럼 복잡할 수도 있다. 쿼리 키가 직렬화 가능하고 쿼리 데이터에 고유한 것이면 사용할 수 있다.

Query Keys 결정론적으로 hash 되었다

객체의 키 순서에 관계없이 다음 쿼리는 모두 동일한 것으로 간주된다.

useQuery({queryKey: ['todos', status, page],...})
useQuery({queryKey: ['todos', page, status],...})
useQuery({queryKey: ['todos', undefined, status, page],...})

위의 키들이 동일한 것으로 간주되는 이유는 hash 구조로 저장되기 때문이다.

queryFn이 변수에 의존한다면, 변수를 queryKey에 포함 시키자!

쿼리 키는 가져오는 데이터를 고유하게 설명하므로 쿼리 함수에서 변경되는 모든 변수를 쿼리 키에 포함해야 한다.

function Todos({todoId}) {
	const result = useQuery({
		queryKey: ['todos', todoId], 
		queryFn: () => 
			fetchTodoById(todoId);
	})
}

쿼리 키는 쿼리 함수에 대한 종속성 역할을 한다.
쿼리 키에 종속 변수를 추가하면, 쿼리가 독립적으로 캐시되고, 변수가 변경될 때마다 쿼리가 자동으로 다시 가져오도록 할 수 있다.

Query Function

쿼리 함수는 promise를 반환하는 모든 함수가 될 수 있다. 반환되는 프로미스는 데이터를 해결하거나 오류를 발생시켜야 한다.

에러 핸들링

React-query에서 쿼리에 오류가 발생했다고 판단하려면, 쿼리 함수가 거부된 promise를 던지거나 반환해야 한다. 쿼리 함수에서 발생하는 모든 오류는 쿼리의 ‘error’ 상태에 유지된다.

fetch나 자동적으로 에러를 던지지 않는 경우 방법을 사용시

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에 대해 작성해볼 예정이다.

0개의 댓글