useOptimistic를 사용하여 고도로 반응성이 높은 UI 구축
Olivia Novak
Dev Intern · Leapcell

소개: 즉각적인 피드백으로 사용자 경험 향상
빠르게 변화하는 웹 애플리케이션 세계에서 사용자 경험은 최고입니다. 원활하고 반응성이 뛰어난 인터페이스는 더 이상 사치가 아니라 기본적인 기대치입니다. 그러나 서버 측 처리가 필요한 데이터와 상호 작용할 때 발생하는 가장 일반적인 마찰 지점 중 하나입니다. 좋아요를 누르거나, 장바구니에 항목을 추가하거나, 양식을 제출하는 것을 생각해 보세요. UI가 서버로부터 확인을 기다리는 동안 짧은 지연은 전반적인 사용자 경험을 저해하는 느린 인식을 초래할 수 있습니다. 이것이 바로 "낙관적 업데이트" 개념이 빛을 발하는 곳입니다. 서버를 기다리는 대신, 작업이 성공할 것이라고 가정하고 UI를 즉시 낙관적으로 업데이트합니다. 실패하면 변경 사항을 깔끔하게 되돌립니다. 이 접근 방식은 반응성과 사용자 만족도를 극적으로 향상시킵니다. React의 새로운 useOptimistic 훅은 이러한 낙관적 업데이트를 구현하는 강력하고 우아한 방법을 제공하며, 이 글에서는 진정한 즉각적인 느낌의 UI를 구축하기 위해 이를 활용하는 방법을 깊이 파고들 것입니다.
낙관적 업데이트 및 useOptimistic 훅 이해
실제 구현에 들어가기 전에 몇 가지 핵심 개념을 명확히 하겠습니다.
낙관적 업데이트: 언급했듯이 낙관적 업데이트는 사용자의 상호 작용 시 서버가 성공을 확인하기 전에 작업의 시각적 효과가 즉시 적용되는 UI 패턴입니다. 이를 통해 사용자에게 즉각적인 피드백을 제공하여 애플리케이션이 더 빠르고 반응성이 뛰어나게 느껴집니다. 서버에서 결국 오류를 반환하면 UI가 이전 상태로 되돌아갑니다.
대기 상태: 이는 낙관적 업데이트가 적용된 후 서버가 응답하기 전 UI의 임시 상태를 나타냅니다. 이 기간 동안 UI는 작업의 예상 결과를 반영합니다.
되돌리기 메커니즘: 낙관적 업데이트의 중요한 부분은 서버 작업이 실패할 경우 UI를 원래 상태로 되돌릴 수 있는 기능입니다. 이를 통해 데이터 일관성을 보장하고 오해의 소지가 있는 정보를 방지합니다.
React 18(현재 실험 API)에 도입된 useOptimistic 훅은 이러한 낙관적 업데이트를 용이하게 하도록 특별히 설계되었습니다. 이를 통해 현재 실제 상태와 낙관적으로 업데이트된 상태의 두 가지 상태를 유지할 수 있습니다. 낙관적 업데이트를 트리거하면 useOptimistic은 현재 상태와 의도된 작업을 기반으로 "대기 상태"를 계산하는 방법을 제공하며, 이는 즉시 UI에 반영됩니다. 백그라운드 작업이 완료되면 실제 상태가 업데이트되고 useOptimistic이 대기 상태를 자동으로 해결합니다.
간단한 댓글 섹션에서 사용자가 댓글에 "좋아요"를 누를 수 있는 실용적인 예시를 통해 이를 설명해 보겠습니다.
import React, { useState, useOptimistic } from 'react'; interface Comment { id: string; text: string; likes: number; likedByUser: boolean; } // API 호출 시뮬레이션 const likeCommentApi = async (commentId: string, isLiking: boolean): Promise<Comment> => { return new Promise((resolve, reject) => { setTimeout(() => { if (Math.random() > 0.1) { // 90% 성공률 resolve({ id: commentId, text: 'This is a sample comment.', likes: isLiking ? 101 : 100, // 실제 좋아요 수 시뮬레이션 likedByUser: isLiking, }); } else { reject(new Error('Failed to like/unlike comment.')); } }, 500); // 네트워크 지연 시뮬레이션 }); }; function CommentSection() { const initialComment: Comment = { id: 'c1', text: 'This is a sample comment.', likes: 100, likedByUser: false, }; const [comment, setComment] = useState<Comment>(initialComment); // useOptimistic는 [optimisticState, setOptimisticState]를 반환합니다. // optimisticState는 낙관적인 업데이트가 적용된 현재 상태입니다. // setOptimisticState는 낙관적인 업데이트를 트리거하는 데 사용됩니다. const [optimisticComment, addOptimisticComment] = useOptimistic( comment, (currentComment, newLikedState: boolean) => { // 이 함수는 대기 상태를 결정합니다. // currentComment는 실제 상태이고 newLikedState는 addOptimisticComment의 페이로드입니다. return { ...currentComment, likes: newLikedState ? currentComment.likes + 1 : currentComment.likes - 1, likedByUser: newLikedState, }; } ); const handleLike = async () => { const newLikedState = !comment.likedByUser; // UI를 즉시 낙관적으로 업데이트합니다 addOptimisticComment(newLikedState); try { const updatedComment = await likeCommentApi(comment.id, newLikedState); // API 호출이 성공하면 실제 상태를 업데이트합니다. // 그러면 낙관적 상태가 자동으로 해결됩니다. setComment(updatedComment); } catch (error) { console.error('API Error:', error); // API 호출이 실패하면 UI가 자동으로 되돌려집니다. // 오류로 setComment를 호출하지 않았기 때문입니다. // 더 복잡한 되돌리기 또는 특정 오류 메시지를 위해 // 더 정교한 오류 상태 관리가 필요할 수 있습니다. alert('Failed to update like status. Please try again.'); // 여기서 일반적인 패턴은 실제 상태를 낙관적 업데이트 이전 상태로 *되돌려* UI를 되돌리는 것일 수 있습니다. // 그러나 낙관적 업데이트는 실패 후 setComment가 호출되지 않으면 이를 암묵적으로 처리합니다. // `setComment`가 새 데이터와 함께 호출되지 않으면 `optimisticComment`는 자연스럽게 `comment`로 회귀합니다. } }; return ( <div style={{ padding: '20px', border: '1px solid #ccc', borderRadius: '8px' }}> <p>{optimisticComment.text}</p> <p>Likes: {optimisticComment.likes}</p> <button onClick={handleLike} disabled={false}> {optimisticComment.likedByUser ? 'Unlike' : 'Like'} </button> {optimisticComment.likedByUser !== comment.likedByUser && ( <span style={{ marginLeft: '10px', color: 'gray' }}> (Updating...)</span> )} </div> ); } // App.tsx 또는 유사한 파일에서: // function App() { // return <CommentSection />; // }
이 예시에서:
useState를 사용하여comment상태를 초기화합니다. 이것이 우리의 실제 진실의 원천입니다.- 그런 다음 
comment상태와 리듀서 함수로useOptimistic을 초기화합니다.- 리듀서 
(currentComment, newLikedState)는 현재 실제 상태(currentComment)와addOptimisticComment에 전달된 페이로드(newLikedState)를 받습니다. - 즉시 표시될 새 낙관적 상태(
optimisticComment)를 반환합니다. 여기서는 좋아요 수를 늘리거나 줄이고likedByUser를 토글합니다. 
 - 리듀서 
 handleLike가 호출되면 먼저addOptimisticComment(newLikedState)를 호출합니다. 이렇게 하면 API 호출이 시작되기 전에도optimisticComment가 즉시 업데이트되어 UI가 "좋아요" 상태로 다시 렌더링됩니다.- 그런 다음 
