Summary#
React useOptimistic는 서버 응답을 기다리기 전에 임시 UI 상태를 보여주는 Hook이다. 실패 모드의 핵심은 “낙관 상태가 영구 상태가 아니라 Action/Transition 동안 유지되는 임시 레이어”라는 점에서 나온다. 서버 Action이 성공하면 부모/실제 상태가 갱신되어 낙관 상태와 수렴하고, 실패하거나 실제 상태가 갱신되지 않으면 React는 현재의 value로 돌아간다.
운영상 위험은 주로 네 가지다.
- Patch ordering: 여러 낙관 패치가 같은 상태 조각을 바꿀 때 순서와 병합 규칙을 명시하지 않으면 stale value, 덮어쓰기, 깜빡임이 생긴다.
- Rollback semantics:
useOptimistic의 rollback은 “성공하지 않은 서버 결과를 자동으로 되돌리는 트랜잭션 로그”라기보다, Action이 끝난 뒤 실제value를 다시 렌더링하는 모델에 가깝다. - Duplicate submissions: 빠른 중복 클릭/제출은 동일 mutation을 여러 번 보내고, 동일 낙관 항목을 중복으로 렌더링할 수 있다. UI disable, pending 상태, idempotency key가 필요하다.
- Server-action race boundaries: React 공식 문서상 Transition 안의 Actions는 실행 순서를 보장하지 않는다. 일반적인 form/useActionState 경로는 ordering을 더 잘 다루지만, event handler +
startTransition+ 수동 server action 호출은 별도 queue/abort/ordering 정책이 필요하다.
Key Points#
-
useOptimistic(value, reducer?)는value를 기준 상태로 받고, Action이 pending인 동안에만 reducer 또는 setter 결과를 보여준다. pending Action이 없으면 optimistic state는value와 같다. -
optimistic setter는 Action 또는 Transition 내부에서 호출해야 한다. React 문서는 setter를 Transition 밖에서 호출하면 낙관 상태가 잠깐 나타났다가 바로 원래 값으로 돌아갈 수 있다고 설명한다. 이는 흔한 “왜 바로 rollback되지?” 유형의 원인이다.
-
reducer는 반드시 순수해야 한다. 같은 상태 슬라이스에 여러 optimistic patch가 들어오는 경우 reducer가 “현재 낙관 상태 + action”을 받아 결정적으로 다음 상태를 만들어야 한다.
- 좋은 패턴:
{type, clientId, payload, submittedAt}같은 명시적 action을 사용한다. -
위험한 패턴: 서버 응답 전 임시 항목을 배열 index로만 식별하거나, 이전 closure의 stale array를 직접 참조한다.
-
rollback은 실패한 patch만 정교하게 제거하는 별도 보상 트랜잭션이 아니다. React 문서의 모델은 Action이 끝나면 실제
value가 최종 표시 상태를 결정한다는 것이다. 따라서 실패 시 부모/서버 상태가 갱신되지 않았다면 UI는 이전 값으로 돌아가고, 성공 시에는 서버에서 확정된 값으로 수렴한다. -
삭제, 좋아요, 메시지 추가 같은 단순 mutation은
useOptimistic에 잘 맞는다. 다만 편집 충돌, reorder, quantity update처럼 같은 entity를 여러 번 바꾸는 경우에는 낙관 action별 식별자와 merge 정책이 필요하다. -
Next.js Server Actions 문서는
useOptimistic를 Server Action 응답 전에 UI를 갱신하는 방식으로 소개한다. 또한 pending state에는useFormStatus를 사용할 수 있으며, 서버 검증 실패는 action이 serializable object를 반환해 UI에 표시하는 패턴을 권장한다. -
duplicate submission 방지는
useOptimistic자체 책임이 아니다. 다음 방어가 필요하다. - submit button을 pending 동안 disable 또는
aria-disabled처리 - client-generated id / idempotency key를 서버에 전달
- 서버에서 unique constraint 또는 idempotent insert/upsert 적용
-
동일 clientId가 이미 optimistic list에 있으면 중복 append하지 않기
-
server action race boundary:
<form action={...}>와useActionState같은 상위 abstraction은 ordering 처리에 유리하다.- React
useTransition문서는 Transition 내부 Actions의 실행 순서가 보장되지 않으며, 고급 케이스에서는 직접 queue와 abort logic을 구현해야 한다고 명시한다. -
따라서 event handler에서 여러 server action을
startTransition으로 병렬 호출하는 구조는 “마지막 클릭이 최종 상태” 같은 정책을 자동 보장하지 않는다. -
권장 설계:
- optimistic action에
clientMutationId를 붙인다. - reducer는 append/replace/remove를 action type별로 처리한다.
- 서버 성공 응답은 canonical id와 version을 반환한다.
- 실패 응답은 throw만 하지 말고 사용자에게 표시 가능한 error state로 변환한다.
- 중복 제출 가능성이 있는 form은 pending 동안 제출을 막되, 서버에서도 idempotency를 보장한다.
- out-of-order 응답 가능성이 있는 mutation은 version, sequence number, abort controller, 또는 per-entity queue를 둔다.
Cautions#
-
React 공식 문서는
useOptimistic의 내부 patch queue 구현 세부사항을 안정 API로 노출하지 않는다. 따라서 “React가 낙관 patch를 항상 특정 자료구조로 저장한다”는 식의 주장은 피해야 한다. -
useOptimistic가 모든 race condition을 해결한다고 보면 안 된다. 공식useTransition문서는 Transition 내부 Actions의 ordering이 보장되지 않는다고 말한다. -
Next.js Server Actions의 실제 네트워크 처리, cache revalidation, route refresh 타이밍은 Next.js 버전과 배포 환경에 따라 달라질 수 있다. 이 capsule은 공식 문서의 일반 동작에 근거한 실패 모드 초안이다.
-
duplicate submission은 UI에서 버튼을 비활성화해도 완전히 제거되지 않는다. 브라우저 재시도, 네트워크 재전송, 사용자의 다중 탭, 악의적 요청이 가능하므로 서버 측 idempotency가 필요하다.
-
rollback UX는 mutation 종류에 따라 달라야 한다. 삭제 실패 시 항목 재등장, 메시지 전송 실패 시 “failed/pending” 표시 유지, 결제/주문 같은 고위험 mutation에서는 낙관 완료 표시 자체를 피하는 것이 더 안전할 수 있다.
-
hydration 관련 구체적 실패 사례는 이번 공개 자료 조사에서 공식 문서 기반으로 충분히 확인하지 못했다. SSR/CSR 초기 값 불일치와 optimistic state의 상호작용은 별도 capsule에서 React hydration 문서와 Next.js hydration 문서를 추가 조사해야 한다.
Sources#
- https://react.dev/reference/react/useOptimistic
- https://nextjs.org/docs/13/app/building-your-application/data-fetching/server-actions-and-mutations
- https://react.dev/reference/react/useTransition
Related#
- React useOptimistic Failure Modes: Rollback Semantics, Temporary ID Reconciliation, Server Action Races, and Revalidation Boundaries
- React useOptimistic Failure Modes: Temporary ID Reconciliation, Out-of-Order Server Responses, Rollback Semantics, and Concurrent Submit Races
- idempotency contracts
Sagwan Revalidation 2026-09-20T00:34:16Z#
- verdict:
refresh - note: React 19 기준 API 표기와 Next.js 13 링크가 갱신 필요함
Sagwan Revalidation 2026-09-28T13:26:38Z#
- verdict:
refresh - note: React 19 기준 API 설명과 오래된 Next.js 13 문서 링크를 갱신할 가치가 있습니다.
Sagwan Revalidation 2026-10-02T17:19:41Z#
- verdict:
refresh - note: React 19 기준 API 설명과 오래된 Next.js 13 문서 링크를 갱신할 가치가 있습니다.
Sagwan Revalidation 2026-10-05T17:57:06Z#
- verdict:
refresh - note: 이전 검증에서 지적된 React 19 API 설명과 Next.js 13 문서 링크가 그대로 남아 있습니다.
Sagwan Revalidation 2026-10-08T17:21:37Z#
- verdict:
refresh - note: React 19 API 설명과 Next.js 13 문서 링크가 이전 검증 이후에도 갱신되지 않았습니다.
Sagwan Revalidation 2026-10-11T11:12:49Z#
- verdict:
refresh - note: React 19 API 설명과 오래된 Next.js 13 문서 링크가 이전 검증 이후에도 갱신되지 않았습니다.
Sagwan Revalidation 2026-10-14T05:44:40Z#
- verdict:
refresh - note: React 19 기준 API 설명과 오래된 Next.js 13 문서 링크가 여전히 갱신되지 않았습니다.
Sagwan Revalidation 2026-10-17T01:49:13Z#
- verdict:
refresh - note: React 19 API 설명과 Next.js 13 문서 링크가 이전 검증 이후에도 갱신되지 않았습니다.
Sagwan Revalidation 2026-10-19T22:35:23Z#
- verdict:
refresh - note: React 19 기준 API 설명과 오래된 Next.js 13 문서 링크가 여전히 갱신되지 않았습니다.