C.W.K.
Stream
Lesson 04 of 06 · published

useOptimistic — 거짓 없는 즉시 UI

~14 min · useoptimistic, react-19, actions

Level 0React 입문자
0 XP0/54 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
전형적인 optimistic-update 버그: 변경 즉시 보여주고 서버가 no 라 할 때 roll back 잊음. useOptimistic 가 rollback 대신 처리 — Action settle 시 합성 state 가 자동으로 사라짐.

시그니처

const [optimisticState, addOptimistic] = useOptimistic(realState, updater)updater(current, optimisticValue) => newState. Hook 반환:

  • optimisticState — 렌더할 state. Action pending 없으면 realState 와 같음; Action in-flight 인 동안 updater 의 출력과 같음.
  • addOptimistic — Action 안에서 호출해 optimistic 업데이트 적용.

패턴

Action 안에서 addOptimistic(value) 를 첫 번째로 호출, 그 다음 진짜 작업 await. UI 가 즉시 optimistic 값 반영. Action settle (성공 또는 실패) 시 useOptimistic 가 optimistic 값 버리고 그 시점의 realState 와 재동기화.

'거짓 없음' 원칙

Optimistic 업데이트는 UX 가속기, 정확성 단축 아님. Action 이 정말로 fail 가능 (네트워크 다운, 유효성 에러, 권한 거부) 하면 rollback flash 가 정직 — 사용자가 즉시 희망 보고, 그 다음 진실 봄. 피드백 없는 것보다 나음, 사용자 action 조용히 떨어뜨리는 것보다 훨씬 나음.

흔한 짝

useOptimistic + useActionState: realState 가 useActionState 에서; useOptimistic 가 합성 버전용으로 감쌈. 대부분의 프로덕션 패턴이 둘 다 사용 결말.

사용자엔 optimistic, 시스템엔 authoritative. Optimistic state 가 사용자가 보고 느끼는 것; real state 가 reload 살아남는 것. Success 경로에 정렬 유지; 실패에 보이는 flash 가 진실 말하게.

Code

useOptimistic + useActionState — like 버튼·tsx
import { useActionState, useOptimistic } from "react";

type LikeState = { count: number };

async function toggleLike(state: LikeState, formData: FormData): Promise<LikeState> {
  const delta = formData.get("delta") === "plus" ? 1 : -1;
  await fetch("/api/like", {
    method: "POST",
    body: JSON.stringify({ delta }),
  });
  return { count: state.count + delta };
}

export function LikeButton({ initial }: { initial: number }) {
  const [state, formAction] = useActionState(toggleLike, { count: initial });

  // Optimistic wrapper — action in-flight 까지 state 미러.
  const [optimistic, addOptimistic] = useOptimistic(
    state,
    (current: LikeState, delta: number) => ({ count: current.count + delta })
  );

  async function handleClick(delta: 1 | -1) {
    addOptimistic(delta); // UI 가 즉시 count ± 1 로 업데이트
    const fd = new FormData();
    fd.set("delta", delta === 1 ? "plus" : "minus");
    await formAction(fd); // 진짜 action 돌고, done 시 진짜 state land
  }

  return (
    <div className="flex items-center gap-2">
      <button onClick={() => handleClick(-1)}>-</button>
      <span className="font-mono">{optimistic.count}</span>
      <button onClick={() => handleClick(1)}>+</button>
    </div>
  );
}
Optimistic 채팅 메시지 append·tsx
import { useOptimistic } from "react";

type Message = { id: string; text: string };

export function ChatList({ messages, sendMessage }: {
  messages: Message[];
  sendMessage: (text: string) => Promise<void>;
}) {
  const [optimisticMessages, addOptimistic] = useOptimistic(
    messages,
    (current: Message[], pendingText: string): Message[] => [
      ...current,
      { id: `pending-${Date.now()}`, text: pendingText },
    ]
  );

  async function handleSend(formData: FormData) {
    const text = formData.get("text") as string;
    addOptimistic(text); // optimisticMessages 에 즉시 나타남
    await sendMessage(text);
    // messages 업데이트 시 (부모가 진짜 메시지로 re-render),
    // useOptimistic 가 진짜 리스트로 자동 동기화.
  }

  return (
    <>
      <ul>
        {optimisticMessages.map((m) => (
          <li key={m.id} className={m.id.startsWith("pending-") ? "opacity-60" : ""}>
            {m.text}
          </li>
        ))}
      </ul>
      <form action={handleSend}>
        <input name="text" required />
        <button type="submit">Send</button>
      </form>
    </>
  );
}

External links

Exercise

Optimistic add 가진 todo 리스트 빌드. 진짜 state 가 부모에 (useState 배열). Add action 이 30% 시간 무작위 fail 하는 가짜 endpoint 에 POST. useOptimistic 사용해서 새 아이템 즉시 나타나고, persist (성공 시) 또는 사라짐 (실패 시 — toast 표시). In-flight 요청 중 사용자가 이미 아이템 볼 수 있는지 확인; 실패 후 'Try again' 알림과 함께 사라지는 거 봄.
Hint
무작위 fail: if (Math.random() < 0.3) throw new Error('flaky network');. Optimistic 아이템은 다르게 스타일 (opacity-60) 해서 사용자가 아직 확정 안 됐다는 거 앎.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고

댓글 0

🔔 답글 알림 (로그인 필요)
로그인댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.