"use client";
import { startTransition, useOptimistic } from "react";
import { favoriteArticle, unfavoriteArticle } from "@/actions/article";
import { FaHeart } from "react-icons/fa";
import { FaRegHeart } from "react-icons/fa";
import { useUser } from "@/hooks/useUser";
import { useRouter } from "next/navigation";
interface FavoriteButtonProps {
slug: string;
favorited: boolean;
favoritesCount: number;
}
export default function FavoriteButton({
slug,
favorited,
favoritesCount,
}: FavoriteButtonProps) {
// 낙관적 업데이트를 위한 상태
const [optimisticFavorite, updateOptimisticFavorite] = useOptimistic(
{ favorited, favoritesCount },
(state, newFavorited: boolean) => ({
favorited: newFavorited,
favoritesCount: newFavorited
? state.favoritesCount + 1
: state.favoritesCount - 1,
})
);
const { isLoggedIn } = useUser();
const router = useRouter();
const handleFavorite = async () => {
if (!isLoggedIn) {
const goToLogin = window.confirm(
"로그인 후 이용가능합니다. 로그인 하러 가시겠습니까?"
);
if (goToLogin) {
router.push("/login");
}
return;
}
// 낙관적 업데이트
startTransition(() => {
updateOptimisticFavorite(!optimisticFavorite.favorited);
});
// 서버 액션 호출
try {
if (optimisticFavorite.favorited) {
await unfavoriteArticle(slug);
} else {
await favoriteArticle(slug);
}
} catch (error) {
// 에러 발생 시 원래 상태로 되돌리기
startTransition(() => {
updateOptimisticFavorite(optimisticFavorite.favorited);
});
console.error("Failed to update favorite status", error);
}
};
return (
<button
className={`ml-4 px-3 py-1 rounded-full text-sm flex items-center gap-1 transition-colors
${
optimisticFavorite.favorited
? "bg-brand-primary text-white hover:bg-brand-secondary"
: "border border-brand-primary text-brand-primary hover:bg-brand-primary hover:text-white"
}`}
type="button"
onClick={handleFavorite}
>
{optimisticFavorite.favorited ? (
<FaHeart className="text-red-500" />
) : (
<FaRegHeart />
)}
<span>{optimisticFavorite.favoritesCount}</span>
</button>
);
}
사용자가 게시물에 heart를 누를 수 있게 하는 컴포넌트이다
상위 컴포넌트가 ssr을 할 수 있게 꼭 필요한 부분만 따로 떼어내서 클라이언트 컴포넌트로 만들었다.
낙관적 업데이트를 사용하기 위해
reactQuery나 swr만을 사용해서
어떻게 구현해야할까 고민했는데
마침 딱 좋은 함수가 있어서 가져왔다.
useOptimistic hook인데 이 훅은 낙관적 업데이트를 쉽게 처리하게 해준다
import { useOptimistic } from 'react';
function AppContainer() {
const [optimisticState, addOptimistic] = useOptimistic(
state,
// updateFn
(currentState, optimisticValue) => {
// merge and return new state
// with optimistic value
}
);
}
https://ko.react.dev/reference/react/useOptimistic
매개변수
state: 작업이 대기 중이지 않을 때 초기에 반환될 값입니다.
updateFn(currentState, optimisticValue): 현재 상태와 addOptimistic에 전달된 낙관적인 값을 취하는 함수로, 결과적인 낙관적인 상태를 반환합니다. 순수 함수여야 합니다. updateFn은 두 개의 매개변수를 취합니다. currentState와 optimisticValue. 반환 값은 currentState와 optimisticValue의 병합된 값입니다.
반환값
optimisticState: 결과적인 낙관적인 상태입니다. 작업이 대기 중이지 않을 때는 state와 동일하며, 그렇지 않은 경우 updateFn에서 반환된 값과 동일합니다.
addOptimistic: addOptimistic는 낙관적인 업데이트가 있을 때 호출하는 dispatch 함수입니다. 어떠한 타입의 optimisticValue라는 하나의 인자를 취하며, state와 optimisticValue로 updateFn을 호출합니다.
단순히 설명하면
const [optimisticFavorite, updateOptimisticFavorite] = useOptimistic(
{ favorited, favoritesCount },
(state, newFavorited: boolean) => ({
favorited: newFavorited,
favoritesCount: newFavorited
? state.favoritesCount + 1
: state.favoritesCount - 1,
})
);
const { isLoggedIn } = useUser();
const router = useRouter();
const handleFavorite = async () => {
if (!isLoggedIn) {
const goToLogin = window.confirm(
"로그인 후 이용가능합니다. 로그인 하러 가시겠습니까?"
);
if (goToLogin) {
router.push("/login");
}
return;
}
// 낙관적 업데이트
startTransition(() => {
updateOptimisticFavorite(!optimisticFavorite.favorited);
});
// 서버 액션 호출
try {
if (optimisticFavorite.favorited) {
await unfavoriteArticle(slug);
} else {
await favoriteArticle(slug);
}
} catch (error) {
// 에러 발생 시 원래 상태로 되돌리기
startTransition(() => {
updateOptimisticFavorite(optimisticFavorite.favorited);
});
console.error("Failed to update favorite status", error);
}
};
초기값과, 어떻게 미리 변경 할지 알려주는 코드를 넣어두면
실제로 서버에 전송하는 액션이 동작하지 않아도
updateOptimisticFavorite 이 함수를 호출하면
optimisticFavorite의 값을 바꿔준다는 것이다.
좀 재미있는 함수인 것 같다.
주의해야할 점은
An optimistic state update occurred outside a transition or action. To fix, move the update to an action, or wrap with startTransition.
Source
components/Home/ArticleList.tsx (34:5) @ handleFavorite
32 | const handleFavorite = async (slug: string, favorited: boolean) => {
33 | // 낙관적 업데이트
> 34 | updateOptimisticArticles({ slug, favorited: !favorited });
| ^
35 |
36 | // 서버 액션 호출
37 | try {
onClick
이런 에러가 뜨는데
useOptimistic은 React의 트랜지션이나 액션 내에서만 업데이트해야 합니다.
한다는 에러인데
action은 말 그대로 server action이고
트랜지션은
startTransition(https://ko.react.dev/reference/react/startTransition)으로 영역을 만들어 줄 수 있다.
startTransition을 사용하면:
React에게 이 상태 업데이트가 즉각적인 반응을 필요로 하지 않는 '전환(transition)' 작업임을 알립니다.
이 업데이트를 낮은 우선순위로 처리하여, 다른 중요한 작업(예: 사용자 입력 반응)이 방해받지 않도록 합니다.
큰 상태 변경이 있어도 UI의 반응성을 유지할 수 있습니다.
이런 이점을 가져다 주기 때문에 사용하라고 하는 것 같다.
'개발 > NEXTJS' 카테고리의 다른 글
| api router에서 params 가져오는 법 (0) | 2025.02.26 |
|---|---|
| 상대 경로와 절대 경로 (0) | 2025.02.26 |
| "Bearer Token Authentication" 방식에서 쿠키 기반 인증 시스템으로 마이그레이션 (0) | 2025.02.24 |
| nextjs에서 사용자 정보를 swr과 zustand를 사용하여 관리하기 (0) | 2025.02.23 |
| nextjs 쿠키 옵션 설정 (1) | 2025.02.16 |