프론트엔드/React.js

Optimistic Updates

Jay_Jung 2024. 12. 26. 12:16

 

<참고 레퍼런스>

https://react-query.kro.kr/docs/guides-and-concepts/optimistic-updates

 

Optimistic Updates – React Query 한글 문서

Nextra: the next docs builder

react-query.kro.kr

 

이번에 정리해볼 내용은 tanstack-query 개념 중 하나인 Optimisitc Update에 대해서 정리하고자 한다. 공식문서 챕터를 읽어보면서 내가 진행하고 있는 프로젝트에 적용해볼 좋은 기술임을 느꼈고 이번 기회를 통해서 정리하게 되었다. 나는 사용자 경험 개선에 관심이 많기 때문에 해당 내용이 재밌게 술술 읽혔다.

 

개념: '낙관적 업데이트' 라고 하며 서버의 실제 응답을 기다리지 않고, 예상되는 결과를 미리 UI에 반영하여 사용자 경험을 향상시키는 기법이다.

 

이를 통해 개발자는 사용자에게 즉각적인 피드백을 제공할 수 있고 애플리케이션의 응답성을 높일 수 있다.

 

 

<낙관적 업데이트 구현 방법>

 

1. UI를 통한 업데이트

 

✅ 캐시를 직접 조작하지 않고, 컴포넌트의 상태를 활용하여 UI를 업데이트 한다.

✅ useMutation 훅을 사용하여 뮤테이션을 정의하고, onSettled 콜백에서 쿼리를 무효화하여 최신 데이터를 가져온다
-> onSettled의 반환값은 Promise여야 한다.

뮤테이션의 상태(isPending, isError)와 변수(variables)를 활용하여 UI를 조건부 렌더링 한다.

 

const addTodoMutation = useMutation({
  mutationFn: (newTodo) => axios.post("/api/data", { text: newTodo }),
  onSettled: async () => {
    return await queryClient.invalidateQueries({ queryKey: ["todos"] });
  },
});

const { isPending, variables, mutate, isError } = addTodoMutation;

return ( -> isPending, isError에 따른 조건부 렌더링
  <ul>
    {todoQuery.items.map((todo) => (
      <li key={todo.id}>{todo.text}</li>
    ))}
    {isPending && <li style={{ opacity: 0.5 }}>{variables}</li>}
    {isError && (
      <li style={{ color: "red" }}>
        {variables}
        <button onClick={() => mutate(variables)}>Retry</button>
      </li>
    )}
  </ul>
);

 

 

2. 캐시를 통한 업데이트

 

✅ onMutate 콜백을 사용하여 캐시를 직접 업데이트 한다.(직접 캐시를 관리한다는 점에서 강력하지만 잘못 관리하면 데이터 불일치 문제가 발생하므로 신중하게 확인해야 할듯)

✅ 뮤테이션이 시작되기 전에 캐시를 업데이트(onMutate)하고, onError와 onSettled 콜백에서 캐시를 복원하거나 최신 상태로 동기화

 

const addTodoMutation = useMutation({
  mutationFn: (newTodo) => axios.post("/api/data", { text: newTodo }),
  onMutate: async (newTodo) => {
    await queryClient.cancelQueries({ queryKey: ["todos"] });
    const previousTodos = queryClient.getQueryData(["todos"]);
    queryClient.setQueryData(["todos"], (old) => [...old, newTodo]);
    return { previousTodos };
  },
  onError: (err, newTodo, context) => {
    queryClient.setQueryData(["todos"], context.previousTodos);
  },
  onSettled: () => {
    queryClient.invalidateQueries({ queryKey: ["todos"] });
  },
});

 

<동작단위 구현 개념>

onMutate : 뮤테이션이 시작되기 직전에 호출되는 비동기 함수로, 낙관적 업데이트를 처리하는 데 사용된다.

await queryClient.cancelQueries({ queryKey: ["todos"] }) : 해당 코드를 통해 현재 진행 중인 "todos" 쿼리를 취소하여, 낙관적 업데이트가 기존의 리패치로 인해 덮어쓰여지지 않도록 구현

 

const previousTodos = queryClient.getQueryData(["todos"]) : 현재 캐시에 저장된 "todos" 데이터를 가져와 previousTodos에 저장하게 되고 이는 언제 활용하느냐?

-> 오류 발생 시 데이터를 롤백 할 때 활용

 

return { previousTodos } : return 값을 컨텍스트 객체로 구성하여 반환하고 이를 onError, onSettled에서 활용

 

queryClient.setQueryData(["todos"], context.previousTodos) : 해당 코드를 통해 onMutate에서 저장한 이전 할 일 목록(context.previousTodos)으로 캐시를 복원하여, 오류로 인해 발생한 불일치를 해소할 수 있음!

 

queryClient.invalidateQueries({ queryKey: ["todos"] }) : "todos" 쿼리를 무효화하여, 서버의 최신 상태와 동기화되도록 새로운 데이터를 가져온다.

-> onSettled 함수는 뮤테이션의 성공/실패 상관없이 완료되면 호출되는 함수이다. 

 

 

<어떠한 상황에서 어떤 개념을 써야할까>

단순한 경우에는 UI를 통한 업데이트를, 복잡한 상태 관리가 필요한 경우에는 캐시를 통한 업데이트를 사용하는 것이 일반적이라고 한다.

 

UI 업데이트 캐시 업데이트
특정 컴포넌트 내에서만
데이터 변경이 이루어지는 경우에 적합
여러 컴포넌트에서 동일한 데이터를 공유하거나, 뮤테이션과 쿼리가 다른 컴포넌트에 있을 때 효과적