← 전체 글로 돌아가기

웹 개발

낙관적 업데이트 제대로 하는 방법

사용자는 즉시 피드백을 받아야 하는데, 낙관적 업데이트를 잘못하면 데이터 불일치 문제가 생깁니다.

사용자가 버튼을 클릭하면 즉시 화면에 반영되어야 한다. "잠깐만, 서버에 물어보고 올게"라고 기다리게 하면 반응성이 떨어진다. 그래서 낙관적 업데이트를 쓰는데, 잘못하면 재앙이 된다.

낙관적 업데이트는 무엇인가

서버 응답을 기다리지 않고 먼저 화면을 갱신한다. 그 다음 서버에 요청을 보낸다. 서버가 "OK"라고 하면 상태는 그대로고, 거부하면 되돌린다.

// 나쁜 예: 상태만 바꾸고 실패 처리가 없다
setItems([...items, newItem]);
fetch('/api/items', { method: 'POST', body: JSON.stringify(newItem) });

// 낫다: 실패 시 되돌린다
const optimisticItems = [...items, newItem];
setItems(optimisticItems);

fetch('/api/items', { method: 'POST', body: JSON.stringify(newItem) })
  .catch(err => {
    setItems(items); // 원래 상태로 돌린다
    showError('저장 실패');
  });

먼저 화면을 갱신하는 타이밍

클릭 순간에 바로 갱신하는 게 낫다. 유저가 느낀 "완료 감"이 중요하다. 서버 응답이 오는 데 500ms 걸린다면, 낙관적 업데이트로 즉시 반영하면 사용자는 완료된 줄 안다.

충돌 감지하기

여러 사용자가 같은 데이터를 동시에 수정할 수 있다. 내가 낙관적으로 먼저 표시했는데 서버에서 거부될 수 있다. 그때는 사용자에게 알리고 화면을 되돌린다.

const response = await fetch('/api/items', { method: 'POST', body: JSON.stringify(newItem) });

if (!response.ok) {
  // 서버가 거부했다
  setItems(items); // 롤백
  if (response.status === 409) {
    showError('다른 사용자가 방금 수정했습니다. 새로고침 해주세요.');
  }
  return;
}

const saved = await response.json();
setItems(savedItems =>
  savedItems.map(item => item.id === newItem.id ? saved : item)
);

UI 피드백 주기

낙관적 업데이트가 성공했는지 실패했는지를 사용자가 알아야 한다. 저장 중 표시, 저장됨 표시, 실패 표시를 모두 해준다.

  • 클릭 → UI 즉시 갱신, 로딩 표시 켜기
  • 서버 응답 OK → 로딩 표시 끄기, 필요시 "저장됨" 메시지
  • 서버 응답 실패 → 로딩 표시 끄기, UI 원래대로, 에러 메시지

로컬과 서버 상태 일치 확인

낙관적 업데이트가 끝났다고 해서 끝이 아니다. 서버에 다시 요청해서 실제 데이터가 맞는지 확인한다. 간단한 방법은 저장 후 새로고침하는 것이다.

마지막으로, 낙관적 업데이트는 만능이 아니다. 정말 중요한 데이터면 "저장 중"이 조금 길더라도 서버 응답을 기다리는 게 낫다.