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