React 19 useOptimistic: 서버를 기다리지 않고 즉시 UI 구축

발행: (2026년 4월 21일 AM 09:36 GMT+9)
11 분 소요
원문: Dev.to

Source: Dev.to

React 19: useOptimistic – 서버 응답을 기다리지 않고 즉시 UI를 만들기

React 19에서는 **useOptimistic**이라는 새로운 훅을 도입했습니다. 이 훅은 서버와의 비동기 통신이 진행되는 동안에도 사용자가 즉시 피드백을 받을 수 있게 해 주어, 더 부드럽고 반응성이 뛰어난 사용자 경험을 제공합니다.

useOptimistic이 필요한가?

전통적인 UI 업데이트 흐름은 다음과 같습니다.

  1. 사용자가 액션을 트리거한다. (예: 버튼 클릭)
  2. 클라이언트가 서버에 요청을 보낸다.
  3. 서버가 응답을 반환한다.
  4. 응답을 받은 뒤 UI가 업데이트된다.

이 과정에서 네트워크 지연이 발생하면 사용자는 “버튼을 눌렀지만 아무 일도 일어나지 않는다”는 느낌을 받을 수 있습니다. useOptimistic은 서버 응답을 기다리기 전에 예상되는 상태를 미리 적용함으로써 이런 문제를 해결합니다.

기본 사용법

import { useOptimistic } from "react";

function Counter() {
  const [count, setCount] = useState(0);
  const [optimisticCount, addOptimistic] = useOptimistic(count, (state, delta) => state + delta);

  const handleClick = async () => {
    // 1️⃣ UI에 즉시 반영
    addOptimistic(1);

    // 2️⃣ 서버에 실제 요청 전송
    await fetch("/api/increment", { method: "POST" });

    // 3️⃣ 서버 응답이 오면 실제 상태와 동기화
    setCount(prev => prev + 1);
  };

  return (
    <button onClick={handleClick}>
      클릭 횟수: {optimisticCount}
    </button>
  );
}
  • 첫 번째 인자: 현재 실제 상태(count).
  • 두 번째 인자: 상태를 어떻게 변형할지 정의하는 함수. 여기서는 state + delta 로 간단히 구현했습니다.
  • addOptimistic을 호출하면 예상값이 즉시 UI에 반영됩니다. 실제 서버 응답이 도착하면 setCount 로 실제 상태와 동기화합니다.

폼 제출에 적용하기

function TodoForm() {
  const [todos, setTodos] = useState<string[]>([]);
  const [optimisticTodos, addOptimisticTodo] = useOptimistic(todos, (list, newTodo) => [...list, newTodo]);

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    const input = e.currentTarget.elements.namedItem("todo") as HTMLInputElement;
    const newTodo = input.value.trim();
    if (!newTodo) return;

    // UI에 즉시 추가
    addOptimisticTodo(newTodo);
    input.value = "";

    // 서버에 저장 요청
    const res = await fetch("/api/todos", {
      method: "POST",
      body: JSON.stringify({ text: newTodo }),
      headers: { "Content-Type": "application/json" },
    });

    // 성공 여부에 따라 실제 상태를 업데이트
    if (res.ok) {
      const saved = await res.json();
      setTodos(prev => [...prev, saved.text]);
    } else {
      // 실패 시 낙관적 업데이트를 되돌림
      setTodos(prev => prev.filter(t => t !== newTodo));
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <ul>
        {optimisticTodos.map((t, i) => (
          <li key={i}>{t}</li>
        ))}
      </ul>
      <input name="todo" placeholder="새 할 일 입력" />
      <button type="submit">추가</button>
    </form>
  );
}
  • 사용자가 폼을 제출하면 addOptimisticTodo 로 UI에 바로 항목이 나타납니다.
  • 서버가 성공적으로 저장하면 실제 todos 상태와 동기화하고, 실패하면 낙관적 업데이트를 롤백합니다.

useOptimistic 내부 동작 원리

  1. 낙관적 상태 저장: React는 현재 상태와 낙관적 업데이트 함수를 기억합니다.
  2. 동시성 제어: 여러 낙관적 업데이트가 동시에 발생해도 순서를 보장합니다.
  3. 자동 롤백: setState 혹은 useTransition 등으로 실제 상태가 바뀌면, 낙관적 값은 자동으로 최신 상태와 병합됩니다.

이 메커니즘 덕분에 개발자는 명시적으로 롤백 로직을 작성할 필요 없이 낙관적 UI를 구현할 수 있습니다.

언제 useOptimistic을 사용해야 할까?

  • 사용자 입력에 즉시 피드백이 필요한 경우 (예: 좋아요 버튼, 카운터, 채팅 메시지)
  • 네트워크 지연이 눈에 띄게 느껴지는 상황 (모바일 환경, 저속 연결)
  • 복잡한 상태 동기화가 필요 없는 단순한 변형 (리스트에 아이템 추가/삭제 등)

반면, 서버 검증이 반드시 선행되어야 하는 경우(예: 결제 처리, 권한 검사)는 낙관적 UI만으로는 충분하지 않으며, 기존의 useTransition 혹은 로딩 스피너와 함께 사용해야 합니다.

결론

React 19의 useOptimistic사용자 경험을 크게 향상시키는 강력한 도구입니다. 서버 응답을 기다리는 동안에도 UI가 즉시 반응하도록 함으로써, 현대 웹 애플리케이션에서 기대되는 “즉시성”을 손쉽게 구현할 수 있습니다.

