tanstack query V5 에서 사라진 onSuccess, onError, onSettled

오다혜·2023년 7월 17일
5

문제

onSuccessonError and onSettled have been removed from Queries. They haven't been touched for Mutations. Please see this RFC for motivations behind this change and what to do instead.

→ V5에서 onSuccess, onError, onSettled 가 useQuery 의 option 에서 사라졌다.

왜 사라졌을까?

APIs are consistent, and the callbacks are not.

You can (apparently) use them to execute side effects, like showing error notifications:

1. 의도치 않게 여러 번 호출되는 callbackFn

아래와 같은 useQuery가 있을 때 에러 처리를 하기 위해서 onError에 callbackFn 을 등록할 수 있었다.

export function useTodos() {
  return useQuery({
    queryKey: ['todos', 'list'],
    queryFn: fetchTodos,
    onError: (error) => {
      toast.error(error.message)
    },
  })
}

만약 onError가 없다면 useEffect 로 처리를 해야 한다. 근데 useTodos 를 쓰는 곳이 여러 군데라면 useTodo가 호출될 때마다(혹은 최신화될 때마다) useEffect 가 호출되고 toast 가 여러 번 뜨게 된다.

export function useTodos() {
  const query = useQuery({
    queryKey: ['todos', 'list'],
    queryFn: fetchTodos,
  })

  React.useEffect(() => {
    if (query.error) {
      toast.error(query.error.message)
    }
  }, [query.error])

  return query
}

onError 을 사용하면, useQuery가 같은 api call 을 하나로 묶어주니까 에러 콜백도 한 번만 일어나게 될 것이라고 예상할 수 있을 것이다. → 근데 이는 완벽히 잘못된 생각

onError는 각 인스턴스마다 실행되므로 useEffect 를 사용하는 에러 처리와 동일한 결과가 나타나게 된다. 즉, 원치 않게 에러 처리 함수를 여러 번 호출하게 될 수 있다.

이를 해결하기 위해서는 onError 가 아니라 전역인 QueryClient 에서 처리하는 것을 권장하고 있다.

const queryClient = new QueryClient({
  queryCache: new QueryCache({
    onError: (error) =>
      toast.error(`Something went wrong: ${error.message}`),
  }),
})

2. state 동기화 중간에 잘못된 상태가 생김

useState 로 state를 만들고, useQuery 가 호출되면 onSuccess 로 state를 동기화 시켜주는 패턴도 많이 보인다. 다음의 예시도 todo 데이터를 fetching 해온 이후에 onSuccess에서 todoCount 의 상태를 동기화해주고 있다. 근데 이렇게 쓰는 것은 안티 패턴이다.

export function useTodos() {
  const [todoCount, setTodoCount] = React.useState(0)
  const { data: todos } = useQuery({
    queryKey: ['todos', 'list'],
    queryFn: fetchTodos,
    //😭 please don't
    onSuccess: (data) => {
      setTodoCount(data.length)
    },
  })

  return { todos, todoCount }
}

만약, fetchTodos를 호출한 결과값의 length가 5라고 해보자. 그러면 데이터를 가져오고 렌더링하는 일련의 과정 중에는 다음의 3가지 상태가 존재하게 된다.

  1. todo: undefined, todoCount: 0
  2. todo: length 5짜리 배열, todoCount: 0
  3. todo: length 5짜리 배열, todoCount: 5

setTodoCount의 결과가 동기적으로 동작하지 않기 때문에 싱크가 안 맞는 시점이 존재하게 된다.

export function useTodos() {
  const { data: todos } = useQuery({
    queryKey: ['todos', 'list'],
    queryFn: fetchTodos,
  })

  const todoCount = todos?.length ?? 0

  return { todos, todoCount }
}

해결방법은 그냥 state를 안 쓰면 된다. data가 변경될 때마다 useTodos 가 계속 재실행될 것이므로 굳이 state를 필요가 없는 것이다.

싱크가 안 맞는 순간이 생기는 경우에 생길 잠재적 오류의 가능성이 있기 때문에 쓰지 말라고 tkdodo가 여러 번 얘기한 것 같지만.. 아무래도 API 가 존재하기 때문에 쓰지 말라고 해도 계속 비슷한 사례가 발생하는 것 같다.

3. 또 다른 store 와 싱크가 안 맞음

redux 등 다른 store 를 추가로 사용하는 경우에 onSuccess 에서 싱크를 맞추고 싶을 수도 있다.

export function useTodos(filters) {
  const { dispatch } = useDispatch()

  return useQuery({
    queryKey: ['todos', 'list', { filters }],
    queryFn: () => fetchTodos(filters),
    staleTime: 2 * 60 * 1000,
    onSuccess: (data) => {
      dispatch(setTodos(data))
    },
  })
}

하지만 만약 cache값이 남아있어서 fetch 를 하지 않았지만 data의 값이 바뀌었다면? onSuccess 를 호출하지 않으므로 useQuery의 반환값과 타 스토어에서 가지고 있는 값이 싱크가 안 맞게 된다.

export function useTodos(filters) {
  const { dispatch } = useDispatch()

  const query = useQuery({
    queryKey: ['todos', 'list', { filters }],
    queryFn: () => fetchTodos(filters),
    staleTime: 2 * 60 * 1000,
  })

  React.useEffect(() => {
    if (query.data) {
      dispatch(setTodos(query.data))
    }
  }, [query.data])

  return query
}

data가 바뀔 때마다 스토어의 값을 업데이트 해주어야 하므로 useEffect 에서 처리하는 것이 옳다고 한다.

물론 좋게 사용한 예시도 있긴 했지만 극히 드물고 위의 anti-pattern 을 만들 위험이 커서 없앴다고 한다.

api A의 response 결과로 api B 의 request 를 해야 하는 경우

이미 공식문서에 enabled 속성을 사용해서 구현하라고 나와있다.

Dependent Queries | TanStack Query Docs

궁금증

  1. axiosInstance 가 왜 여러 번 호출되는 것일까? useQuery 가 호출될 때마다 인스턴스가 생성되는 것 같은데 이 때 만들어지는 queryInstance 가 re-render 시에 안 사라지는 것인가..?
  2. axiosError 가 있고, 가끔 axiosResponse 형태로 내려오는 것이 있다. 왜 차이가 생길까?

참고

RFC: remove callbacks from useQuery · TanStack/query · Discussion #5279

Breaking React Query's API on purpose

React Query Error Handling

profile
프론트엔드에 백엔드 한 스푼 🥄

3개의 댓글

comment-user-thumbnail
2023년 7월 18일

많은 도움이 되었습니다, 감사합니다.

1개의 답글
comment-user-thumbnail
2024년 7월 4일

Thanks for info :)

답글 달기