frontend

낙관적 업데이트(Optimistic Updates) 가이드: TanStack Query로 구현하는 초고속 사용자 경험

사용자는 기다려주지 않습니다. 버튼을 누르고 0.5초의 로딩 바를 보는 것과, 누르는 즉시 결과가 화면에 반영되는 것은 완전히 다른 서비스 경험을 제공합니다. 오늘은 현대 웹 개발에서 사용자 경험(UX)을 극대화하는 강력한 전략인 **낙관적 업데이트(Optimistic Updates)**에 대해 심도 있게 다루어 보겠습니다.

목차

  1. 낙관적 업데이트란 무엇인가?
  2. 왜 비효율을 감수하고 사용하는가?
  3. TanStack Query를 활용한 구현 메커니즘
  4. 실전 예제: 할 일 목록(Todo) 추가 로직
  5. 주의사항 및 트러블슈팅
  6. 자주 묻는 질문(FAQ)

낙관적 업데이트란 무엇인가?

**낙관적 업데이트(Optimistic Updates)**는 서버의 응답이 “성공할 것이라고 낙관적으로 가정”하고, 클라이언트의 UI를 먼저 업데이트하는 기법입니다. 일반적인 요청 흐름이 요청 -> 대기 -> 응답 -> 반영 순서라면, 낙관적 업데이트는 요청 + 반영 -> 대기 -> 응답 확인(확정 혹은 롤백)의 순서를 따릅니다.

낙관적 업데이트의 시퀀스 다이어그램: 성공과 실패 시의 흐름 비교

이 방식은 특히 네트워크 지연(Latency)이 발생하는 환경에서 앱이 마치 로컬 소프트웨어처럼 팽팽하게 돌아가는 느낌을 줍니다. 물론, 단순히 화면만 바꾸는 것이 아니라 실패했을 때를 대비한 보험(Rollback) 설계가 필수적입니다.


왜 비효율을 감수하고 사용하는가?

개발자의 관점에서 보면 낙관적 업데이트는 다소 “지분거리는” 코드를 양산합니다. 서버 응답이 오기도 전에 가짜 데이터를 넣어야 하고, 실패하면 다시 되돌리는 로직을 짜야 하니까요. 하지만 그럼에도 불구하고 실무에서 이를 도입하는 이유는 명확합니다.

  1. 지각된 성능(Perceived Performance)의 비약적 향상: 실제 서버 처리 속도는 같지만, 사용자는 클릭 즉시 반응하는 UI를 보며 앱이 매우 빠르다고 느낍니다.
  2. 데이터 무결성 유지: 단순히 눈속임으로 끝나는 것이 아니라, 최종적으로는 서버의 진실(Source of Truth)과 동기화하는 과정을 거칩니다.
  3. 현대적인 인터페이스 구현: 인스타그램의 ‘좋아요’나 메신저의 ‘메시지 전송’ 등 이미 많은 글로벌 서비스가 이 방식을 통해 매끄러운 경험을 제공하고 있습니다.

TanStack Query를 활용한 구현 메커니즘

TanStack Query(F.K.A React Query)는 낙관적 업데이트를 구현하기 위한 최적의 상태 관리 도구입니다. useMutation 훅의 세 가지 라이프사이클을 활용하여 체계적인 업데이트를 수행합니다.

  • onMutate: 실제 서버 요청이 가기 전에 호출됩니다. 여기서 현재 데이터를 백업(Snapshot)하고 캐시를 수동으로 업데이트합니다.
  • onError: 요청이 실패했을 때 호출됩니다. 백업해둔 스냅샷을 사용해 UI를 이전 상태로 되돌립니다(Rollback).
  • onSettled: 성공/실패 여부와 상관없이 마지막에 호출됩니다. 서버를 다시 찔러(Invalidate) 최종 데이터를 동기화합니다.

실전 예제: 할 일 목록(Todo) 추가 로직

가장 일반적인 ‘할 일 목록 추가’ 시나리오를 통해 코드를 작성해 보겠습니다. 이 예제는 TypeScript를 기반으로 하며, 실패 시 완벽한 롤백을 보장합니다.

import { useMutation, useQueryClient } from '@tanstack/react-query';

interface Todo {
  id: string;
  text: string;
}

