Trace 독서 활동 SNS 만들기 - 5

디듀·2026년 2월 16일

낙관적 업데이트라는 개념은 tanstack-query를 학습하기 전부터 익히 들어왔던 내용이다. 특히 전 회사에서 SNS 프로젝트 개발에 참여했을 때, 좋아요 기능을 작업하는 과정에서 직접 구현했던 기억이 난다. 그때는 아직 경력이 1년도 안 되었을 시점이었고 tanstack-query를 사용하지 않았으며, 낙관적 업데이트라는 정확한 명칭에 대해 알지는 못 했지만 효용에 대해서는 확실히 학습했었던 것 같다.

낙관적 업데이트

낙관적 업데이트란 웹 애플리케이션에서 사용자의 경혐을 향상시키기 위해 등장한 개념이다. 기존의 웹 애플리케이션에서 UI를 업데이트 하려면 '서버에 API를 요청하고, 요청한 것에 대한 응답이 돌아올 때까지 기다린 뒤 돌아온 값을 화면에 반영'하는 과정을 거쳐야만 했다.

이렇게 되면 사용자가 어떤 버튼을 클릭했을 때, 해당하는 행동에 대한 반응이 화면에서 즉각적으로 나타나지 않기 때문에 사용자 경험이 저하되는 결과를 가져올 수가 있다. 예를 들어 어떤 포스트에 좋아요를 눌렀을 때 응답이 돌아오기까지 약 1초의 시간이 소요된다면 사용자의 입장에서는 1초만큼의 딜레이가 발생한 것처럼 느껴질 수도 있고, 그 사이에 여러 번 버튼을 눌러 불필요한 API 호출까지 발생할 수 있다. 물론 이 딜레이는 서버에서 돌아오는 응답이 느리면 느릴수록 더 심하게 체감된다.

이러한 문제를 해결하기 위해 등장한 개념이 '낙관적 업데이트'다.

낙관적 업데이트란?

낙관적 업데이트란, 사용자의 동작에 대한 응답이 도착하기 전 UI 레벨에서 먼저 상태를 업데이트하는 기법을 의미한다. 즉 기존의 단계에서 '서버에 API를 요청하고, 요청한 것에 대한 응답이 돌아올 때까지 기다린다'는 단계를 획기적으로 축소시킨 것이다.

조금 전과 같은 예시로 사용자가 어떤 포스트에 좋아요를 눌렀을 때, 아직 좋아요를 누르지 않은 상태라면 좋아요를 누른 상태로 변경하고 그것을 UI에 즉시 반영한다. 이후에 서버에 요청한 것에 대한 응답값이 돌아오면, 응답 상태에 따라서 실제 서버 데이터를 반영하면 된다. (e.g. 오류가 발생했을 경우 업데이트 전의 상대로 다시 변경)

이렇게 함으로써 사용자는 웹 애플리케이션이 자신의 행위에 대해서 더욱 빠르게 대응하는 것처럼 느낄 수 있다. 물론, 상황에 따라 낙관적 업데이트를 적용해야 하는 기능과 그렇지 않은 기능이 존재할 수 있다. 이때는 로딩 후 서버 응답 값에 따라 화면의 상태를 변경하는 것이 좋다.

낙관적 업데이트를 적용하는 것이 좋은 경우

  1. 성공 확률이 매우 높은 작업
  • 네트워크 실패나 권한 실패가 드문 경우, 즉 실패 자체가 예외 케이스인 경우에는 낙관적 업데이트를 활용한 이득이 더 크다고 볼 수 있다.

    e.g. 좋아요, 북마크, 팔로우/언팔로우 등

  1. 결과를 클라이언트가 완전히 예측 가능한 경우
  • 서버가 별도 계산/변형을 하지 않고 클라이언트에서 결과값을 예측할 수 있는 경우에도 낙관적 업데이트를 적용할 수 있다.

    e.g. 카운트 증가/감소, 리스트에 항목 추가/제거, 상태 플래그 변경 등

  1. 즉각적인 반응이 UX에 중요한 경우
  • 즉각적으로 UI의 상태 변경이 반영될 때, 사용자가 체감하는 성능이 개선될 경우 낙관적 업데이트를 적용할 수 있다.

    e.g. 피드/SNS, 드로잉/편집 툴, 쇼핑몰의 장바구니 등

낙관적 업데이트를 적용하지 않는 것이 좋은 경우

  1. 실패 가능성이 의미 있게 높은 작업
  • 낙관적 업데이트의 전제는 "대부분 성공한다"는 것으로, 실패율이 높으면 UI 롤백이 빈번해지고 사용자는 시스템을 불신하게 된다.

    e.g. 결제 요청, 재고 차감, 권한 검증이 복잡한 작업 등

  1. 롤백 비용이 큰 작업
  • 롤백이 기술적으로 가능하더라도 인지적 비용이 크면 좋지 않다.
  • 이 경우 사용자는 자신이 했던 복잡한 작업이 갑작스럽게 사라진 것으로 인식할 수 있다.

    e.g. 긴 텍스트 작성 후 저장 실패, 복잡한 폼 제출, 대규모 리스트 재정렬, 드래그 기반 편집 등

  1. 사용자 인지가 중요한 액션
  • '실제로 성공했는지' 여부가 중요한 작업의 경우에도 사용자가 혼란스러움을 느낄 수 있다.

    e.g. 삭제, 결제, 예약, 제출 등

