게시글에 '좋아요' 를 하는 기능의 응답이 3초정도 소요 된다고 가정해보자.
그렇다면 일반적인 코드에서는 해당하는 UI 도 3초뒤에 변경이 된다.
이 때 useOptimistic 를 사용하면 먼저 렌더링 UI를 적용한 후.결과를 피드백 할 수 있다.
낙관적 UI 업데이트는 서버 응답을 기다리지 않고 UI를 즉시 업데이트하는 방식입니다. 사용자의 액션이 성공할 것이라고 가정하고 UI를 미리 업데이트하여 즉각적인 피드백을 제공합니다. 이는 사용자의 대기 시간을 줄여 더욱 빠르고 반응성이 뛰어난 애플리케이션을 만들 수 있도록 도와줍니다.
useOptimistic 훅은 낙관적 UI 업데이트를 위한 상태를 관리하고, 업데이트 함수를 제공하는 React 훅입니다. 이 훅을 사용하면 서버 응답을 기다리지 않고 UI를 업데이트하고, 서버 응답에 따라 UI를 최종적으로 확정할 수 있습니다.
'use client';
function Post({ post, action }) {
return (
<article className="post">
<div className="post-image">
<img src={post.image} alt={post.title} />
</div>
<div className="post-content">
<header>
<div>
<h2>{post.title}</h2>
<p>
Shared by {post.userFirstName} on{' '}
<time dateTime={post.createdAt}>
{formatDate(post.createdAt)}
</time>
</p>
</div>
<div>
<form action={action.bind(null, post.id)} className={post.isLiked ? 'liked' : ''}>
<LikeButton />
</form>
</div>
</header>
<p>{post.content}</p>
</div>
</article>
);
}
export default function Posts({ posts }) {
// 낙관적 업데이트
// useOptimistic 훅을 사용하여 낙관적 UI 업데이트를 위한 상태와 업데이트 함수를 생성..
// posts: 초기 상태 값으로, 게시글 목록을 나타냅니다.
// (prevPosts, updatedPostId) => { ... }: 업데이트 함수로, 상태를 어떻게 변경할지 정의합니다.
// prevPosts: 이전 상태 값 (게시글 목록)
// updatedPostId: 업데이트할 게시글의 ID
const [optimisticPosts, updateOptimisticPosts] = useOptimistic(posts, (prevPosts, updatedPostId) => {
// 업데이트된 인덱스 찾음
// findIndex를 사용하여 이전 게시글 목록(prevPosts)에서 업데이트할 게시글의 인덱스를 찾습니다.
const updatedPostIndex = prevPosts.findIndex(post => post.id === updatedPostId);
// 없으면 업데이트 전 게시글 으로,
if (updatedPostIndex === -1) {
return prevPosts;
}
// 업데이트할 게시글의 복사본을 생성합니다.
const updatedPost = { ...prevPosts[updatedPostIndex] };
// 게시글의 좋아요 수, 상태를 업데이트합니다.
updatedPost.likes = updatedPost.likes + (updatedPost.isLiked ? -1 : 1);
updatedPost.isLiked = !updatedPost.isLiked;
// 이전 게시글 목록의 복사본을 생성합니다.
const newPosts = [...prevPosts];
// 복사본에서 업데이트된 게시글을 새로운 게시글로 교체합니다.
newPosts[updatedPostIndex] = updatedPost;
// 업데이트된 게시글 목록(newPosts)을 반환하여 상태를 업데이트합니다.
return newPosts;
});
if (!optimisticPosts || optimisticPosts.length === 0) {
return <p>There are no posts.</p>;
}
//async 사용시 자연스럽게 use server 사용 안쓰면 use client..
async function updatePost(postId) {
updateOptimisticPosts(postId);
// togglePostLikeStatus -> server action
await togglePostLikeStatus(postId);
}
return (
<ul className="posts">
{optimisticPosts.map((post) => (
<li key={post.id}>
<Post post={post} action={updatePost} />
</li>
))}
</ul>
);
}
에러발생 시. 롤백되는 코드는 따로 작성해야 한다.
async function updatePost(postId) {
updateOptimisticPosts(postId);
try {
await togglePostLikeStatus(postId);
} catch (err) {
updateOptimisticPosts(postId); // 롤백
}
}
향상된 사용자 경험: 서버 응답을 기다리지 않고 UI를 즉시 업데이트하여 사용자에게 빠른 피드백을 제공합니다.
반응성 향상: 사용자 액션에 대한 즉각적인 반응으로 애플리케이션의 반응성을 높입니다.
네트워크 지연 보상: 네트워크 지연으로 인한 사용자 경험 저하를 완화합니다.
간편한 구현: 낙관적 UI 업데이트 로직을 간편하게 구현할 수 있도록 도와줍니다.
데이터 불일치 가능성: 서버 응답이 실패하거나 예상과 다를 경우 UI와 실제 데이터가 불일치할 수 있습니다.
복잡한 로직: 복잡한 상태 변화를 처리해야 하는 경우 낙관적 업데이트 로직이 복잡해질 수 있습니다.
서버 상태 동기화 필요: 서버 응답에 따라 UI를 최종적으로 확정하고, 오류 발생 시 UI를 롤백하는 로직을 구현해야 합니다.
오류 처리: 낙관적 업데이트 이후 서버 응답이 실패할 경우, 사용자에게 적절한 오류 메시지를 표시하고 UI를 롤백하는 등의 오류 처리 로직을 구현해야 합니다.
useOptimistic 는 서버 상태를 자동으로 동기화하지 않는다.. 따라서 롤백 등 후처리가 필요한 코드는 직접 작성해야 하고, 또한 useOptimistic 훅에 익숙하지 않은 사람이 코드를 봤을 때 해석이 가능할까 싶은 생각도 들기도 함..
하지만 작성만 깔끔하게 된다면 사용자 경험이 높은 UI를 구현이 가능할 것 같다.