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

오다혜·2023년 7월 17일

문제

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 :)

답글 달기