낙관적 업데이트 적용

SNS 프로젝트인 Trace에는 당연하게도(?) 좋아요 기능이 구현되어 있다. tanstack-query를 통해 좋아요 기능에 낙관적 업데이트를 적용하는 방법을 알아보자!

아래는 좋아요 토글 함수에 할당된 useMutation hook의 전문이다.

/** -----------------------------
 * @description 포스트 좋아요 토글 뮤테이션
 * @param callbacks 콜백
 * @returns 포스트 좋아요 토글 뮤테이션
 * ----------------------------- */
export const useTogglePostLike = (callbacks?: UseMutationCallback) => {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: togglePostLike,
    onSuccess: () => {
      callbacks?.onSuccess?.();
    },
    onMutate: async ({ postId }) => {
      callbacks?.onMutate?.();

      await queryClient.cancelQueries({
        queryKey: QUERY_KEYS.post.byId(postId),
      });

      // * 이전 포스트 데이터 조회
      const prevPost = queryClient.getQueryData<Post>(
        QUERY_KEYS.post.byId(postId)
      );

      // * 포스트 데이터 업데이트
      queryClient.setQueryData<Post>(QUERY_KEYS.post.byId(postId), (post) => {
        if (!post) throw new Error("포스트가 존재하지 않습니다.");

        return {
          ...post,
          isLiked: !post.isLiked,
          like_count: post.isLiked ? post.like_count - 1 : post.like_count + 1,
        };
      });

      return { prevPost };
    },
    onError: (error, _, context) => {
      callbacks?.onError?.(error);

      // * 에러 발생 시 이전 포스트 데이터 복구
      if (context && context.prevPost) {
        queryClient.setQueryData(
          QUERY_KEYS.post.byId(context.prevPost.id),
          context.prevPost
        );
      }
      if (callbacks?.onError) callbacks.onError(error);
    },
    onSettled: () => {
      callbacks?.onSettled?.();
    },
  });
};

1. 이전 요청 취소

만약 이전에 포스트 상세 정보에 대해 요청한 쿼리가 있을 경우, 낙관적 업데이트 내용이 이전 데이터로 덮어씌워질 수 있으므로 취소한다.

await queryClient.cancelQueries({
  queryKey: QUERY_KEYS.post.byId(postId),
});

2. 업데이트 실패에 대비하여 이전 쿼리 데이터 저장

낙관적 업데이트를 시도하는 도중 여러 가지 이유로 실패하거나 다른 처리를 해주어야 할 때, 이전 쿼리 데이터를 사용할 수 있도록 따로 저장해 놓는다. 해당 데이터는 onMutate 콜백 함수의 마지막에서 return 해준다. (onError에서 사용할 수 있도록)

const prevPost = queryClient.getQueryData<Post>(
  QUERY_KEYS.post.byId(postId)
);

3. 쿼리 데이터 업데이트

미리 적용한 쿼리 키를 기반으로 해서, 업데이트한 포스트와 id가 같은 포스트의 좋아요 수를 변경한다. isLiked는 '내가 해당 포스트에 좋아요를 눌렀는가'를 나타내는 지표로, 현재의 값과 반대되는 값으로 업데이트하면 된다. 마찬가지로 현재 post.isLiked === true라면 이미 좋아요를 누른 상태에서 좋아요를 취소하기 위해 버튼을 클릭했다는 뜻이므로 카운트를 감소시키고, 반대의 경우 카운트를 증가시킨다.

queryClient.setQueryData<Post>(QUERY_KEYS.post.byId(postId), (post) => {
  if (!post) throw new Error("포스트가 존재하지 않습니다.");

  return {
    ...post,
    isLiked: !post.isLiked,
    like_count: post.isLiked ? post.like_count - 1 : post.like_count + 1,
  };
});

4. 에러 처리

onMutate에서 return한 prevPost 값은 onError에서 context로 넘겨받아 사용할 수 있다. 혹시 예기치 못한 에러가 발생했을 경우 저장해 두었던 값으로 롤백시킨다.

onError: (error, _, context) => {
  callbacks?.onError?.(error);

  // * 에러 발생 시 이전 포스트 데이터 복구
  if (context && context.prevPost) {
    queryClient.setQueryData(
      QUERY_KEYS.post.byId(context.prevPost.id),
      context.prevPost
    );
  }
  if (callbacks?.onError) callbacks.onError(error);
}

5. 확인!

아래 이미지와 같이 즉각적으로 좋아요 버튼이 반응하는 것을 확인할 수 있다!


언뜻 보면 복잡하다고 생각될 수 있지만, tanstack-query에서는 직접적으로 쿼리 데이터를 수정하는 경우가 (낙관적 업데이트를 제외하더라도) 생각보다 많기 때문에 자주 사용하면 금방 익숙해질 수 있는 스킬 같다.

앞으로도 열심히 연습 또 연습!

profile
세상에서 가장 부지런한 사람이 되고 싶은 게으름뱅이

0개의 댓글