회사에서 커스텀 훅 기반으로 React 프로젝트를 다시 구축하게 되면서
React-Query 라이브러리도 사용하게 되었다.
그래서 React-Query에 대해 한번 깊게 파보기 위해 차근차근 포스팅해보려고 한다.
우선, 공식문서 토대로 번역해서 요약한 거라 의역이 조금 많이 되어 있어
혹시라도 추가해야하거나 부족한 부분이 있다면 댓글로 마구마구 피드백 감사드립니다:)
☞ React Query는 데이터 Fetching, 캐싱, 동기화, 서버 쪽 데이터 업데이트 등을 쉽게 만들어 주는 React 라이브러리이다.
✔️ 기존에 Redux, Mobx, Recoil과 같은 다양하고 훌륭한 상태 관리 라이브러리들이 있긴 하지만, 클라이언트 쪽의 데이터들을 관리하기에 적합할 순 있어도 서버 쪽의 데이터들을 관리하기에는 적합하지 않은 점들이 있다.
이 점을 해결하기 위해 React-Query가 만들어졌다.
✔️ 위에서 설명한 것처럼 React 어플리케이션 내에서 데이터 패칭, 캐싱, 동기적, 그리고 서버의 상태의 업데이트를 좀 더 용이하기 위해 만들어준다.
✔️ 기존에는 직접 만들어서 사용했던 기능들을 별도의 옵션으로 지원하여 복잡하고 이해할 수 없는 수많은 코드를 대신 React-Query 로직을 통해 짧은 코드로 대체할 수 있게 되었다.
✔️ 프로젝트 구조가 기존보다 단순해져 애플리케이션을 유지 보수하기 쉽고, 새로운 기능을 쉽게 구축할 수 있다.
그 외에도 많은 장점이 있다.
→ React Query는 별도의 설정 없이 즉시 사용 가능하다
→ 캐싱을 효율적으로 관리해준다
→ 같은 데이터에 대한 여러번의 요청이 있을 시 중복을 제거한다.
→ 백그라운드에서 알아서 오래된 데이터를 업데이트해준다.
→ 데이터 업데이트 시 최대한 빨리 반영한다.
→ 페이징처리, 지연 로딩 데이터와 같은 성능 최적화해준다
→ 서버 쪽 데이터를 가비지 컬렉션을 이용하여 자동으로 메모리를 관리해준다.
→ 구조적 공유를 통해 쿼리의 결과를 기억해준다.
import {
useQuery,
useMutation,
useQueryClient,
QueryClient,
QueryClientProvider,
} from 'react-query'
import { getTodos, postTodo } from '../my-api'
//먼저 client를 만들어준다.
const queryClient = new QueryClient()
function App() {
return (
// 애플리케이션에 클라이언트를 제공한다.
<QueryClientProvider client={queryClient}>
<Todos />
</QueryClientProvider>
)
}
function Todos() {
// client에 접근하기
const queryClient = useQueryClient()
// Queries (데이터 패칭하기 위한 hook)
const query = useQuery('todos', getTodos)
// Mutation
const mutation = useMutation(postTodo, {
onSuccess: () => {
// todos 라는 unique key에 대한 기존 데이터를 무효화하고 다시 가져오기
queryClient.invalidateQuries('todos')
},
})
})
return (
<div>
<ul>
// useQuery로 가져온 데이터는 아래와 같이 query.data로 꺼내올 수 있다.
{query.data.map(todo => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
<button
onClick={() => {
// 데이터 변경하기
mutation.mutate({
id: Date.now(),
title: 'Do Laundry',
})
}}
>
Add Todo
</button>
</div>
)
}
❇️ useQuery, useMutation 등 React-query에서 제공하는 hook에는 정말 다양하고 개발자들을 편리하게 해주는 옵션들이 많다!
이번편에서는 그 중 Queries 관련한 내용에 대해서 자세히 다뤄볼 예정이다.
다음 포스팅에서는 Mutation내용으로 이어나갈 예정이다
✅ 사용법
import { useQuery } from 'react-query'
function App() {
const info = useQuery('todos', fetchTodoList)
}
✅ Query는 GET, POST 메소드 포함을 기반으로 서버에서 데이터를 패칭하기 위한 비동기 함수와 사용된다.
✅ 'todos'
: 첫번째 인자로, unique key를 명시해준다.
✔️ 해당 key는 내부적으로 데이터 재요청, 캐싱, 쿼리를 공유하기 위해 사용된다.
✅ fetchTodoList
: 두번째 인자에는 우리가 요청할 비동기 함수를 넣어주는데, 데이터와 error를 return해준다.
✅ useQuery return값에 포함되어 있는 states
isLoading
or status === 'loading'
: 현재 데이터를 요청 중이나 아직 데이터가 없을 경우isError
or status === 'error'
: 쿼리에서 에러가 났을 경우error
: 해당 property로 에러 메세지를 확인할 수 있다.isSuccess
or status === 'success'
- 쿼리 요청 성공data
: 해당 property로 성공한 데이터를 확인할 수 있다.isIdle
or status === 'idle'
: 이 쿼리는 현재 사용할 수 없을 때 나옴isFetching
: 데이터 요청 중일 때는 (내부적으로 리패칭 중 일때도 포함) 항상 True를 리턴한다 function Todos() {
const { isLoading, isError, data, error } = useQuery('todos', fetchTodoList)
if (isLoading) {
return <span>Loading...</span>
}
if (isError) {
return <span>Error: {error.message}</span>
}
// We can assume by this point that `isSuccess === true`
return (
<ul>
{data.map(todo => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
)
}
function Todos() {
const { status, data, error } = useQuery('todos', fetchTodoList)
if (status === 'loading') {
return <span>Loading...</span>
}
if (status === 'error') {
return <span>Error: {error.message}</span>
}
// also status === 'success', but "else" logic works, too
return (
<ul>
{data.map(todo => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
)
}
✅ React Query는 query key로 캐싱을 관리한다.
✅ 간단한 문자열이어도 되고, 배열 형태, 중첩된 객체와 같이 복잡하든 상관없다
✅ Query key가 순차적 진행을 보장하는 직렬화 기법으로 쿼리의 데이터는 유일하다.
✔️ Query key가 문자열일 때
// A list of todos
useQuery('todos', ...) // queryKey === ['todos']
// Something else, whatever!
useQuery('somethingSpecial', ...) // queryKey === ['somethingSpecial']
✔️ Query key가 배열일 때
// An individual todo
useQuery(['todo', 5], ...)
// queryKey === ['todo', 5]
// An individual todo in a "preview" format
useQuery(['todo', 5, { preview: true }], ...)
// queryKey === ['todo', 5, { preview: true }]
// A list of todos that are "done"
useQuery(['todos', { type: 'done' }], ...)
// queryKey === ['todos', { type: 'done' }]
✔️ Query key에 있는 object들의 순서는 중요하지 않다.
동일한 Query key -> array의 object내 에 있는 값들의 순서는 중요하지 않음 (동일함)
useQuery(['todos', { status, page }], ...)
useQuery(['todos', { page, status }], ...)
useQuery(['todos', { page, status, other: undefined }], ...)
동일 하지 않은 Query key. -> array 값의 순서는 중요하다
useQuery(['todos', status, page], ...)
useQuery(['todos', page, status], ...)
useQuery(['todos', undefined, page, status], ...)
✔️ Query key는 파라미터를 사용해도 된다.
파라미터가 unique한 값이라면 인자로 받아 아래와 같이 사용할 수 있다.
function Todos({ todoId }) {
const result = useQuery(['todos', todoId], () => fetchTodoById(todoId))
}
✅ Query Function은 promise를 return하는 함수
✅ Promise는 data를 return하거나 에러가 나면 에러를 return한다.
아래와 같이 function을 작성할 수 있다.
useQuery(['todos'], fetchAllTodos)
useQuery(['todos', todoId], () => fetchTodoById(todoId))
useQuery(['todos', todoId], async () => {
const data = await fetchTodoById(todoId)
return data
})
useQuery(['todos', todoId], ({ queryKey }) => fetchTodoById(queryKey[1]))
✅ Query Function에서 발생된 에러는 query의 error state에 남는다.
const { error } = useQuery(['todos', todoId], async () => {
if (somethingGoesWrong) {
throw new Error('Oh no!')
}
return data
})
✅ fetch와 같이 기본적으로 Http 요청 실패에 대한 에러를 제공하지 않는 경우 아래와 같이 직접 error를 던져줘야한다.
✔️ axios와 graphql-request과 같은 대부분의 함수가 기본적으로 HTTP 요청이 실패하면 에러를 반환하지만,
fetch와 같이 몇몇 함수는 오직 network error에 대해서만 에러를 내뱉는 경우가 있다.
이 경우에 아래와 같은 방법으로 에러를 직접 반환해줘야한다.
useQuery(['todos', todoId], async () => {
const response = await fetch('/todos/' + todoId)
if (!response.ok) {
throw new Error('Network response was not ok')
}
return response.json()
})
A [fetch()](https://developer.mozilla.org/en-US/docs/Web/API/fetch)
promise only rejects when a network error is encountered (which is usually when there's a permissions issue or similar). A [fetch()](https://developer.mozilla.org/en-US/docs/Web/API/fetch)
promise does not reject on HTTP errors (404
, etc.).
✅ Query Function Variables
Query keys는 데이터를 요청하는동안 식별한는 것 뿐만아니라 또한 필요할 때 편리하게 Query Function에서 사용도 할 수 있다.
이렇게 하면, 필요할 때마다 아래와 같이 Query Function 에서 추출해서 사용할 수 있다.
function Todos({ status, page }) {
const result = useQuery(['todos', { status, page }], fetchTodoList)
}
// Access the key, status and page variables in your query function!
function fetchTodoList({ queryKey }) {
const [_key, { status, page }] = queryKey
return new Promise()
}
✅ 여러개의 인자가 아니라 객체 하나로도 사용할 수 있다.
import { useQuery } from 'react-query'
useQuery({
queryKey: ['todo', 7],
queryFn: fetchTodo,
...config,
})
useQueries
**✅ 여러개의 useQuery를 나란히 사용할 수 있다.
→ 아래와 같이 사용할 시 다른 쿼리가 실행되기 전에 첫번째 쿼리가 중단하고 에러를 반환할 수 있다.
function App () {
// The following queries will execute in parallel
const usersQuery = useQuery('users', fetchUsers)
const teamsQuery = useQuery('teams', fetchTeams)
const projectsQuery = useQuery('projects', fetchProjects)
...
}
✔️ 그럴땐 , **useQueries
hook을 사용할 수 있다.
useQueries는 Query Option 객체를 배열로 받을 수 있고, 마찬가지로 결과값 또한 배열로 return 해준다.
function App({ users }) {
const userQueries = useQueries(
users.map(user => {
return {
queryKey: ['user', user.id],
queryFn: () => fetchUserById(user.id),
}
})
)
}
✅ enabled: true이면 특정 조건이 trued 때만 실행하겠다
→ 이전 함수가 끝나고 다음 것이 영향을 받을 때 enabled option을 사용하면 좋다.
const { data: user } = useQuery(['user', email], getUserByEmail)
const userId = user?.id
// Then get the user's projects
const { isIdle, data: projects } = useQuery(
['projects', userId],
getProjectsByUser,
{
// The query will not execute until the userId exists
enabled: !!userId,
}
)
✅ useIsFetching
→ useQuery 내부 isLoading state 말고 backgroud에서의 fetching 체크가능하다
import { useIsFetching } from 'react-query'
function GlobalLoadingIndicator() {
const isFetching = useIsFetching()
return isFetching ? (
<div>Queries are fetching in the background...</div>
) : null
}
--
정리가 깔끔해서 이해하기 좋았습니다
좋은글 감사합니다!