회사에서도 그렇고 사이드 프로젝트를 할 때도 그렇고 저는 오랬동안 redux, redux-saga를 사용해 오면서 리엑트에서 APIs들을 관리 해왔습니다. 사용하면서 느꼈던 것이 작은 API를 하나 사용하려고 해도 적어야 되는 코드에 량이 생각보다 많다라는 것 이 였습니다.
예를들어 todo list를 가져올 때 redux 와 redux-saga를 사용하면 보통 코드는 아래와 비슷할 것 입니다.
dispatch(fetchTodoList())
function* handleFetchTodoList(action) {
await put(fetchTodoListApi.request())
try {
const response = await fetch(url, action.data)
await put(fetchTodoListApi.success(response))
} catch(e) {
await put(fetchTodoListApi.failure(e))
}
}
// redux
const initialState = {
isLoading: false,
todoList: null
}
case fetch_todo_list_request:
...
...
위와 같이 하나 fetch하는데 액션이 4개나 필요하고 redux 에서도 state 관리를 해야 합니다.
이런 것에서 문제점을 느낀 저자는 이런 서버에서 오는 데이터를 server state이라고 말하고 구지 이런 데이터를 redux에서 따로 관리 할 필요는 없다라고 생각해서 만들게 된 것 같습니다.
react-query를 사용하면 위에 같은 코드들이 확 줄게 됩니다. 아래에 보는 것 처럼 여러가지 state을 관리를 해주게 됩니다.
const { isLoading, error, data } = useQuery('todos', fetchTodoList)
React-query는 잘 관리되고 많이 사용되어 지는가?
프로젝트에서 사용할 라이브러리를 선택할 때 꽤나 중요한 요소 입니다. 특히나 리엑트를 사용하게 되면 완전한 프레임워크가 아니기 때문에 여러가지 라이브러리를 사용하게 되는데 쓰기 시작했다가 라이브러리가 나중에 관리도 안되고 버그도 많고 그러면 나중에 골치가 아파 지기 때문이죠. 이런 부문에서 react query는 합격점을 줘도 될것 같습니다.
react-query 사용하는 이유?
- 여러가지 feature를 어려운 설치 없이 바로 사용이 가능하다.
- api 캐시를 알아서 해주고 config를 통해서 캐쉬 시간과 업데이트 주기를 설정 해줄 수 있다.
- pagination api를 특별한 셋업 없이 바로 사용 할 수 있다.
- optimistic update가 가능하다(api가 성공할 것이라고 가정하고 ui를 업데이트 한 뒤에 나중에 성공하면 그대로 두고 실패하면 업데이트 전으로 되돌리는 기능)
- 코드를 많이 줄일 수 있다.
- 캐시를 통해서 UX를 향상 시킬수 있다(back, forward button)
*react-query 라이브러리는 graphql, typescript도 지원합니다.
중요한 기본 설정 및 설명
cache 시간은 은 기본적으로 5분 ( cacheTime 옵션으로 변경 가능)
query는 실패 했을 때 기본으로 3번 자동 재 시도 합니다.
서버에 request를 넣지 않고 cache에서 바로 데이터를 가져오고 싶을 때 staleTime를 설정 해주면 된다.
refetchOnMount, refetchOnWindowFocus, refetchOnReconnect를 기본적으로 true로 설정 되어 있다.
기본적인 useQuery사용법
useQuery(arg1 arg2, arg3)
arg1 에는 캐시를 확인 할 때 사용되는 키를 넘겨 줄 수 있습니다. 아래와 같이 string, object array 형태 모두 가능합니다.
'todos', ['todo', 1], ['todo', { test: 1 }]
arg2는 promise를 return 하는 function 이면 됩니다. 예를들면 아래와 같습니다.
const fetchTodos = () => {
const response = fetch('/todos')
return response.json()
}
arg3는 option들을 넣어 주시면 됩니다.
만약에 todos api를 사용하기 위해서는 user를 먼저 불러와야 한다면 다음과 같이 사용할 수 있습니다.
const { data: user } = useQuery(['user', email], getUserByEmail)
const userId = user?.id
// Then get the user's projects
const { isIdle, data: todos, refetch } = useQuery(
['todos', userId],
getTodosByUser,
{
// The query will not execute until the userId exists
enabled: !!userId,
}
)
enabled가 true로 바뀌는 순간 api를 call 하게 됩니다. enabled가 false라도 refetch function을 통해서 여전히 api를 사용할 수 있습니다.
enabled가 false인 동안에는 status가 idle로 나오게 됩니다.
mutation
mutation은 graphql에서 사용이 되는데 여기서도 비슷한 개념으로 사용됩니다. useMutation도 update, create를 사용할 때 사용됩니다.
아래 처럼 backend에서 만약에 데이터가 돌아오면 이미 있던 state를 update 해줄 수 있습니다.
const mutation = useMutation(editTodo, {
onSuccess: data => {
queryClient.setQueryData(['todo', { id: 5 }], data)
}
})
Author가 업필 하기를 optimistic query는 다른 라이브러리에 아직 없다고 합니다. optimistic query를 간단히 설명해보겠습니다.
예를들어 facebook에 좋아요 버튼이 있는데 이것을 유저가 누른다면 일단 client 쪽 state을 먼저 업데이트 합니다. 그리고 만약에 실패 한다면 예전 state으로 돌아가고 성공하면 필요한 데이터를 다시 fetch를 해서 서버 데이터와 확실히 연동을 해 줍니다.
optimistic query가 정말 유용할 때는 인터넷 속도가 느리거나 서버가 느릴 때 유저가 한 액션을 기다릴 필요 없기 바로 업데이트 되는 것 처럼 보이기 때문에 UX적으로 좋습니다.
아래 코드를 보시면 좀더 이해가 잘 가실 것 같습니다.
useMutation(updateTodo, {
// When mutate is called:
onMutate: async newTodo => {
// Cancel any outgoing refetches (so they don't overwrite our optimistic update)
await queryClient.cancelQueries('todos')
// Snapshot the previous value
const previousTodos = queryClient.getQueryData('todos')
// Optimistically update to the new value
queryClient.setQueryData('todos', old => [...old, newTodo])
// Return a context object with the snapshotted value
return { previousTodos }
},
// If the mutation fails, use the context returned from onMutate to roll back
onError: (err, newTodo, context) => {
queryClient.setQueryData('todos', context.previousTodos)
},
// Always refetch after error or success:
onSettled: () => {
queryClient.invalidateQueries('todos')
},
})
이 외에도 server side도 지원하고 api 취소도 가능합니다. 제가 조금더 사용해보고 후기 또는 자세한 사용법에 대해서 더 올리도록 하겠습니다.