본문 바로가기
Frontend2026년 1월 5일7분 읽기

React 19 useOptimistic 낙관적 UI 업데이트 패턴

YS
김영삼
조회 479

React 19 useOptimistic 훅

낙관적 UI 업데이트는 서버 응답을 기다리지 않고 즉시 UI를 변경하는 패턴입니다. React 19에서 도입된 useOptimistic 훅으로 이 패턴을 간단하게 구현할 수 있습니다.

기본 사용법

import { useOptimistic } from 'react';

function TodoList({ todos, addTodoAction }) {
  const [optimisticTodos, addOptimisticTodo] = useOptimistic(
    todos,
    (currentTodos, newTodo) => [
      ...currentTodos,
      { ...newTodo, pending: true }
    ]
  );

  async function handleSubmit(formData) {
    const title = formData.get('title');
    const newTodo = { id: Date.now(), title, completed: false };

    addOptimisticTodo(newTodo);
    await addTodoAction(newTodo);
  }

  return (
    <div>
      <form action={handleSubmit}>
        <input name="title" />
        <button type="submit">추가</button>
      </form>
      <ul>
        {optimisticTodos.map(todo => (
          <li key={todo.id} style={{
            opacity: todo.pending ? 0.6 : 1
          }}>
            {todo.title}
            {todo.pending && <span> (저장 중...)</span>}
          </li>
        ))}
      </ul>
    </div>
  );
}

좋아요 토글 구현

function LikeButton({ postId, initialLiked, initialCount }) {
  const [liked, setLiked] = useState(initialLiked);
  const [count, setCount] = useState(initialCount);

  const [optimisticLiked, toggleOptimisticLike] = useOptimistic(
    liked,
    (current) => !current
  );

  const [optimisticCount, updateOptimisticCount] = useOptimistic(
    count,
    (current, delta) => current + delta
  );

  async function handleLike() {
    const newLiked = !optimisticLiked;
    toggleOptimisticLike(null);
    updateOptimisticCount(newLiked ? 1 : -1);

    try {
      const result = await toggleLikeAction(postId);
      setLiked(result.liked);
      setCount(result.count);
    } catch (error) {
      console.error('좋아요 처리 실패:', error);
    }
  }

  return (
    <button onClick={handleLike}>
      {optimisticLiked ? '❤️' : '🤍'} {optimisticCount}
    </button>
  );
}

메시지 전송 패턴

function ChatRoom({ messages, sendMessageAction }) {
  const [optimisticMessages, addOptimisticMessage] = useOptimistic(
    messages,
    (state, newMessage) => [
      ...state,
      {
        ...newMessage,
        status: 'sending',
        timestamp: new Date().toISOString()
      }
    ]
  );

  async function handleSend(formData) {
    const content = formData.get('message');
    const tempMessage = {
      id: crypto.randomUUID(),
      content,
      sender: 'me'
    };

    addOptimisticMessage(tempMessage);

    try {
      await sendMessageAction(tempMessage);
    } catch (e) {
      toast.error('메시지 전송 실패');
    }
  }

  return (
    <div className="chat">
      {optimisticMessages.map(msg => (
        <div key={msg.id} className={msg.status || 'sent'}>
          {msg.content}
          {msg.status === 'sending' && <Spinner />}
        </div>
      ))}
    </div>
  );
}

useOptimistic vs 기존 패턴 비교

패턴장점단점
useOptimistic (React 19)간결, 자동 롤백, React 통합React 19 필요
수동 상태 관리완전한 제어복잡, 롤백 직접 구현
React Query mutation캐시 통합, 재시도추가 라이브러리 필요

에러 처리 모범 사례

  • useOptimistic의 상태는 action이 완료되면 자동으로 원본 상태로 복원
  • 실패 시 사용자에게 토스트나 알림으로 피드백 제공
  • 재시도 버튼을 제공하여 사용자가 직접 재요청 가능하도록
  • 네트워크 상태 감지로 오프라인일 때 큐에 저장 후 나중에 전송
  • pending 상태를 시각적으로 구분하여 사용자 혼란 방지

댓글 0

아직 댓글이 없습니다.
Ctrl+Enter로 등록