낙관적 업데이트 UX 관점에서 바라보기

2025-09-13

유저의 인터랙션에 의한 UI 변경이 일어날 때 서버의 요청에 의해 반영이 되기까지 시간을 기다리지 않고 유저가 먼저 UI 결과를 먼저 본다고 가정하고 상태를 반영하는 전략이다.

대표적인 예시로 좋아요 버튼을 누르면 바로 숫자가 올라가고, 나중에 서버에 요청이 성공하면 그대로 상태를 유지, 실패하면 롤백하는 경우가 있을 것 같다.

좋아요 버튼을 누르고 서버의 응답이 1s 걸린다면? 유저는 서비스에 답답함을 느낄 것이고 좋지 않은 경험을 제공하게 된다.

그럼 낙관점 업데이트는 항상 좋은가?


낙관적 업데이트를 고려할 때 사용성에 대해 생각해보자

1. 응답이 늦어 상태가 늦게 변경되는 경우
2. 낙관적 업데이트를 했으나 API 요청에 실패해서 다시 상태를 되돌려야 하는 경우

어떤 것이 더 사용성이 안 좋을까?


1️⃣ 응답이 늦는 경우

버튼을 눌러도 즉각 UI가 변경되지 않고 시간이 흐른 뒤에 UI가 변경된다.

→ 유저가 웹페이지가 느리다고 체감하고 좋지 않은 경험


2️⃣ 낙관적 업데이트를 했으나 API 요청이 실패해서 롤백하는 경우

좋아요를 눌렀는데 취소 되어 유저에게 혼란을 줄 수 있고 서비스의 불신도 줄 수 있다.


사용자 관점에서 더 나쁜 것은?

→ 실패 후 롤백

이미 유저의 눈 앞에서 UI가 변경되었지만 실패해서 다시 원래대로 UI가 바뀐다면

유저는 “버그인가?”라고 느낄 수 있다.

특히 댓글일 경우 다시 작성해야한다는 짜증 유발

많은 유저들은 0.5 ~ 1초 늦는 것 보다는 UI가 바뀌었다가 돌아오는 것에 더 민감하게 반응할 것이다.


🔎 하지만 낙관적 업데이트 없는 지연 업데이트의 경우에도 API가 실패할 경우가 있지않나?

지연 업데이트에서 API가 실패할 경우 좋아요 버튼을 눌렀을 땐 아무 반응이 없게 될 것이다

버튼 눌렀는데 반응이 없는 경우 보다 눌렀더니 좋아요가 눌러졌는데 다시 돌아가는 경우가 훨씬 더 큰 혼란을 줄 수 있다.


🤔 그렇다면 낙관적 업데이트는 실패할 가능성 때문에 의미가 없는가?

그렇지는 않다고 생각한다

좋아요, 북마크, 장바구니 추가와 같은 대부분의 요청에는 성공할 것이고 실패해도 유저가 큰 부담을 느끼지 않을 것이다

다시 좋아요 버튼을 누르면 되고, 장바구니 담기를 누르면 된다.


🚀 즉각적인 반응이 UX에 더 중요하다

서버의 응답을 기다린다면 0.5s ~ 1s 정도 기다리게 될 것이며 이는 느린 UX를 제공하게 된다.

낙관적 업데이트는 즉각적인 피드백을 주기 때문에 UX에 필수적인 요소이다

특히 모바일 환경에서는 네트워크 지연이 더 심하기 때문에 낙관적 UI가 더 체감이 좋다고 판단된다.


생각해보자

위에서 예시를 둔 좋아요, 장바구니 담기와 같은 일반적인 UI 변경에 경우는 실패하더라도 유저가 큰 부담을 느끼지 않는다

그냥 버튼을 다시 누르면 된다

그럼 어떤 경우에 유저가 부담을 느끼고 짜증을 느낄까?


댓글 작성, 게시물 업로드 등 사용자의 입력에 기반한 데이터 변경 UI

유저가 직접 내용을 다 입력하고 작성을 눌렀는데 서버에서 validation이 실패하거나, 이미지 업로드가 실패했다거나 네트워크등 여러 이유로 UI가 롤백이 되어 다시 작성하게 된다면?

주문 버튼과 같이 실패 할 경우 사용자 입장에서 불안감을 주는 경우

유저가 주문하기 버튼을 누르고 나서 “주문 완료!” 라는 UI가 낙관적으로 업데이트 되었다가 API 실패로 롤백이 되었다면? 유저는 주문이 성공한 것인지, 실패한 것인지 헷갈리고 혼란을 야기한다.

이렇게 유저에게 많은 혼란을 주는 경우에는 낙관적 업데이트보다 일반적인 서버로 요청 이후의 업데이트가 적절하다고 판단된다.


그럼 어떻게 해야할까?

즉각적인 UI 변경을 제공하기 위해 낙관적 업데이트는 특수한 경우를 제외하고는 적용하는 것이 UX에 좋다.

그럼 낙관적 업데이트 후 API 요청에 실패할 경우에는 어떻게 대처해야 할까?

실패했을 떄의 처리 전략이 있어야 한다.

낙관적 업데이트는 미리 UI를 업데이트 하고 서버에게 요청을 보내는데 서버로 보내는 이 때 만약 실패 할 경우 롤백과 함께 유저에게 토스트 팝업, 모달 등을 활용하여 에러가 발생했다는 메세지를 알려주어야 한다.

GPT가 만들어 준 아주 간단한 예시

import { useMutation, useQueryClient } from "@tanstack/react-query"
import { toast } from "./toast"

type Post = {
  id: string
  liked: boolean
}

const likePost = async (postId: string) => {
  const res = await fetch(`/api/posts/${postId}/like`, { method: "POST" })
  if (!res.ok) throw new Error("좋아요 실패")
  return res.json()
}

const useLikePost = (postId: string) => {
  const queryClient = useQueryClient()

  return useMutation({
    mutationFn: () => likePost(postId),

    // 낙관적 업데이트
    onMutate: async () => {
      await queryClient.cancelQueries({ queryKey: ["post", postId] })

      const prevPost = queryClient.getQueryData<Post>(["post", postId])

      // UI 상 liked를 true로 낙관적 업데이트
      queryClient.setQueryData<Post>(["post", postId], (old) =>
        old ? { ...old, liked: true } : old
      )

      // 실패했을 때 되돌리기 위한 이전 데이터 저장
      return { prevPost }
    },

    // 실패했을 때 롤백
    onError: (_err, _variables, context) => {
      if (context?.prevPost) {
        queryClient.setQueryData(["post", postId], context.prevPost)
      }
      toast.error("좋아요 처리에 실패했어요 😢")
    },

    // 성공/실패 관계 없이 캐시 리패치
    onSettled: () => {
      queryClient.invalidateQueries({ queryKey: ["post", postId] })
    },
  })
}


반드시 실패를 대비한 로직을 추가해주자 낙관적 업데이트는 빠르고 부드러운 사용자 경험을 위해 매우 유용한 전략이다. 그러나 항상 "성공이 전제되어도 괜찮은가?"라는 질문을 먼저 던져보아야 한다 사용자에게 혼란을 주지 않기 위해선, 빠른 UI와 안정된 결과 사이에서 균형 잡힌 판단이 필요한 것 같다.

© 2024 SongChangYeop All rights reserved