2025 . 오은

repo

tanstack-query optimistic

문제 정의

좋아요 기능 구현 중에 서버 상태와 로컬 상태가 동기화 되는데에 항상 일정 시간이 필요함을 알게 되었습니다. 좋아요 버튼 클릭시 바로 반영되게 해보았지만, 이렇게 할 경우 중간에 문제가 생기거나 하여 서버에 반영되지 않았을 경우의 처리가 어려웠습니다. 이러한 문제를 해결하기 위해 기존에는 매번 상태를 수동으로 관리하려 했지만, 이는 코드의 복잡성을 증가시키고 유지보수를 어렵게 만드는 요인이 되었습니다.

해결 과정

이러한 상황에 대처하는 패턴이 존재했고, 이를 낙관적 업데이트(optimistic update)라고 한다는 것을 알게 되었습니다. tanstack-query는 이런 optimistic update를 비교적 쉽게 구현하게 해주고 있었습니다. 구현 방식은 유저가 좋아요 등을 클릭하면 서버에서 응답이 오기 전에 우선 낙관적으로 판단하여 UI 부터 업데이트 한 뒤, 만약 실패했다면 다시 롤백시키는 방법이었습니다.

  1. mutationFn:

    • mutationFn은 뮤테이션을 수행하는 비동기 함수입니다. 여기서는 addTodo 함수가 이 역할을 합니다.
  2. onMutate:

    • onMutate는 뮤테이션이 시작될 때 호출됩니다.
    • 서버에 요청을 보내기 전에 로컬 상태를 업데이트하여 응답 지연을 사용자에게 숨기기 위한 낙관적 업데이트(Optimistic Update)를 수행합니다.
    • 쿼리를 취소하여 경쟁 상태를 방지하고, 현재의 todos 상태를 가져와 이전 상태로 되돌릴 수 있도록 저장합니다.
    • queryClient.setQueryData를 사용하여 로컬 상태를 즉시 업데이트합니다.
    • 반환된 객체(previousTodos)는 onError 또는 onSettled에서 사용할 수 있습니다.
  3. onError:

    • 뮤테이션이 실패했을 때 호출됩니다.
    • 낙관적 업데이트로 인해 변경된 로컬 상태를 이전 상태로 복원합니다.
    • context 매개변수는 onMutate에서 반환된 값을 포함합니다.
  4. onSettled:

    • 뮤테이션이 성공하든 실패하든 상관없이 뮤테이션이 끝나면 호출됩니다.
    • invalidateQueries를 사용하여 todos 쿼리를 무효화하여, 서버에서 최신 데이터를 다시 가져오도록 합니다.

예시 코드

const addMutation = useMutation({
    mutationFn: addTodo, // 실제 변이 함수
    onMutate: async (newTodo) => {
        console.log("onMutate 호출");
        await queryClient.cancelQueries({ queryKey: ["todos"] }); // 쿼리 취소
        const previousTodos = queryClient.getQueryData(["todos"]); // 현재 상태 저장
        queryClient.setQueryData(["todos"], (old) => [...old, newTodo]); // 낙관적 업뎃
        return { previousTodos }; // 이전 상태 반환
    },
    onError: (err, newTodo, context) => {
        console.log("onError");
        console.log("context:", context);
        queryClient.setQueryData(["todos"], context.previousTodos); // 오류 시 이전 상태 복원
    },
    onSettled: () => {
        console.log("onSettled");
        queryClient.invalidateQueries({ queryKey: ["todos"] }); // 변이 후 쿼리 무효화
    },
});

결과

  • 사용자 경험 개선: 바로 반영되기를 기대하는 동작에서 실제로 UI를 바로 반영 상태로 우선적으로 변경함으로서 사용자에게 더 자연스러운 경험을 제공할 수 있었습니다.
  • 상태 관리 간소화: 이러한 패턴을 사용하기 전에는 복잡한 방법으로 상태를 우선 반영시켰었는데, useMutation 안에서 모두 처리됨으로써 상태 관리 방법이 획기적으로 간결해졌습니다.
  • 데이터 일관성 유지: 서버 요청이 실패할 경우 이전 상태로 롤백하여 클라이언트와 서버 간의 데이터 일관성을 유지할 수 있었습니다.
  • 에러 처리 효율화: onError 핸들러를 통해 오류 발생 시 자동으로 상태를 복원함으로써, 에러 처리가 일관되고 효율적으로 이루어졌습니다.
  • 쿼리 최적화: onSettled에서 쿼리를 무효화하여 최신 데이터를 다시 패칭함으로써, 항상 최신 상태의 데이터를 유지할 수 있었습니다.

배운 점 (인사이트)

  • 낙관적 업데이트의 강점과 한계:

    • 강점: 사용자 경험을 크게 향상시킬 수 있는 강력한 도구임을 배웠습니다. UI가 즉시 반영되어 사용자에게 빠른 피드백을 제공할 수 있습니다.
    • 한계: 모든 상황에서 낙관적 업데이트가 적합한 것은 아닙니다. 예를 들어, 데이터의 일관성이 매우 중요한 경우나 변이가 빈번히 실패할 가능성이 높은 경우에는 신중하게 적용해야 합니다. 잘못된 낙관적 업데이트는 데이터 불일치를 초래할 수 있습니다.
  • TanStack Query의 유연한 상태 관리:

    • useMutation 훅을 활용하여 복잡한 상태 관리 로직을 간단하게 처리할 수 있었습니다. 특히, onMutate, onError, onSettled 등의 생명주기 메서드를 통해 변이의 다양한 단계를 효과적으로 관리할 수 있었습니다.
  • 에러 핸들링의 중요성:

    • 낙관적 업데이트를 사용할 때, 서버 요청이 실패할 경우의 에러 핸들링이 매우 중요함을 배웠습니다. onError 메서드를 통해 이전 상태로 롤백함으로써 데이터 일관성을 유지할 수 있었습니다.
  • 데이터 일관성과 캐싱 관리:

    • 쿼리 캐시를 적절히 무효화하고, 필요한 경우 다시 패칭함으로써 데이터 일관성을 유지할 수 있음을 깨달았습니다. 이를 통해 최신 데이터를 보장하고, 캐싱된 데이터의 신뢰성을 높일 수 있었습니다.