‘React Query’는 React에서 서버 상태를 효율적으로 관리하기 위한 라이브러리입니다. 서버 데이터의 패칭, 캐싱, 동기화, 업데이트를 쉽게 처리할 수 있게 해주며, 복잡한 상태 관리 로직을 간소화합니다.
React Query는 주로 서버 상태를 관리할 때 사용합니다. 서버 상태란 API를 통해 가져오는 데이터로, 애플리케이션 외부에 존재하며 지속적인 동기화가 필요한 데이터를 말합니다.
서버 데이터와 로컬 상태를 명확히 구분하여 관리할 수 있어 상태 관리의 복잡성을 줄일 수 있습니다. 여러 컴포넌트에서 동일한 데이터를 사용할 때도 React Query가 자동으로 데이터를 동기화하여 일관성을 유지합니다.
리액트 쿼리는 세가지 핵심 개념을 가지고 있는데 Query 와 Mutation
, QueryClient와 QueryClientProvider
, useQuery와 useMutaion 훅
으로 이루어지며 데이터 패칭과 상태관리를 간소화 할 수 있다.
query는 데이터를 읽어오는 작업, Mutation은 데이터를 변경하는 작업을 의미한다.
//Query 작업
const {data, isLoading, error} = useQuery('todos', fetchTodos);
// istLoading이 true일 때 : data는 undefined, error는 null
// 데이터 로드 성공 시 : data는 fetched data, isLodaing은 false, error는 null
// 에러 발생 시: data는 undefined, isLoading은 false, error는 Error 객체
const mutation = useMutation((newTodo)=>{
return axios.post('/todos', newTodo)
})
mutation.mutate((title: 'New Todo'))
// mutation.isLoading: true during the mutation
// mutation.isError: true if an error occurrd
// mutation.isSuccess: true if the mutation succeeded
이 두 요소는 React Query의 설정과 컨텍스트를 제공하여 앱 전체에서 일관된 방식으로 사용 가능
const queryClient = new QueryClient();
function App() {
return (
<QueryClientProvider client={queryClient}>
{애플리케이션 컴포넌트들}
</QueryClientProvider>
)
}
// 이 설정으로 모든 하위 컴포넌트에서 React Query 기능을 사용할 수 있다.
이 훅들은 실제로 데이터를 가져오고 변경하는 데 사용됨
const {data, isLoading, error} = useQuery('todos', fetchTodos)
const mutation = useMutation((newTodo)=> {
return axios.post('/todos', newTodo)
})
useQuery 사용 예시
const [checked, setChecked] = useState(false)
const {istLoading, error, data: products} = useQuery({
queryKey: ['products', checked],
queryFn: async()=>{
console.log('fetching...', checked)
const reponse = await fetch(`data/${checkd ? 'item_' : ''}products.json`)
return await response.json()
}
staleTime: 1000*60*3
})
if( isLoading) return <p> loading...</p>
if (error) return <p> {error} </p>
useMutation 사용예시 - 서버의 데이터 변경 추가나 삭제
const addTodo = (newTodo)=> fetch(`https://jsonplaceholder.typicode.com/todos`,{
method:'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(newTodo)
})
.then(response => {
if(!reponse.ok) {
throe new Error(~~~)
}
return response.json()
})
function AddTodo() {
const queryClient = useQueryClient()
const mutation = useMutation(addTodo, {
onSuccess: () => {
queryCleint.invalidateQueries('todos')
}
})
}
const hadnleSumbit = (e) => {
e.prventDefault()
const newTodo = {title: e.target.todo.value, completed: false}
mutation.mutate(newTodo)
}
return (
<form onSubmit={handleSubmit}>
<input type="text" name="todo" />
<button type="submit">Add Todobutton>
{mutation.isLoading ? 'Adding todo...' : ''}
{mutation.isError ? `An error occurred: ${mutation.error.message}` : ''}
{mutation.isSuccess ? 'Todo added!' : ''}
form>
);
데이터가 변경되었을 때 관련된 쿼리를 자동으로 업데이트하는 데 사용 자동으로 새로고침 하는 경우 활용
const queryClient = useQueryClient()
//todos 쿼리 무효화
qyeryClient.invalidateQueries('todos')
// todos 쿼리가 무효화되고 백그라운드에서 새로운 데이터를 fetch함
const {isError, error} = useQuery('todos',fetchTodos, {
rerty:3,
onError: (error)=> {
console.log('An error occurred:', error)
}
})
if(isError) {
return <div>{error.message}</div>
}
Query key는 리액트 쿼리에서 데이터를 식별하고 관리하는 핵심 요소로 복잡한 애플리케이션에서 데이터를 관리하고 업데이트하기 위해 중요하다.
즉 고유한 키로 쿼리를 식별하여 캐시가 관리된다.
// 단순 문자열 키
const {data: todos} = useQuery('todos', fetchTodos)
//배열 형태의 키
const {data: todo} = useQuery(['todo',5], ()=> fetchTodoById(5))
// 객체를 포함한 배열 키
const {data: doneTodos} = useQuery(['todos',{status:'done'}], ()=>
fetchTodosByStatus('done'))
const {data, isLodaing, error, isPreviousData} = useQuery(
['todos',page],
()=> fetchTodoPage(page),
{keepPreviousData: true}
)
const {data, fetchNextPage,hadNextPage,isFetchingNextPage} = useInfiniteQuery('todos', fetchTods, {
hetNextPageParam: (lastPage,pages) => lastPage.nextCursor
})
Prefetching은 사용자 경험을 개선하기 위해 데이터를 미리 로드하는데 사용
const queryClient = useQueryClient();
queryClient.prefetchQuery('todos',fetchTodos)
//todos 데이터가 백그라운에서 미리 로드되어 캐시에 저장됨
Optimistic Updates는 서버 응답을 기다리지 않고 UI를 즉시 업데이트하는 기술입니다.
const queryClient = useQueryClient();
const mutation = useMutation(updateTodo, {
onMutate: async(newTodo) => {
await queryClient.cancelQueries('todos')
const previousTodos = queryClient.getQueryData('todos')
queryClient.setQueryData('todos', old => [...old, newTodo])
return {previousTodos}
}
onError: [err, newTodo, context]=> {
queryClient.setQueryData('todo', context.previousTodos
)},
onSettled: ()=> {
queryClient.invalidateQueries('todos')
}
})
// 변경 즉시 UI 업데이트
// 에러 시 이전 상태로 롤백
// 성공 여부와 관계없이 서버 데이터와 동기화