likeCommentApi가 비동기적으로 호출됩니다. - API 호출이 성공하면 
setComment(updatedComment)가 호출됩니다. 이렇게 하면 실제 상태가 업데이트되고useOptimistic이 자동으로 동기화하여optimisticComment가 성공적으로 업데이트된comment를 반영하는지 확인합니다. - API 호출이 실패하면 
setComment가 호출되지 않습니다. 실제comment상태가 변경되지 않았기 때문에optimisticComment는 효과적으로 마지막 안정적인comment상태로 되돌려져 UI가 되돌려집니다. 사용자에게 경고 메시지도 표시됩니다. 
이 패턴은 like 버튼이 매우 빠르게 느껴지도록 하여 UI가 즉시 업데이트됩니다. 사용자는 변경 사항을 보기 위해 네트워크 요청이 완료될 때까지 기다릴 필요가 없습니다.
고려 사항 및 모범 사례
- 오류 처리: 
useOptimistic은 실제 상태가 업데이트되지 않으면 되돌리기를 암묵적으로 처리하지만, 강력한 애플리케이션에는 더 명시적인 오류 처리가 필요합니다. 작업이 실패할 때 사용자에게 명확한 피드백을 제공하세요. - 멱등성: 낙관적 업데이트는 멱등성 작업(초기 적용 이후 결과를 변경하지 않고 여러 번 적용될 수 있는 작업)과 가장 잘 작동합니다. 좋아요/좋아요 취소는 좋은 예입니다.
 - 복잡한 양식: 여러 필드가 있는 복잡한 양식의 경우 
addOptimisticComment에 전체 대기 양식 데이터 모양을 전달하거나 더 정교한 리듀서를 사용할 수 있습니다. - 로딩 표시기: 낙관적 업데이트를 사용하더라도 백그라운드 작업이 진행 중임을 미묘하게 나타내는 것이 좋습니다(예: 희미한 로딩 스피너 또는 "업데이트 중..." 텍스트, 예시 참조). 이렇게 하면 특히 작업이 평소보다 오래 걸리거나 실패할 경우 사용자 기대치를 관리할 수 있습니다.
 - 서버 측 재검증: 성공적인 낙관적 업데이트 후 서버에서 관련 데이터를 재검증하거나 로컬 상태 관리(예: 캐시된 데이터)가 올바르게 업데이트되었는지 확인하여 오래된 데이터 문제를 방지하는 것을 고려하세요.
 
useOptimistic 훅은 전통적으로 복잡한 낙관적 UI 업데이트 관리 작업을 단순화하여 상태 관리 보일러플레이트의 많은 부분을 추상화합니다. 이를 통해 개발자는 유연하고 매력적인 사용자 경험을 제공하는 데 집중할 수 있습니다.
결론: 초고속 인터페이스를 향한 도약
useOptimistic 훅은 고도로 반응성이 뛰어난 React 애플리케이션을 구축하는 데 있어 상당한 진전을 나타냅니다. 개발자가 비교적 쉽게 낙관적 UI 업데이트를 구현할 수 있도록 지원함으로써 사용자에게 속도와 반응성에 대한 인식을 근본적으로 변화시킵니다. 낙관적 업데이트를 채택하는 것, 특히 useOptimistic에서 제공하는 구조화된 접근 방식을 사용하면 즉각적이고 즐거우며 진정으로 사용자를 최우선으로 생각하는 최신 웹 경험을 만드는 데 중요합니다. 이 훅은 사용자 작업과 서버 확인 간의 격차를 해소하여 훨씬 더 원활하고 매력적인 상호 작용 흐름을 가져옵니다.

