[React] 낙관적 업데이트(Optimistic Update) 제대로 구현해보기

2025. 4. 9. 13:53·Web/React
목차
  1. 프로젝트 배포 링크
  2. 🔍 낙관적 업데이트란?
  3. 🎯 구현 목표 및 설계
  4. 1. API 구현 대체 (setTimeout 사용)
  5. 2. Zustand로 상태 관리
  6. 3. UI에서 즉각적인 피드백
  7. 4. 테스트 카드 구성: 성공 vs 실패
  8. 1. 트러블 슈팅 과정
  9. 빠른 토글로 인한 낙관적 업데이트 롤백 이슈 해결기
  10. 2. react-query로 리팩토링

 

 

 

프론트엔드에서 사용자 경험을 향상시키는 데 있어 낙관적 업데이트는 매우 중요한 전략입니다. 이번 포스팅에서는 낙관적 업데이트가 무엇인지, 그리고 이를 실제 프로젝트에 어떻게 적용할 수 있는지를 직접 구현해보며 정리해봤습니다.

 

프로젝트 배포 링크

https://react-opimistic-update-psi.vercel.app/

 

Vite + React + TS

 

react-opimistic-update-psi.vercel.app

 

 


🔍 낙관적 업데이트란?

낙관적 업데이트는 요청이 성공할 것이라고 '낙관적으로' 가정하고, UI를 먼저 업데이트한 뒤, 나중에 서버 응답에 따라 결과를 반영하거나 롤백하는 방식입니다.

예를 들어, 좋아요 버튼을 클릭했을 때 실제 요청 결과를 기다리는 대신:

  • 하트 아이콘이 즉시 활성화(채워짐) 되고,
  • 좋아요 수가 즉시 1 증가합니다.
  • 만약 요청이 실패한다면 → UI는 이전 상태로 롤백됩니다.

즉각적인 피드백을 통해 UX(사용자 경험)을 향상시키는 것이 핵심입니다.

 

 


🎯 구현 목표 및 설계

1. API 구현 대체 (setTimeout 사용)

현재는 서버가 존재하지 않기 때문에, API 요청은 setTimeout으로 대체하여 마치 비동기처리 인것처럼 구현을 진행했습니다.

// likeApi.ts
const likeApi = async (like: boolean, isError: boolean) => {
  if (isError) {
    await new Promise((res) => setTimeout(res, 3000));
    throw new Error('낙관적 업데이트 실패');
  }

  await new Promise((res) => setTimeout(res, 300));
  return like;
};

2. Zustand로 상태 관리

데이터베이스 없이도 좋아요 수를 저장할 수 있도록 상태 관리는 Zustand로 구현했습니다.

import { create } from 'zustand';

interface LikeState {
  likes: { [key: string]: number };
  incrementLikes: (id: string) => void;
  decrementLikes: (id: string) => void;
}

const useStore = create<LikeState>((set) => ({
  likes: {},
  incrementLikes: (id) =>
    set((state) => ({
      likes: {
        ...state.likes,
        [id]: (state.likes[id] || 0) + 1,
      },
    })),
  decrementLikes: (id) =>
    set((state) => ({
      likes: {
        ...state.likes,
        [id]: (state.likes[id] || 0) - 1,
      },
    })),
}));

export default useStore;

 

카드 컴포넌트에서 id로 서로 다른 카드임을 증명합니다.

like 상태를 관리하기 위한 좋아요 증가 함수와 좋아요 감소 함수를 각각 구현했습니다.


3. UI에서 즉각적인 피드백

다음과 같이 동작하는 것으로 설정했습니다.

하트의 경우 font-awesome을 활용하여 아이콘을 font처럼 사용했습니다.

 

사용자가 하트를 누르면:

  • 빈 하트 → 빨간 하트로 즉시 전환
  • 좋아요 수 즉시 증가

요청이 실패하면:

  • 하트는 다시 비워지고
  • 좋아요 수도 원상복귀

 


4. 테스트 카드 구성: 성공 vs 실패

