React 19 폼 액션과 useOptimistic으로 낙관적 UI 만들기

서버 응답을 기다리지 않고 UI를 먼저 갱신할 때 지켜야 할 안전 규칙

React 19 낙관적 UI 썸네일

핵심 요약

  1. 낙관적 업데이트는 실패가 드물고 되돌리기 쉬운 작업에만 쓴다.
  2. useOptimistic은 실제 상태가 갱신되면 임시 상태가 자동으로 사라져 별도 롤백 코드가 필요 없다.
  3. 폼 액션은 실패를 예외가 아니라 결과 객체로 다뤄 사용자에게 오류를 표시한다.
  4. 중복 제출 방지에는 버튼 비활성화와 서버측 idempotency 키를 함께 둔다.

좋아요 버튼을 눌렀는데 서버 응답이 올 때까지 아무 변화가 없다면, 사용자는 두 번 누르거나 새로고침을 한다. 반대로 서버 응답 전에 UI를 먼저 바꾸는 낙관적 업데이트는 체감을 크게 개선하지만, 실패했을 때 되돌리는 로직을 빠뜨리면 화면과 실제 데이터가 어긋난다. 이 글은 React 19의 폼 액션과 useOptimistic으로 이 패턴을 안전하게 구현하는 방법을 정리했다.

낙관적 UI가 사용자 경험을 바꾸는 지점

낙관적 업데이트는 "성공할 것이라 가정하고" 화면을 먼저 갱신한다. 체감 지연이 줄고, 실패 시에만 되돌리면 되기 때문에 서버가 대체로 성공하는 작업에 잘 맞는다. 반대로 실패가 잦거나 되돌리기 어려운 작업에는 쓰지 않는 편이 낫다.

폼 액션과 서버 상태 연결

React 19에서는 폼의 action에 함수를 넘기면 제출 흐름을 프레임워크가 관리한다. 서버 액션을 쓰면 왕복 로직을 직접 만들지 않아도 된다.

import { useActionState } from "react";

const [state, formAction, isPending] = useActionState(addComment, initialState);

return (
  <form action={formAction}>
    <input name="text" />
    <button disabled={isPending}>{isPending ? "저장 중" : "등록"}</button>
    {state.error && <p role="alert">{state.error}</p>}
  </form>
);

핵심은 액션이 결과 객체를 반환하게 만들어, 실패를 예외가 아니라 값으로 다루는 것이다. 서버 액션 쪽 패턴은 App Router 서버 액션 에러 핸들링에서 더 자세히 다뤘다.

useOptimistic으로 임시 상태 표현하기

React 공식 useOptimistic 문서에 따르면 useOptimistic은 비동기 액션이 진행되는 동안 실제 상태와 별개로 "표시용 임시 상태"를 렌더링한다. 액션이 끝나면 실제 상태로 자연스럽게 대체된다.

const [optimisticComments, addOptimistic] = useOptimistic(
  comments,
  (current, newText) => [...current, { id: "temp", text: newText, pending: true }]
);

성공·실패 시 상태 확정과 롤백

임시 상태는 실제 상태가 갱신되는 순간 사라진다. 액션이 실패하면 실제 상태가 그대로라 임시 항목도 자동으로 사라지고, 오류 메시지만 남긴다. 즉 "롤백"을 위해 별도 상태를 되돌리는 코드를 쓰지 않아도 되는 점이 이 훅의 장점이다. 대신 실패를 사용자에게 명확히 알려야 한다.

낙관적 업데이트가 위험한 작업 구분

안전한 예위험한 예
좋아요·북마크 토글결제·송금
댓글·메모 추가예약 확정·재고 차감
읽음 표시되돌릴 수 없는 삭제·발송

위험한 작업은 낙관적으로 보여주되 확정 전에는 반드시 서버 결과를 기다리거나, 확인 단계를 둔다.

중복 제출과 재시도 처리

  • 제출 중에는 버튼을 비활성화하거나 진행 상태를 표시한다
  • 같은 요청이 두 번 도착해도 안전하도록 서버에 idempotency 키를 둔다
  • 실패 후 재시도는 임시 항목을 중복 추가하지 않도록 식별자를 부여한다

접근성과 로딩 상태 표시

