React 19 useOptimistic: 서버를 기다리지 않고 즉시 UI 구축
Source: Dev.to
React 19: useOptimistic – 서버 응답을 기다리지 않고 즉시 UI를 만들기
React 19에서는 **useOptimistic**이라는 새로운 훅을 도입했습니다. 이 훅은 서버와의 비동기 통신이 진행되는 동안에도 사용자가 즉시 피드백을 받을 수 있게 해 주어, 더 부드럽고 반응성이 뛰어난 사용자 경험을 제공합니다.
왜 useOptimistic이 필요한가?
전통적인 UI 업데이트 흐름은 다음과 같습니다.
- 사용자가 액션을 트리거한다. (예: 버튼 클릭)
- 클라이언트가 서버에 요청을 보낸다.
- 서버가 응답을 반환한다.
- 응답을 받은 뒤 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 내부 동작 원리
- 낙관적 상태 저장: React는 현재 상태와 낙관적 업데이트 함수를 기억합니다.
- 동시성 제어: 여러 낙관적 업데이트가 동시에 발생해도 순서를 보장합니다.
- 자동 롤백:
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 스타터 키트는 모든 것이 사전 연결되어 제공되어, 긴 설정 과정을 절약해 줍니다.