카드 설명
✅ 사이다 카드 요청이 정상적으로 처리되는 카드.
❌ 쭈니 카드 요청 실패를 의도적으로 발생시켜 롤백을 테스트하는 카드.

 

성공과 실패 카드를 각각 만들어, 낙관적 업데이트와 롤백 동작을 누구나 알기쉽게 테스트해볼 수 있습니다.

 

750

이 과정은 콘솔 로그로 확인할 수 있습니다!

 

1. 트러블 슈팅 과정

빠른 토글로 인한 낙관적 업데이트 롤백 이슈 해결기

🧨 기존 구현에서의 문제

기존에는 좋아요 버튼 클릭 시, 단순하게 낙관적 업데이트가 진행되면 상태값을 3초 후 롤백하는 방식으로 처리하고 있었습니다.

try {
  if (isError) {
    await new Promise((resolve) => {
      setTimeout(resolve, 3000);
    });
    decrementLikes(id);
    setLiked(false);
  }
} catch (error) {}

하지만 사용자가 3초 롤백 대기 시간 내에 하트를 연속 클릭하는 경우, 상태값이 꼬이면서 좋아요 수가 음수로 내려가는 버그가 발생했습니다.

✅ 개선: pending 상태 도입

이를 해결하기 위해 pending 상태를 추가하여, API 요청 중에는 중복 클릭을 방지했습니다.

const [pending, setPending] = useState(false);

const handleLike = async () => {
  if (pending) return;
  setPending(true);

  if (liked) {
    decrementLikes(id);
    setLiked(false);
    setPending(false);
    return;
  }

  incrementLikes(id);
  setLiked(true);

  try {
    if (isError) {
      await new Promise((resolve) => setTimeout(resolve, 3000));
      decrementLikes(id);
      setLiked(false);
      console.log('낙관적 업데이트 롤백!');
    }
  } finally {
    setPending(false);
  }
};

이제는 요청 도중에 다시 버튼을 누를 수 없기 때문에, 이중 처리 문제를 예방할 수 있게 되었습니다.


2. react-query로 리팩토링

낙관적 업데이트는 react-query의 useMutation 훅으로 훨씬 더 깔끔하게 처리할 수 있습니다.
단순한 setTimeout을 활용한 롤백 처리 대신, onMutate / onError / onSuccess / onSettled 를 사용해 명확하게 사이클을 관리합니다.

 

API 분리

비동기 처리는 apis/likeApi.ts로 분리하여, 컴포넌트에서는 UI 로직에만 집중할 수 있도록 리팩토링했습니다.

const likeApi = async (like: boolean, isError: boolean) => {
  if (isError) {
    await new Promise((res) => setTimeout(res, 3000));
    throw new Error('낙관적 업데이트 실패');
  }

  await new Promise((res) => setTimeout(res, 300));
  return like;
};

⚙️ useMutation 활용

const mutation = useMutation({
  mutationFn: (like: boolean) => likeApi(like, isError),

  onMutate: async (newLike) => {
    console.log(`${id}: ${newLike ? '좋아요' : '좋아요 취소'} 시도`);
    newLike ? incrementLikes(id) : decrementLikes(id);
    setLiked(newLike);
  },

  onError: (_err, newLike) => {
    console.error(`${id}: 실패 → 롤백`);
    newLike ? decrementLikes(id) : incrementLikes(id);
    setLiked(!newLike);
  },

  onSuccess: (_data, newLike) => {
    console.log(`${id}: ${newLike ? '좋아요 성공' : '좋아요 취소 성공'}`);
  },

  onSettled: () => {
    console.log(`${id}: 요청 완료`);
  },
});

🖱️ 핸들러에서는 mutate만 호출

const handleLike = () => {
  if (mutation.isPending) return; // 중복 요청 방지
  const nextLiked = !liked;
  mutation.mutate(nextLiked);
};

 

이렇게 로직을 따로 분리하여 가독성을 향상시키면서도 더욱 더 견고한 로직을 구현할 수 있었습니다.

