
프론트엔드에서 사용자 경험을 향상시키는 데 있어 낙관적 업데이트는 매우 중요한 전략입니다. 이번 포스팅에서는 낙관적 업데이트가 무엇인지, 그리고 이를 실제 프로젝트에 어떻게 적용할 수 있는지를 직접 구현해보며 정리해봤습니다.
프로젝트 배포 링크
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 실패
카드 | 설명 |
---|---|
✅ 사이다 카드 | 요청이 정상적으로 처리되는 카드. |
❌ 쭈니 카드 | 요청 실패를 의도적으로 발생시켜 롤백을 테스트하는 카드. |
성공과 실패 카드를 각각 만들어, 낙관적 업데이트와 롤백 동작을 누구나 알기쉽게 테스트해볼 수 있습니다.

이 과정은 콘솔 로그로 확인할 수 있습니다!
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 |