react에서 비동기를 다루는 방법은 다양하다.
javascript 언어니까 당연히 Promise, async & await으로 처리가 가능하다.
redux를 사용하고 있다면, redux saga, thunk 등 다양한 미들웨어가 제공된다.
하지만 이런 것들로 서버의 상태를 관리하기란 여간 어려운 일이 아니다.
아래의 이유들로 앱과 서버 간의 관계 및 상태를 말할 수 있을 것이다.
함수형 컴포넌트 내에서 Hook 형태로 사용한다.
서버의 상태를 다른 곳에 저장하여 관리할 필요가 없다.
import { useQuery } from 'react-query';
function Example() {
const { isLoading, error, data, isFetching } = useQuery('repoData', () =>
fetch(
'https://api.github.com/repos/tannerlinsley/react-query'
).then((res) => res.json())
);
if (isLoading) return 'Loading...';
if (error) return 'An error has occurred: ' + error.message;
return (
<div>{JSON.stringify(data)}<div>
);
}
따로 저장하지 않다보니, 서버에서 가공된 상태의 데이터를 바로 사용하는 경우에 적용하기 좋다.
또한 다른 멀리 떨어진 컴포넌트에게 데이터를 전달할 필요가 없는 경우에 적용하기 좋다.
만약, 서버 데이터를 직접 제어하는 것을 선호한다면 React Query는 도입하지 않는 것이 나을 수도 있다.
React Query의 Hook 중 하나인 useQuery 파라미터로 API 데이터의 만료 시간, 리프레싱 주기, 데이터를 캐시에서 유지할 기간, 브라우저 포커스 시 데이터 리프레시 여부, 성공/에러 콜백 등의 기능을 제어할 수 있다.
기능과 관련하여 최근에 구현한 기능 중 하나를 예로 들자면, "아이템 목록"이 가능할 것 같다.
- 목록에 설정한 필터와 같은 설정값은 서버에서 변경될 가능성이 낮기 때문에 만료 시간을 무한으로 설정해 추가 API 호출을 방지할 수 있음.
- 목록 만료 시간을 수 분으로 설정하여, 사용자가 페이지를 반복하여 이동하는 경우 API 반복 호출하는 것을 방지할 수 있음.
- 수 분의 만료 시간 내에, 목록에 아이템을 추가 생성하거나 수정할 경우는? -> 캐시를 강제로 무효화시키고 목록을 새로고침.
- 아이템 정보를 수정한 후 목록으로 돌아갔을 경우, 새로이 API 호출한 결과값으로 수정 정보가 반영된 아이템 목록을 보여줘야 함. 하지만 React Query로 API 호출 결과값이 아닌, 수정 시 캐시된 아이템 정보를 사용하여 목록 정보가 즉시 응답된 것으로 보이게 만들 수 있음.
React Query를 통해 관리하는 쿼리 데이터(Query, useQuery가 반환하는 객체 속성값)는 4가지 상태를 가진다.
1. fresh : 새롭게 추가된 쿼리 인스턴스이며, 만료되지 않은 쿼리. 컴포넌트의 mount, update 시에 데이터를 재요청하지 않음.
2. fetching : 요청 상태인 쿼리.
3. stale : 데이터 패칭이 완료되어 만료된 쿼리. stale 상태의 같은 쿼리를 useQuery로 재호출하여 컴포넌트 마운트를 한다면 캐싱된 데이터가 반환됨.
4. inactive : 비활성 쿼리로써 사용하지 않음. 5분 뒤에 가비지 콜렉터가 캐시에서 제거함.
5. delete : 가비지 콜렉터에 의하여 캐시에서 제거된 쿼리.
React Query API의 기본 설정에 대한 내용이다.
캐시를 관리하기 위하여 QueryClient 인스턴스를 사용한다.
이 때, 컴포넌트가 useQuery hook 안에서 QueryClient 인스턴스에 접근 가능하도록 만드는 QueryClientProvider를 컴포넌트 트리 상위에 추가해야 한다.
import { QueryClient, QueryClientProvider } from 'react-query'
const queryClient = new QueryClient()
function App() {
return (
<QueryClientProvider client={queryClient}>
<Component />
</QueryClientProvider>
}
서버에서 데이터를 가져와 캐싱할 때 사용하는 기본적인 hook이다.
const { data, isLoading, status, error, isFetching } = useQuery(queryKey, queryFunction, options)
queryFunction에는 서버에 데이터를 요청하고 Promise 또는 에러를 반환하는 함수를 전달한다.
queryKey에는 문자열과 배열을 넣을 수 있다. queryKey의 유연성이 캐싱 처리를 도와주는 핵심 역할이다. queryKey의 조합에 따라 key가 다르면 캐싱도 별도 관리하기 때문이다.
자세한 내용은 이 곳에서 확인한다.
다음은 useQuery의 반환값 및 옵션에 대한 내용이다.
반환값
옵션
일반적인 상황에서 여러 query가 선언되어 있는 경우라면, 병렬적으로 요청 및 처리된다.
덕분에 query 처리의 동시성이 극대화된다.
function App() {
const profilesQuery = useQuery('profiles', fetchProfiles)
const projectsQuery = useQuery('projects', fetchProjects)
const groupsQuery = useQuery('groups', fetchGroups)
만약 여러 query들을 동시에 수행해야 하는데, 렌더링이 거듭되는 사이 사이에 계속해서 query가 수행되어야 한다면 query를 수행하는 것이 hook 규칙에 위배될 수도 있다.
그럴 때 쓰면 좋은 것이 useQueries이다
function App({users}) {
const userQueries = useQueries(
users.map(user => {
return {
queryKey: ['user', user.id],
queryFn: () => fetchUserBZyId(user.id),
}
})
)
}
서버에서 데이터를 가져오는 것은 단순히 useQuery를 사용하면 될테지만, 서버의 데이터를 업데이트 하는 경우에는 동일한 방식을 사용하는 것이 적절치 않다. 데이터의 생성/수성/삭제 (CRUD) 시에는 "useMutation" hook을 사용하면 된다.
const mutation = useMutation(newTodo => axios.post('/todos', newTodo))
const handleSubmit = useCallback(
(newTodo) => {
mutation.mutate(newTodo)
},
[mutation],
)
useQuery의 옵션과 같이 콜백을 전달할 수 있다.
(onSuccess, onError, onSettled)
심지어 mutate 호출 시 실행할 onMutate 콜백도 사용할 수 있다.
만약 Redux를 사용한다면 request 성공에 대한 액션을 미들웨어에서 확인 후 추가 액션을 취할 것이다.
(Redux Saga라고 가정한다면, 특정 액션_SUCCESS 와 같은 형태의 액션말이다.)
useMutation을 사용한다면 onSuccess 콜백말고, mutateAsync 함수를 사용하는 것이 가독성에 좋을 것이다.
const mutation = useMutation(newTodo => axios.post('/todos', newTodo))
const handleSubmit = useCallback(
async (newTodo) => {
await mutation.mutateAsync(newTodo)
setAnotherState()
dispatch(createAnotherAction())
},
[mutation],
)
쿼리 데이터가 stale 상태가 되기만을 마냥 기다릴 수 없는 경우들도 있다.
게시글에 댓글을 작성하고 나면, 서버에서 댓글 목록을 다시 가져올 필요가 있다.
이 경우, 기존에 남아있던 댓글 목록에 대한 정보는 쓸모가 없어지게 된다.
쓸모없어진 query들에 대하여 미리 지정해놨던 staleTime이 넘기 전에 직접 무효화시키고 새로운 데이터를 가져오도록 해야한다.
const queryClient = useQueryClient();
queryClient.invalidateQueries() // 캐시의 모든 쿼리에 대한 무효화
queryClient.invalidateQueries('todos') // todos로 시작하는 모든 쿼리에 대한 무효화
queryClient.invalidateQueries(['todos', 1]) // 해당하는 키를 가지는 쿼리에 대한 무효화
// predicate 옵션을 사용하면 상세한 설정이 가능
queryClient.invalidateQueries({
predicate: query => query.queryKey[0] === 'todos' && query.queryKey[1]?.rate >= 10, // query key 배열 두번째 원소인 객체의 rage 필드 값이 10 이상인 query에 대한 무효화
})
const todoListQuery = useQuery(['todos', {rate: 15}], fetchTodoList) // 위의 설정으로 인하여 query가 무효화 됨
Effective Component - 변경에 유리한 컴포넌트 설계로 재사용성 높이기 (1) | 2022.06.16 |
---|---|
[React Native / Expo/ 앱] 한국판 "포켓몬 워들"로 없어서 못 구한다는 띠부씰 모으기 (0) | 2022.04.15 |
styled-components (0) | 2019.08.30 |
service worker란? (0) | 2019.07.11 |
Redux - State Container (0) | 2019.07.03 |
댓글 영역