저작자표시 비영리 동일조건

'Web > React' 카테고리의 다른 글

[React] Context API로 검색 결과 키워드를 강조해보자!  (0) 2025.04.26
[React] 스크롤 애니메이션을 구현하는 다양한 방법을 알아보자  (0) 2025.04.03
[React] 웹 브라우저 최적화 입문 - Lighthouse를 통한 성능 분석  (0) 2024.10.12
[Tailwind CSS] React(JavaScript) & Vite + Tailwind CSS + PostCSS & AutoFixer로 프론트엔드 레포를 초기 구성하는 법  (0) 2024.10.03
  1. 프로젝트 배포 링크
  2. 🔍 낙관적 업데이트란?
  3. 🎯 구현 목표 및 설계
  4. 1. API 구현 대체 (setTimeout 사용)
  5. 2. Zustand로 상태 관리
  6. 3. UI에서 즉각적인 피드백
  7. 4. 테스트 카드 구성: 성공 vs 실패
  8. 1. 트러블 슈팅 과정
  9. 빠른 토글로 인한 낙관적 업데이트 롤백 이슈 해결기
  10. 2. react-query로 리팩토링
'Web/React' 카테고리의 다른 글
  • [React] Context API로 검색 결과 키워드를 강조해보자!
  • [React] 스크롤 애니메이션을 구현하는 다양한 방법을 알아보자
  • [React] 웹 브라우저 최적화 입문 - Lighthouse를 통한 성능 분석
  • [Tailwind CSS] React(JavaScript) & Vite + Tailwind CSS + PostCSS & AutoFixer로 프론트엔드 레포를 초기 구성하는 법
abyss-s
abyss-s
프론트엔드 공부합니다.
  • abyss-s
    abyss-s의 블로그입니다.
    abyss-s
  • 전체
    오늘
    어제
    • 분류 전체보기 (188)
      • Web (16)
        • JavaScript (6)
        • TypeScript (1)
        • React (5)
        • Vue (0)
        • Storybook (1)
        • Next.js (1)
      • Backend & Infra (8)
        • Database (3)
        • Node.js (2)
        • SpringBoot (1)
      • PS (71)
      • CS (30)
        • OS (13)
        • Structure & Algorithm (5)
        • Network (10)
        • 정보처리기사 (2)
      • Language (18)
        • OOP (1)
        • JAVA (13)
        • C++ (4)
      • Activities (12)
        • 멋쟁이 사자처럼 (2)
        • OSSCA (3)
        • LG U+ URECA (3)
        • Project (2)
      • AI (0)
      • Git & Github (5)
      • Notion (1)
      • IT (4)
      • Statistics (11)
      • Book (4)
      • Diary (1)
      • Game (1)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

    • 깃허브
    • 백준
    • 트위터
  • 공지사항

    • abyss-s의 티스토리에 오신 것을 환영합니다.
  • 인기 글

  • 태그

    운영체제
    그리디
    Python
    백준
    자바스크립트
    네트워크
    코드트리
    자바기반응용프로그래밍
    github
    Java
    통계학
    OS
    BAEKJOON
    DP
    파이썬
    JavaScript
    C++
    React
    BFS
    생활코딩
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.0
abyss-s
[React] 낙관적 업데이트(Optimistic Update) 제대로 구현해보기

개인정보

  • 티스토리 홈
  • 포럼
  • 로그인
상단으로

티스토리툴바

단축키

내 블로그

내 블로그 - 관리자 홈 전환
Q
Q
새 글 쓰기
W
W

블로그 게시글

글 수정 (권한 있는 경우)
E
E
댓글 영역으로 이동
C
C

모든 영역

이 페이지의 URL 복사
S
S
맨 위로 이동
T
T
티스토리 홈 이동
H
H
단축키 안내
Shift + /
⇧ + /

* 단축키는 한글/영문 대소문자로 이용 가능하며, 티스토리 기본 도메인에서만 동작합니다.