다음 프로젝트에서 낙관적 UI를 적용해 보세요. 작은 코드 변경만으로도 사용자에게 더 부드럽고 빠른 인터랙션을 제공할 수 있습니다. 🚀

소개

useOptimistic는 React 19에서 가장 많이 활용되지 않는 훅 중 하나입니다. 로컬 상태와 로딩 스피너에 의존하는 대신, 서버 요청이 백그라운드에서 실행되는 동안에도 즉각적이고 반응성 있는 UI 업데이트를 제공할 수 있습니다.

useOptimistic 사용하기

기존 접근법 – 사용자가 지연을 느낌

async function addTodo(text: string) {
  setLoading(true);
  const newTodo = await createTodo(text); // 200‑800 ms wait
  setTodos(prev => [...prev, newTodo]);
  setLoading(false);
}

useOptimistic 사용 시

const [optimisticTodos, addOptimisticTodo] = useOptimistic(
  todos,
  (currentTodos, newText: string) => [
    ...currentTodos,
    { id: crypto.randomUUID(), text: newText, pending: true }
  ]
);

async function addTodo(text: string) {
  addOptimisticTodo(text);               // Instant UI update
  const newTodo = await createTodo(text); // Background request
  setTodos(prev => [...prev, newTodo]);
}

아이템이 즉시 나타납니다. 요청이 실패하면 낙관적 업데이트가 자동으로 롤백됩니다.

핵심 API

const [optimisticState, dispatchOptimistic] = useOptimistic(
  state,      // real state (from server/parent)
  updateFn    // (currentState, action) => nextOptimisticState
);

React는 비동기 작업이 완료되면(성공하든 예외가 발생하든) optimisticState를 자동으로 state로 되돌립니다. 수동으로 롤백을 수행할 필요가 없습니다.

예시: Todo List

function TodoList({ todos }: { todos: Todo[] }) {
  const [optimisticTodos, addOptimisticTodo] = useOptimistic(
    todos,
    (state, text: string) => [
      ...state,
      { id: crypto.randomUUID(), text, completed: false, pending: true }
    ]
  );

  async function formAction(formData: FormData) {
    addOptimisticTodo(formData.get("todo") as string);
    await saveTodo(formData.get("todo") as string);
  }

  return (
    <>
      {optimisticTodos.map(todo => (
        <div key={todo.id}>
          {todo.text}
        </div>
      ))}

      <form onSubmit={e => {
        e.preventDefault();
        const data = new FormData(e.currentTarget);
        formAction(data);
      }}>
        <input name="todo" />
        <button type="submit">Add</button>
      </form>
    </>
  );
}

예시: 좋아요 토글

const [optimistic, dispatch] = useOptimistic(
  { liked, count },
  (current) => ({
    liked: !current.liked,
    count: current.liked ? current.count - 1 : current.count + 1,
  })
);

async function handleLike() {
  dispatch(null);               // Instant toggle
  const result = await toggleLike(postId);
  setLike(result);
}

실패 처리

useOptimistic는 비동기 작업이 throw될 때만 롤백합니다. 조용한 실패는 낙관적 상태를 그대로 남겨두게 됩니다.

나쁜 패턴 – throw 없음

async function save(data) {
  const json = await fetch(...).then(r => r.json());
  if (!json.success) return json.error; // returns, doesn't throw
}

좋은 패턴 – 오류 시 throw

async function save(data) {
  const res = await fetch(...);
  if (!res.ok) throw new Error(`Failed: ${res.status}`);
  return res.json();
}

오류 처리를 포함한 예시

async function submitComment(formData: FormData) {
  const text = formData.get("comment") as string;
  setError(null);
  addOptimisticComment(text);

  try {
    const comment = await postComment(postId, text);
    setComments(prev => [...prev, comment]);
  } catch {
    setError("Failed to post. Try again.");
    // useOptimistic auto-reverts the optimistic entry
  }
}

일반적인 함정

  • 대규모 서버 변경 – 서버 응답이 낙관적 플레이스홀더를 크게 다른 데이터로 교체하면 UI가 깜박일 수 있습니다.
  • 높은 충돌 시나리오 – 협업 편집, 재고 집계, 또는 금융 기록과 같은 경우 빈번한 롤백이 발생할 수 있습니다.
  • 되돌릴 수 없는 작업 – 삭제, 결제, 혹은 전송된 이메일은 낙관적 업데이트를 적용하기 전에 확인해야 합니다.

useOptimistic을 언제 사용할까

  • 추가, 좋아요, 토글, 순서 변경, 소프트 삭제.
  • UI가 결과를 보여줄 때까지의 지연 시간(인식된 지연)이 사용자의 주요 관심사인 모든 CRUD 작업.

useOptimistic은 서버를 더 빠르게 만들지는 않지만, 서버 응답 전에 결과를 보여줌으로써 앱이 더 빠르게 느껴지게 합니다.

Resources

  • React 19 + Next.js Server Actions + optimistic UI – AI SaaS 스타터 키트는 모든 것이 사전 연결되어 제공되어, 긴 설정 과정을 절약해 줍니다.
0 조회
Back to Blog

관련 글

더 보기 »

대화형 풍력 터빈 계산기 만들기

소규모 풍력 에너지는 언제나 한 가지 과제에 직면해 왔습니다: 특정 위치에서 터빈이 생산할 전력을 정확히 추정하는 일입니다. 태양 에너지와 마찬가지로...