일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
- github
- HTML
- react
- @media
- redux
- coding
- media query
- Servlet
- 코딩테스트
- CSS
- 반응형 페이지
- 그럼에도불구하고
- 변수
- node
- JavaScript
- frontend
- 프론트엔드
- 자바
- 코드업
- 그럼에도 불구하고
- react-router-dom
- cleancode
- git
- java
- node.js
- JS
- max-width
- 자바문제풀이
- TypeScript
- webpack
- Today
- Total
그럼에도 불구하고
[React Query] React Query에 대하여 본문
React Query에 대해 알아보겠습니다.
🧑🏻💻 React Query란?
공식문서에 따르면 React Query란 fetching, caching, 서버 데이터와의 동기화를 지원해 주는 라이브러리라고 합니다.
React Query는 React Application에서 서버의 상태를 불러오고, 캐싱하며, 지속적으로 동기화하고 업데이트하는 작업을 도와주는 라이브러리입니다. React Query는 우리에게 친숙한 Hook을 사용하여 React Component 내부에서 자연스럽게 서버(또는 비동기적인 요청이 필요한 Source)의 데이터를 사용할 수 있는 방법을 제안한다고 합니다. - 카카오페이 React Query 설명 인용
또한 카카오페이 프론트엔드 개발자분들의 말을 더 빌려보자면, React Query를 사용하는 이유를 아래와 같이 요약할 수 있습니다.
⭐️ 1. React Query는 React Application에서 서버 상태를 불러오고, 캐싱하며, 지속적으로 업데이트하는 작업을 도와주는 라이브러리입니다.
⭐️ 2. 복잡하고 장황한 코드가 필요한 다른 데이터 불러오기 방식과 달리 React Component 내부에서 간단하고 직관적으로 API를 사용할 수 있습니다.
⭐️ 3. 더 나아가 React Query에서 제공하는 캐싱, Window Focus Refetching 등 다양한 기능을 활용하여 API 요청과 관련된 번잡한 작업 없이 "핵심 로직"에 집중할 수 있습니다.
🧑🏻💻 React Query의 장점
📌 캐싱(Caching)
React Query의 장점 중 하나는 데이터를 캐싱한다는 점입니다.
캐싱이란 특정 데이터의 복사본을 저장하여 이후 동일한 데이터에 다시 접근할 때, 속도를 높이는 것을 말합니다.
React Query는 캐싱을 통해 동일한 데이터에 대한 반복적인 비동기 데이터 호출을 방지하고, 이는 불필요한 API 호출을 막아 서버에 대한 부하를 줄이는 결과를 가져옵니다.
여기서 드는 의문은 "반복적인 데이터가 아닌 데이터가 변경되었을 경우에는 어떻게 인식하는가?"입니다.
만일 서버 데이터를 불러와 캐싱한 후, 실제 서버 데이터를 확인했을 때 서버 상에서 데이터의 상태가 변경되어 있다면, 사용자는 실제 데이터가 아닌 변경 전의 데이터를 보게 됩니다. 이는 사용자에게 잘못된 정보를 보여주는 에러를 낳습니다.
🏷️ 언제 데이터를 갱신해야 하는가?
위와 같은 에러를 발생시키지 않고 필요한 상황에 적절하게 데이터를 갱신해 줄 수 있어야 합니다. 그렇다면 그런 상황은 언제일까요?
- 사용자가 화면을 보고 있을 때
- 페이지의 전환이 일어났을 때
- 페이지 전환 없이 이벤트가 발생해 데이터를 요청할 때
크게 보면 위와 같이 3가지로 나눌 수 있습니다. 이를 위해 React Query에서는 기본적인 아래의 옵션을 제공합니다.
refetchOnWindowFocus, //default: true
refetchOnMount, //default: true
refetchOnReconnect, //default: true
staleTime, //default: 0
cacheTime, //default: 5분 (60 * 5 * 1000)
위의 옵션들을 통해 React Query가 어떤 시점에 데이터를 Refetching 하는지 알 수 있습니다.
- 브라우저에 포커스가 들어온 경우 (refetchOnWindowFocus)
- 새로운 컴포넌트 마운트가 발생한 경우 (refetchOnMount)
- 네트워크 재연결이 발생한 경우 (refetchOnReconnect)
더 많은 옵션은 React Query 공식 홈페이지에서 확인할 수 있습니다. 🧐
🏷️ staleTime vs cacheTime
※ stateTime ( 데이터의 유통기한, default : 0 )
- stateTime은 데이터가 fresh -> stale 상태로 변경되는 데 걸리는 시간을 뜻합니다. ( 최신 데이터 : fresh 데이터, 기존 데이터 : stale 데이터 )
- fresh 상태일 때는 Refetch 트리거가 발생해도 Refetch가 일어나지 않습니다. (위의 3가지 경우)
- 기본 값이 0이므로 따로 설정해주지 않으면, Refetch 트리거가 발생했을 때 무조건 Refetch가 발생합니다.
※ cacheTime ( 데이터의 유지시간 default : 5분 )
- cacheTime은 데이터가 inactive 한 상태일 때 캐싱된 상태로 남아있는 시간을 뜻합니다.
- 특정 컴포넌트가 unmount(페이지 전환 등으로 화면에서 사라질 때)가 되면 사용된 데이터는 inactive 상태로 바뀌고, 이때 데이터는 cacheTime만큼 유지됩니다.
- cacheTime 이후 데이터는 garbage collector로 수집되어 메모리에서 해제됩니다.
- 만일 cacheTime이 지나지 않았는데 해당 데이터를 사용하는 컴포넌트가 다시 mount 되면, 새로운 데이터를 fetch 해오는 동안 캐싱된 데이터를 보여줍니다.
- 즉, 캐싱된 데이터를 계속 보여주는 게 아니라 fetch 하는 동안 임시로 보여주는 것입니다.
📌 프로젝트의 규모
프로젝트의 규모가 커지고 관리해야 할 데이터가 많아지면, Client에서 관리하는 데이터와 Server에서 관리하는 데이터가 분리될 필요성을 느끼게 됩니다.
Client Data: 모달 관련 데이터, 페이지 관련 데이터 등등
Server Data: 사용자 정보, 비즈니스 로직 관련 정보 등등
간단하게 생각해서 비동기 API 호출을 통해 불러오는 데이터들을 Server 데이터라고 할 수 있습니다.
실제 Client 데이터의 경우 Redux, Recoil과 같은 전역 상태 관리 라이브러리들을 통해 잘 관리되어오고 있으나, 문제는 이러한 라이브러리들이 Server 데이터까지도 관리를 해야 하는 상황이 발생한다는 것입니다.
+ Redux를 사용할 경우 프로젝트 규모가 커질수록 너무 장황한 Boilerplate 코드
위의 상태 관리 라이브러리에도 비동기 함수를 처리하는 로직이 존재하거나, 서드 파티 라이브러리를 지원하는 것이 많습니다. 그러나 이들의 Client 데이터와 Server 데이터를 완벽히 분리하여 관리에 용이하도록 충분한 기능이 지원된다고 보기는 어렵습니다.
즉, 위의 라이브러리들은 Client 데이터를 관리하는데 로직이 집중되어 있기 때문에, Server 데이터까지 효율적으로 관리하기에는 한계가 분명합니다.
이때 React Query는 이런 문제에 대한 해결책을 제시해 주는데, 아래 코드를 살펴보겠습니다.
const { data, isLoading } = useQueries(
['key'],
() => {
return api({
url: URL,
method: 'GET',
});
},
{
onSuccess: (data) => {
// 성공했을 때 data로 ~~를 하는 로직
}
},
{
onError: (error) => {
// 실패했을 때 error로 ~~를 하는 로직
}
}
)
위의 예시에서는 컴포넌트 내부에서 위와 같은 로직을 통해 Server 데이터를 가져오고 있는데, 이때 onSuccess와 onError 함수를 통해 fetch 성공과 실패에 대한 분기를 아주 간단하게 구현할 수 있습니다. 이는 Server 데이터를 불러오는 과정에서 구현해야 할 추가적인 설정들을 진행할 필요가 없어졌음을 말해줍니다.
즉, Client 데이터는 상태 관리 라이브러리가 관리하고, Server 데이터는 React-Query가 관리할 수 있습니다.
이를 통해 Client 데이터와 Server 데이터를 온전하게 분리할 수 있습니다.
⭐️ Server 데이터를 상태 관리 라이브러리로 관리하는건?
상태 관리 라이브러리를 통해 Server 데이터를 관리할 수 있지만,
refetch가 여러 번 일어나는 상황에 매번 Server 데이터를 전역 상태로
가져오는 것이 마냥 좋다고는 할 수 없습니다.
📌 판단은 오로지 나 자신의 몫 :)
잠깐 언급했던 Redux의 Boilerplate 코드를 언급하자면,
Redux에는 Redux의 기본 원칙이 존재합니다. 이 기본 원칙을 충족하기 위해서 Redux을 사용하는데 너무 많은 Boilerplate 코드가 요구됩니다. (redux-toolkit의 등장으로 많이 보완됐지만..) Redux로 비동기 데이터를 관리하는 일에는 여전히 불필요하게 느껴지는 반복되는 Boilerplate 코드가 필요합니다.
예를 들어, 하나의 API 요청을 처리하기 위해 여러 개의 Action과 Reducer가 필요하여 전체 코드가 눈에 잘 들어오지 않습니다.
// features/todos/todos.slice.ts
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { TodoItem } from 'types/todo';
export interface TodoListState {
fetchTodos: {
data?: TodoItem[];
isLoading: boolean;
error?: Error;
};
postTodos: {
isLoading: boolean;
error?: Error;
};
}
const initialState: TodoListState = {
fetchTodos: {
data: undefined,
isLoading: false,
error: undefined,
},
postTodos: {
isLoading: false,
error: undefined,
},
};
export const todoListSlice = createSlice({
name: 'todoList',
initialState,
reducers: {
// 이전 State의 값을 바탕으로 다음 State의 값을 새로 만드는 순수함수 "Reducer"
// redux-toolkit은 immer를 내부적으로 사용하므로 조금 더 자연스럽게 Reducer를 구성할 수 있게끔 도와줍니다.
requestFetchTodos: (state) => {
state.fetchTodos.isLoading = true;
},
successFetchTodos: (state, action: PayloadAction<TodoItem[]>) => {
state.fetchTodos.data = action.payload;
state.fetchTodos.isLoading = false;
state.fetchTodos.error = undefined;
},
errorFetchTodos: (state, action: PayloadAction<string>) => {
state.fetchTodos.data = undefined;
state.fetchTodos.isLoading = false;
state.fetchTodos.error = action.payload;
},
requestPostTodos: (state, _: PayloadAction<string>) => {
state.postTodos.isLoading = true;
},
successPostTodos: (state) => {
state.postTodos.isLoading = false;
},
errorPostTodos: (state, action: PayloadAction<string>) => {
state.postTodos.isLoading = false;
state.postTodos.error = action.payload;
},
},
});
export const {
requestFetchTodos,
successFetchTodos,
errorFetchTodos,
requestPostTodos,
successPostTodos,
errorPostTodos,
} = todoListSlice.actions;
export default todoListSlice.reducer;
📌 로직의 단순화
🏷️ Data Fetching
기존에 서버에서 데이터를 가져오기 위해서는 다음과 같은 절차가 필요했습니다.
- Fetching을 위한 코드 작성
- 데이터를 담아 둘 상태 생성
- useEffect를 이용해 컴포넌트 Mount시 데이터를 Fetching 한 후에 상태 저장
import { useEffect, useState } from "react";
const getData = async () => {
const data = await fetch(
"https://jsonplaceholder.typicode.com/posts"
).then((response) => response.json());
return data;
};
export default function App() {
const [state, setState] = useState<any[]>([]);
useEffect(() => {
getData()
.then((dataList) => setState(dataList))
.catch((e) => setState([]));
}, []);
return <div>{JSON.stringify(state)}</div>;
}
위와 같은 코드를 React Query를 사용한다면 다음과 같이 바꿀 수 있습니다.
import { useQuery } from "react-query";
const getData = async () => {
const data = await fetch(
"https://jsonplaceholder.typicode.com/posts"
).then((response) => response.json());
return data;
};
export default function App() {
const { data } = useQuery(["data"], getData);
return <div>{JSON.stringify(data)}</div>;
}
서버 데이터를 담을 상태를 만들고, useEffect로 상태에 데이터를 담는 모든 과정을 useQuery 단 한 줄로 처리할 수 있습니다.
이로 인해 발생할 수 있는 장점은 다음과 같습니다. ( 개인적인 생각 )
1. 코드 수 감소
일반적인 서버 데이터를 받아오는 과정의 코드가, 받아올 서버 데이터에 비례해서 추가되는 상태와 Side Effect를 생각하면 많은 수의 코드를 감소시킬 수 있습니다.
2. Side Effect 제거
상태마다 작성해줘야 하는 useEffect를 사용하지 않을 수 있어, 서버 데이터로 발생할 수 있는 Side Effect와 같은 문제를 해결시킬 수 있습니다.
3. 개발자들의 성향에 따른 코드를 보다 보편화
서버에서 데이터를 불러오는 방법에는 명확한 정답이 없기 때문에, 개발자의 입맛대로 데이터를 불러오는 코드를 React Query를 사용하면 내장된 기능으로 관련 로직을 전부 처리해 주니 지원하는 훅을 사용하여 모두가 획일화된 방식으로 코드를 작성할 수 있습니다.
🏷️ 동기적 실행
특정 API를 호출하기 위해 다른 API의 값을 파라미터로 넣어줘야 할 때가 있습니다.
그럴 때는 API를 동기적으로 호출해줘야 합니다.
const [state1, setState1] = useState();
const [state2, setState2] = useState();
useEffect(()=>{
getData().then((dataList)=>{
setState1(dataList[0]);
})
},[]);
// state1의 데이터를 파라미터로 넣어 호출하는 API
useEffect(()=>{
if(state1){
getAfterData(state1).then((dataList)=>{
setState2(dataList);
})
}
},[state1]);
API 호출을 위한 조건이 추가되어 동기적으로 관리가 필요하고 useEffect의 위치가 다른 코드들과 섞인다면 어떤 데이터가 언제, 어떻게 호출되는지 흐름과 시점을 파악하기 어렵습니다.
이는 React Query의 enabled 옵션을 사용하면 보다 깔끔하고 간단하게 대체할 수 있습니다.
const { data: data1 } = useQuery(["data1"], getData);
const { data: data2 } = useQuery(["data2", data1], getData, {
enabled: !!data1
});
enabled은 조건이 충족될 때만 API를 호출하게 되어 위와 같이 간단하게 API를 동기적으로 호출할 수 있습니다.
🧑🏻💻 React Query의 단점
React Query가 무조건적으로 좋은 것만은 아닙니다.
다음과 같은 단점이 있을 수 있습니다.
- 러닝 커브가 높아 콘셉트를 이해하고 초기 설정하는데 시간이 걸릴 수 있습니다.
- 라이브러리 자체에 버그나 누락된 기능이 있을 수 있습니다.
- 소규모 프로젝트에서 사용 시 프로젝트 규모에 비해 복잡성이 추가될 수 있습니다.
- 모든 케이스에서 적합한 것은 아니기 때문에 React Query의 도입 여부를 따져보고 가치가 있을 때 도입해야 합니다.
🧑🏻💻 여러 가지 기능들
React Query에서 data fetching을 위해 제공하는 대표적인 기능에 대해 알아보겠습니다.
기본적으로 GET에는 useQuery, PUT, UPDATE, DELETE에는 useMutation이 사용됩니다.
📌 useQuery
- 첫 번째 파라미터로 unique key를 포함한 배열이 들어갑니다. ( v4 버전 이후) 동일한 쿼리를 불러올 때 유용하게 사용할 수 있습니다.
- 첫 번째 파라미터에 들어가는 배열의 첫 요소는 unique key로 사용되고, 두 번째 요소부터는 query 함수 내부의 파라미터로 값들이 전달됩니다.
- 두 번째 파라미터로 실제 호출하고나 하는 비동기 함수가 들어갑니다. 이때 함수는 Promise를 반환하는 형태입니다.
- 최종 반환 값은 API의 성공, 실패 여부, 반환 값을 포함한 객체입니다.
아래의 예시는 v3 버전과 v4 버전일 때를 구분한 예시입니다.
import { useState } from "react";
import { useQuery } from "react-query"; // v3 버전
import { useQuery } from '@tanstack/react-query' // v4 버전
import { PostDetail } from "./PostDetail";
const maxPostPage = 10;
async function fetchPosts() {
const response = await fetch(
"https://jsonplaceholder.typicode.com/posts?_limit=10&_page=0"
);
return response.json();
}
export function Posts() {
const [currentPage, setCurrentPage] = useState(0);
const [selectedPost, setSelectedPost] = useState(null);
// replace with useQuery
const { data, isError, error, isLoading } = useQuery(
"posts" // v3 버전일 때
["posts"] // v4 버전일 때
, fetchPosts, { staleTime: 2000});
if(isLoading) return <h3>Loading...</h3>;
if(isError) return <>
<h3>Oops, something went wrong!</h3>
<p>{error.toString()}</p>
</>
return (
<>
<ul>
{data.map((post) => (
<li
key={post.id}
className="post-title"
onClick={() => setSelectedPost(post)}
>
{post.title}
</li>
))}
</ul>
<div className="pages">
<button disabled onClick={() => {}}>
Previous page
</button>
<span>Page {currentPage + 1}</span>
<button disabled onClick={() => {}}>
Next page
</button>
</div>
<hr />
{selectedPost && <PostDetail post={selectedPost} />}
</>
);
}
useQuery 함수가 반환하는 객체를 보면 data를 통해 성공 시 데이터를 반환할 수 있습니다.
또한 isLoading을 통해 로딩 여부를, isError를 통해 에러 발생 여부를, 마지막으로 error를 통해 보다 직관적인 error 발생 이유를 알 수 있습니다.
⭐️ useQueries
여러 개의 useQuery를 한 번에 실행하고자 하는 경우, 기존의 Promise.all()처럼 묶어서 실행할 수 있습니다.
const testQuery = useQueries({
queries: [
{ queryKey: ['post', 1], queryFn: fetchPost},
{ queryKey: ['post', 2], queryFn: fetchPost}
]
})
📌 useMutation
useMutation은 PUT, UPDATE, DELETE와 같이 값을 변경할 때 사용할 수 있는 API입니다. 반환 값은 useQuery와 동일합니다.
아래의 예시는 TodoList와 같은 애플리케이션에서 사용할 수 있는 예시입니다.
function App() {
const mutation = useMutation({
mutationFn: (newTodo) => {
return axios.post('/todos', newTodo)
},
})
return (
<div>
{mutation.isLoading ? (
'Adding todo...'
) : (
<>
{mutation.isError ? (
<div>An error occurred: {mutation.error.message}</div>
) : null}
{mutation.isSuccess ? <div>Todo added!</div> : null}
<button
onClick={() => {
mutation.mutate({ id: new Date(), title: 'Do Laundry' })
}}
>
Create Todo
</button>
</>
)}
</div>
)
}
위의 코드에서 반환 값은 useQuery와 동일하지만, 사용 시 post 비동기 함수를 넣어주었습니다. 이때 useQueryt와 다르게 useMutation의 첫 번째 파라미터에 비동기 함수가 들어가고, 두 번째 인자로 상황 별 분기 설정이 들어간다는 차이점이 있습니다.
실제 사용 시에는 mutation.mutate 메서드를 사용하고, 첫 번째 인자로 API 호출 시에 전달해줘야 하는 데이터를 넣어주면 됩니다.
🏷️ 출처
https://velog.io/@kandy1002/React-Query-%ED%91% B9-%EC% B0% 8D% EC%96% B4% EB% A8% B9% EA% B8% B0
'React > React query' 카테고리의 다른 글
[React Query] isLoading과 isFetching은 헷갈려 (0) | 2023.10.19 |
---|---|
[React Query] 페이지네이션(Pagination)과 데이터 프리페칭(Prefetching) (1) | 2023.10.18 |
[React Query] React Query를 TypeScript로 사용할 때 Type 지정 (0) | 2023.10.14 |
[React Query] useInfiniteQuery와 Infinite Scroll (0) | 2023.08.30 |