낙관적 갱신은 화면만 바꾸므로 스크린 리더가 변화를 놓칠 수 있다. 저장 중·저장 완료·실패를 role="status"나 role="alert"로 알리고, 폼 필드 오류는 입력과 연결한다. 목록의 서버 상태 동기화는 2026 프론트엔드 상태관리 선택와 함께 설계하면 일관성이 높아진다.

낙관적 업데이트를 테스트하는 방법

낙관적 UI는 "빠르다"는 이유로 검증을 건너뛰기 쉽지만, 실패 경로가 오히려 더 중요하다. 성공만 테스트하면 서버 오류나 중복 제출 상황에서 화면이 어긋나는 문제를 놓친다.

  1. 성공 시 실제 상태로 자연스럽게 대체되는지 확인한다
  2. 실패 시 임시 항목이 사라지고 오류가 표시되는지 확인한다
  3. 제출 중 두 번 눌러도 요청이 한 번만 나가는지 확인한다
  4. 느린 응답에서 로딩 표시가 유지되는지 확인한다
  5. 스크린 리더에 상태 변화가 전달되는지 확인한다

서버 액션을 목(mock)으로 대체하면 실패·지연 시나리오를 쉽게 재현할 수 있다. 다만 목이 실제 응답 형식과 어긋나면 테스트가 통과해도 운영에서 깨지므로, 응답 스키마를 한 곳에서 공유하고 양쪽에서 검증하는 편이 안전하다.

또한 낙관적 갱신이 여러 화면에 걸쳐 있으면 한 화면의 갱신이 다른 화면의 캐시를 무효화해야 할 수 있다. 이때는 화면별 임시 상태를 따로 만들기보다 서버 상태 도구의 무효화 경로를 한 곳으로 모아 중복과 불일치를 줄인다.

실패를 복구 가능하게 만드는 문구

낙관적 UI에서 실패는 곧 "방금 한 일이 없던 일이 되는" 경험이다. 사용자가 무엇이 실패했고 다시 시도할 수 있는지를 즉시 알 수 있어야 한다. 오류 문구에는 실패한 대상, 원인 요약, 다음 행동을 담는다.

재시도 버튼은 임시 항목을 중복 생성하지 않도록 같은 식별자를 재사용한다. 여러 항목을 동시에 낙관적으로 추가하는 화면이라면 항목별 상태를 분리해, 하나의 실패가 다른 항목의 성공 표시를 지우지 않게 한다.

낙관적 UI의 목표는 "서버를 속이는 것"이 아니라 "기다림을 체감에서 숨기는 것"이다. 실제 데이터가 언제 확정되는지 코드에서 분명히 드러나게 유지하면, 이후 기능을 추가해도 상태가 어긋나는 일이 줄어든다.

초보자가 자주 실수하는 포인트

  • 결제나 송금처럼 되돌리기 어려운 작업에 낙관적 UI를 적용한다.
  • 실패를 조용히 삼켜 임시 항목이 사라진 이유를 사용자가 모른다.
  • 제출 중 버튼을 막지 않아 같은 요청이 여러 번 전송된다.
  • 화면 변화를 스크린 리더에 알리지 않아 접근성이 떨어진다.

체크리스트

  • 낙관적으로 처리할 작업의 위험도를 분류했다.
  • 액션이 성공·실패 결과 객체를 반환하도록 만들었다.
  • 실패 시 사용자에게 오류를 role로 알린다.
  • 제출 중 중복 전송을 막고 서버에 idempotency 키를 두었다.
  • 로딩·저장 중 상태를 화면과 스크린 리더 모두에 표시한다.

자주 묻는 질문

useOptimistic과 useActionState를 함께 써야 하나?

함께 쓰는 것이 일반적이다. useActionState는 제출 상태와 결과를, useOptimistic은 표시용 임시 상태를 담당한다. 둘의 역할을 섞지 않는 것이 중요하다.

낙관적 업데이트가 실패하면 자동으로 되돌아가나?

임시 상태는 실제 상태가 갱신되지 않으면 사라지므로 화면은 원래대로 돌아간다. 다만 실패 사실을 사용자에게 알려 주지 않으면 혼란을 준다.

참고 자료 · 검증 기준

위 자료와 내용을 대조한 날짜: . 도구·서비스 정책은 이후 바뀔 수 있으므로 적용 전 공식 문서를 다시 확인하세요.

이 글은 실제 실무 환경 테스트 및 검증된 기술 자료를 바탕으로 작성되었으며, 환경 변화에 따라 지속적으로 갱신됩니다.