export const useAddTodo = () => {
  const queryClient = useQueryClient();

  return useMutation({
    // 실제 서버에 데이터를 저장하는 액션
    mutationFn: (newTodo: Todo) => fetch('/api/todos', {
      method: 'POST',
      body: JSON.stringify(newTodo),
    }),

    // 1. 뮤테이션 시작 시점 (낙관적 업데이트 수행)
    onMutate: async (newTodo) => {
      // 진행 중인 refetch를 취소하여 낙관적 데이터가 덮어씌워지지 않게 함
      await queryClient.cancelQueries({ queryKey: ['todos'] });

      // 이전 상태를 백업 (Snapshot)
      const previousTodos = queryClient.getQueryData<Todo[]>(['todos']);

      // 캐시를 수동으로 업데이트하여 UI에 즉시 반영
      queryClient.setQueryData(['todos'], (old: Todo[] | undefined) => 
        old ? [...old, newTodo] : [newTodo]
      );

      // 스냅샷을 포함한 컨텍스트 반환
      return { previousTodos };
    },

    // 2. 서버 요청 실패 시 (롤백 수행)
    onError: (err, newTodo, context) => {
      if (context?.previousTodos) {
        // 백업해둔 데이터로 캐시 원복
        queryClient.setQueryData(['todos'], context.previousTodos);
      }
      console.error('업데이트 실패:', err);
    },

    // 3. 완료 시 (최종 동기화)
    onSettled: () => {
      // 서버 데이터와 클라이언트 캐시의 일치성을 위해 다시 가져오기 수행
      queryClient.invalidateQueries({ queryKey: ['todos'] });
    },
  });
};

💡 개발자의 팁: 임시 ID 관리

낙관적 업데이트를 할 때 서버에서 생성할 ID가 아직 없는 상태라면, crypto.randomUUID()Date.now()를 활용해 임시 고유 키를 생성하세요. 이후 onSettled 단계에서 서버의 실제 ID를 포함한 데이터로 자연스럽게 교체됩니다.


주의사항 및 트러블슈팅

낙관적 업데이트는 양날의 검과 같습니다. 잘못 구현하면 데이터가 꼬이거나 사용자에게 잘못된 정보를 전달할 수 있습니다.

  • 데이터 타입 불일치: 서버가 반환하는 실제 객체 구조와 클라이언트에서 낙관적으로 추가하는 객체 구조가 동일해야 합니다.
  • 잦은 실패 상황: 서버 에러가 잦은 환경에서는 UI가 생겼다 사라졌다 하는 ‘깜빡임’이 발생하여 오히려 불쾌한 경험을 줄 수 있습니다. 이럴 때는 서버 액션을 통한 정직한 업데이트가 낫습니다.
  • 병렬 업데이트: 여러 개의 업데이트가 동시에 일어날 경우 cancelQueries를 통해 이전 요청의 간섭을 반드시 차단해야 합니다.

자주 묻는 질문(FAQ)

Q. 서버 액션(Server Actions)과 낙관적 업데이트 중 무엇이 나은가요?

단순한 폼 제출이나 페이지 이동이 동반된다면 Server Actions가 구현 비용 대비 효율적입니다. 하지만 대시보드처럼 한 화면 내에서 수많은 인터렉션이 발생하고 ‘빠릿함’이 핵심이라면 낙관적 업데이트를 추천합니다.

Q. 실패 시 롤백 로직을 반드시 짜야 하나요?

네, 필수입니다. 롤백이 없다면 사용자는 성공한 줄 알았던 데이터가 새로고침 후 사라지는 ‘데이터 유실’ 현상을 겪게 되며, 이는 서비스의 신뢰도와 직결됩니다.

Q. staleTime이 짧아도 낙관적 업데이트가 유효한가요?

유효합니다. 낙관적 업데이트는 staleTime과 상관없이 캐시를 강제로 수정하는 행위입니다. 다만 onSettled에서 invalidateQueries를 호출하면 최신 데이터를 다시 가져오므로 무결성이 보장됩니다.


오늘 살펴본 낙관적 업데이트는 결국 **“기술적 복잡성을 개발자가 짊어지고, 사용자에게는 즐거움을 주는 방식”**입니다. 여러분의 프로젝트 상황에 맞춰 적절히 도입해 보시기 바랍니다!

이 글이 마음에 드셨나요?

로딩 중...