[Tanstack Query] 3. 데이터 변경하기와 옵티미스틱 업데이트

임승민·2025년 2월 16일
0

Tanstack Query

목록 보기
3/9
post-thumbnail

useQuery는 데이터를 읽어오는 용도, useMutation은 데이터를 변경하는 용도이다.

  • useMutation을 사용해 데이터를 추가,수정,삭제를 할 수 있다.

1. Mutation 기본 개념

mutationFn: 실행할 API 요청 (POST, PUT, DELETE)
onSuccess: 요청 성공 시 실행할 로직
onError: 요청 실패 시 실행할 로직

2. 기본 사용

const addPost = async (newPost) => {
  const { data } = await axios.post(`${api_url}/posts`, newPost);
  return data;
};

const mutation = useMutation({
  mutationFn: addPost,
  onSuccess: (data) => {
    console.log("✅ 게시글 추가 성공:", data);
  },
  onError: (error) => {
    console.error("❌ 게시글 추가 실패:", error);
  },
});

// 버튼 클릭 시 Mutation 실행 
const addPost = () => {
  mutation.mutate(newPost);
};

3. Mutation 후 데이터 갱신

데이터를 추가/수정/삭제 했다면 기존 데이터를 최신 상태로 유지해야 한다.

queryClient.invalidateQueries()를 사용해 자동으로 쿼리를 재실행해 최신 데이터를 유지한다.

const queryClient = useQueryClient();
const mutation = useMutation({
  mutationFn: addPost,
  onSuccess: () => {
    queryClient.invalidateQueries({queryKey: ["posts"]}); // 게시글 목록 갱신
  },
});
  • invalidateQueries:  v5에서는 객체 형태로 전달해야 함

결과 예상

  1. 새 게시글이 추가되면 ["posts"] 키를 가진 쿼리를 무효화 (invalidateQueries)
  2. 기존 게시글 목록이 자동으로 최신 데이터로 갱신된다.

쿼리 무효화: 특정 쿼리의 캐시가 유효하지 않다고 선언하는 것

무효화된 쿼리는 다음에 자동으로 최신 데이터를 가져오게 된다.

4.  옵티미스틱 업데이트

서버 응답을 기다리지 않고, UI를 먼저 업데이트하는 걸 옵티미스틱 업데이트라고 한다.

이를 통해 더 나은 UX를 제공할 수 있다. 하지만 관리가 어렵기 때문에  UI 반응성을 높여야 할 곳 일부에 사용하는 것이 좋다.

queryClient.setQueryData: 쿼리의 캐시 데이터를 직접 수정하는 함수 (옵티미스틱 업데이트에 필요)

onMutate: Mutation이 실행되기 직전에 호출되는 함수 (옵티미스틱 업데이트에 필요)

onSettled: Mutation이 성공/실패하든, 요청이 끝나면 실행되는 함수

const mutation = useMutation({
  mutationFn: deletePost,
  onMutate: async (postId) => {
    // 기존 데이터 백업
    const prevPosts = queryClient.getQueryData(["posts"]);

    // UI에서 삭제된 것처럼 보이게 처리
    queryClient.setQueryData(["posts"], (oldPosts) =>
      oldPosts.filter((post) => post.id !== postId)
    );
    
    return { prevPosts }; // 에러 발생 시 기존 상태로 롤백
  },
  onError: (error, postId, context) => {
	  // 기존 상태로 복구
	  queryClient.setQueryData(["posts"], context.prevPosts);
  },
  onSettled: () => {
    // 서버와 동기화 (쿼리 무효화)
    queryClient.invalidateQueries(["posts"]);
  },
});

const handleDelete = (postId) => {
  mutation.mutate(postId);
};

결과 예상

  1. 삭제 버튼 클릭 시 UI에서 먼저 삭제됨
  2. 실제 API 요청을 보냄
  3. 요청이 실패하면 기존 데이터로 복구
  4. 요청이 끝나면 쿼리 무효화

5. Mutation 상태 관리 (isLoading, isSuccess, isError)

Mutation 사용 시 상태도 함께 사용하면 더 좋은 UX를 제공할 수 있다.

<script setup>
const updatePost = async ({ id, title }) => {
  const { data } = await axios.put(`${api_url}/posts/${id}`, { title });
  return data;
};

const mutation = useMutation(updatePost);
const editPost = () => {
  mutation.mutate({ id: 1, title: "업데이트된 제목" });
};
</script>

<template>
  <div>
    <button @click="editPost">게시글 수정</button>
    <p v-if="mutation.isLoading">수정 중...</p>
    <p v-else-if="mutation.isError">오류 발생: {{ mutation.error.message }}</p>
    <p v-else-if="mutation.isSuccess">수정 완료</p>
  </div>
</template>

정리

useMutation을 사용하면 데이터를 추가/수정/삭제 가능

invalidateQueries를 사용하면 데이터 변경 후 최신 상태 유지 가능

옵티미스틱 업데이트를 적용하면 UI를 빠르게 반영 가능

isLoading, isError, isSuccess를 활용하면 UX 개선 가능


궁금

context.previousPosts 활용 VS oldUsers 그대로 반환

context.previousPosts는 저장해뒀던 값을 다시 설정하는 거니까 이해가 간다. 하지만 oldUsers도 users쿼리의 현재 변경된 값의 이전 값이니까 반환하면 첫번째 방식이랑 같다고 생각했다.

그런데 oldUsers는 변경 전 데이터가 아니라, 이미 변경된 캐시 데이터라고 한다.

onError: (error, postId, context) => {
  queryClient.setQueryData(["posts"], context.previousPosts);
},
onError: () => {
  queryClient.setQueryData(["users"], (oldUsers: User[]) => {
    return oldUsers;
  });
},

0개의 댓글

관련 채용 정보