
원문: https://www.epicreact.dev/use-optimistic-to-make-your-app-feel-instant-zvyuv
앱에서 버튼을 클릭하고, UI가 반응하기까지 얼마나 걸리나요? 만약 네트워크 요청이 완료될 때까지 기다린 다음에야 결과를 보여준다면, 사용자는 로딩 스피너만 바라보며 진짜 작동하고 있는 것이 맞는지 의문을 가질지도 모릅니다.
이럴 때 Optimistic UI(낙관적 UI)가 큰 도움이 됩니다. 원리는 간단합니다. 일단 잘 될 거라고 가정하고, UI를 즉시 업데이트한 다음, 서버의 응답을 기다리는 거죠. 문제가 생기면 나중에 되돌릴 수 있습니다. 하지만 대부분의 경우에 아무 문제 없이 잘 작동하기 때문에, 앱이 매우 빠르게 느껴지게 됩니다.
React 19에서는 이런 패턴을 쉽게 구현할 수 있도록 새로운 훅 useOptimistic을 도입했습니다.
useOptimistic이 무엇인가요?useOptimistic은 비동기 작업이 진행되는 동안 임시 상태를 화면에 보여줄 수 있는 React 훅입니다. 이 훅은 사용자가 폼을 제출할 때 리스트에 새로운 항목을 즉시 추가하거나, 저장되기 전에 항목을 변경할 수 있도록 해줄 때 유용합니다. 또한 사용자가 수행한 작업이 처리 중이라는 피드백을 제공하고, 진행 상황의 각 단계를 UI에 표시하는 데도 사용할 수 있습니다. 예를 들면 "좌석 예약 중" → "캘린더 이벤트 생성 중" → "초대장 발송 중" → "완료" 처럼 말이죠.
직접 예시를 통해 살펴볼까요?
아래는 "좋아요" 버튼을 구현한 예시입니다. 사용자가 버튼을 클릭하자마자 숫자가 바로 증가합니다.
import { useState, useOptimistic, startTransition } from "react";
function LikeButton({ initialCount, sendLike }) {
const [count, setCount] = useState(initialCount);
// 업데이트 함수는 optimistic delta를 count에 더합니다
const [optimisticCount, addOptimisticLike] = useOptimistic(
count,
(current, delta) => current + delta
);
function handleLike() {
// 낙관적으로 count를 증가시킵니다
addOptimisticLike(1);
// 실제로는 전달받은 비동기 함수로 서버에 요청을 보냅니다
startTransition(async () => {
await sendLike();
setCount((c) => c + 1);
});
}
return <button onClick={handleLike}>👍 {optimisticCount}</button>;
}
이렇게 하면 사용자는 네트워크가 느리더라도 좋아요 수가 즉시 증가하는 것을 볼 수 있습니다.
댓글 기능에서도 낙관적 UI를 활용할 수 있습니다. 사용자가 댓글을 입력하고 제출하면, 서버의 응답을 받기 전에라도 댓글이 화면에 즉시 표시되는 것을 원할 수 있습니다.
useOptimistic을 사용해서 아래처럼 구현할 수 있습니다.
import { useState, useOptimistic, startTransition } from "react";
function CommentSection({ initialComments, sendComment }) {
const [comments, setComments] = useState(initialComments);
// 업데이트 함수는 낙관적 댓글을 기존 목록에 병합합니다.
const [optimisticComments, addOptimisticComment] = useOptimistic(
comments,
(currentComments, newComment) => [
{ ...newComment, optimistic: true },
...currentComments,
]
);
async function handleAddComment(text) {
// 댓글을 즉시 화면에 보여줍니다
addOptimisticComment({ id: Date.now(), text });
// 실제로는 전달받은 비동기 함수로 서버에 요청을 보냅니다
startTransition(async () => {
const savedComment = await sendComment(text);
setComments((prev) => [{ ...savedComment }, ...prev]);
});
}
return (
<div>
<form
action={async (formData) => {
handleAddComment(formData.get("text"));
}}
>
<input name="text" placeholder="Write a comment..." />
<button type="submit">Add</button>
</form>
<ul>
{optimisticComments.map((comment) => (
<li
key={comment.id}
className={comment.optimistic ? "text-gray-500" : ""}
>
{comment.text}
</li>
))}
</ul>
</div>
);
}
서버 응답을 받기 전까지 새 댓글은 회색 텍스트로 즉시 화면에 표시됩니다. 에러가 발생하면, 트랜지션이 끝날 때 그 댓글은 사라집니다 (원한다면 에러 메시지를 표시할 수도 있습니다). 에러가 없다면, 낙관적 댓글은 트랜지션이 끝날 때 사라지지만, 새로 생성된 실제 댓글은 일반 댓글 목록에 추가되고, 더 이상 회색으로 표시되지 않습니다.
사용자가 항공편을 예약할 수 있도록 하고, "좌석 예약 중", "결제 처리 중", "확인서 전송 중"과 같은 여러 단계를 통해 진행 상황을 표시하고 싶다고 가정해봅시다. 이때 예약 상태와 진행 메시지 모두에 대해 낙관적 UI 업데이트를 제공할 수 있습니다. 비동기 로직과 단계별 메시지를 모두 프로퍼티로 받으면, 컴포넌트가 UI 상태관리에 집중하면서도 유연하게 만들 수 있습니다.
import { useOptimistic, startTransition } from "react";
import { reserveSeat, processPayment, sendConfirmation } from "./api";
function BookFlight() {
const [message, setMessage] = useOptimistic("Ready to book");
async function handleBooking(formData: FormData) {
setMessage("Reserving seat...");
await reserveSeat(formData.get("flight"));
setMessage("Processing payment...");
await processPayment(formData.get("passenger"));
setMessage("Sending confirmation...");
const bookingId = await sendConfirmation(
formData.get("passenger"),
formData.get("flight")
);
setMessage("Booking complete! Redirecting...");
// 실제 앱에서는 여기에서 라우팅 라이브러리를 사용해 이동합니다
console.log(`Redirecting to /booking/${bookingId}`);
}
return (
<form action={handleBooking}>
<input name="passenger" placeholder="Passenger Name" required />
<input name="flight" placeholder="Flight Number" required />
<button type="submit">Book Flight</button>
<div className="mt-2">
<strong>Status:</strong> {message}
</div>
</form>
);
}
이렇게 하면 모든 비즈니스 로직은 컴포넌트 바깥에 두면서도, 사용자에게 단계별로 매끄러운 피드백을 주는 UI를 제공할 수 있습니다.
useState를 쓰면 안 되나요?이런 의문이 들 수 있습니다. "그냥 상태를 직접 업데이트하면 되는 거 아닌가요?" 그럴 수도 있지만, useOptimistic은 React의 동시 렌더링(Concurrent Rendering)과 트랜지션(Transition) 기능에 최적화되어 있습니다. 단순한 상태 업데이트만으로는 구현하기 어려운 정교한 UI 동기화를 도와주며, 작업 실패 시 자동 롤백도 쉽게 처리할 수 있습니다.
또한 React 트랜지션의 특성상, 트랜지션 안에서 setState를 호출해도 즉시 리렌더링이 발생하지 않을 수 있습니다.
useOptimistic 사용 팁useOptimistic을 startTransition과 함께 사용하면, 비동기 작업 중에도 UI가 끊김 없이 반응성 있게 유지됩니다.낙관적 UI는 앱을 훨씬 빠르고 부드럽게 느껴지게 해주는 작지만 강력한 기법입니다. useOptimistic을 사용하면, 네트워크가 느릴 때조차도 React를 통해 즉각 반응하는 인터페이스를 쉽게 구현할 수 있습니다.
폼, 좋아요 버튼, 댓글, 예약 등 서버와 통신하는 어떤 기능이든, 다음에 구현할 땐 useOptimistic을 꼭 사용해보세요. 사용자들이 확실히 그 차이를 느낄 겁니다.
I really appreciate the time and effort you’ve put into. Thank you for sharing your expertise! PerYourHealth
훅을 활용해, 서버 응답을 기다리지 않고 사용자 인터페이스(UI)를 빠르게 업데이트하는 앱을 만들어보세요. https://www.mybalancenow.it.com
React 19's useOptimistic feature looks truly fantastic! The instant UI response significantly improves the user experience. This is especially helpful on slow connections. I'd definitely consider using this approach when designing a fast-paced game interface like basketball legends . I think anyone who tries it will notice the difference. Feel is as important as speed in the app.
@retro bowl college Very useful and easy to understand article, helped me understand how to create smoother UI with useOptimistic.
Mythic+ dungeon carry in World of Warcraft is a boosting service where professional or highly skilled players help you complete Mythic+ dungeons quickly and without the usual stress of pug groups. These carries are popular because high keystones can be very challenging but are the best source of gear, rating, and weekly rewards.
좋은글 감사합니다~