import { ReviewData } from "@/types";
import style from "./review-item.module.css";
import ReviewItemDeleteButton from "./review-item-delete-button";
export default function ReviewItem({
id,
content,
author,
createdAt,
bookId,
}: ReviewData) {
return (
<div className={style.container}>
<div className={style.author}>{author}</div>
<div className={style.content}>{content}</div>
<div className={style.bottom_container}>
<div className={style.date}>
{new Date(createdAt).toLocaleString()}
</div>
<div className={style.delete_btn}>
<ReviewItemDeleteButton reviewId={id} bookId={bookId} />
</div>
</div>
</div>
);
}
"use client";
import { deleteReviewAction } from "@/actions/delete-review.action";
import { useActionState, useEffect, useRef } from "react";
export default function ReviewItemDeleteButton({
reviewId,
bookId,
}: {
reviewId: number;
bookId: number;
}) {
const formRef = useRef<HTMLFormElement>(null);
const [state, formAction, isPending] = useActionState(
deleteReviewAction,
null
);
useEffect(() => {
if (state && !state.status) {
alert(state.error);
}
}, [state]);
return (
<form ref={formRef} action={formAction}>
<input name="reviewId" value={reviewId} hidden />
<input name="bookId" value={bookId} hidden />
{isPending ? (
<div>...</div>
) : (
<div onClick={() => formRef.current?.requestSubmit()}>
삭제하기
</div>
)}
</form>
);
}
"use server";
import { revalidateTag } from "next/cache";
export async function deleteReviewAction(_: any, formData: FormData) {
const reviewId = formData.get("reviewId")?.toString();
const bookId = formData.get("bookId")?.toString();
if (!reviewId) {
return {
status: false,
error: "삭제할 리뷰가 없습니다",
};
}
try {
const response = await fetch(
`${process.env.NEXT_PUBLIC_API_SERVER_URL}/review/${reviewId}`,
{
method: "DELETE",
}
);
if (!response.ok) {
throw new Error(response.statusText);
}
revalidateTag(`review-${bookId}`);
return {
status: true,
error: "",
};
} catch (err) {
return {
status: false,
error: `리뷰 삭제에 실패했습니다 : ${err}`,
};
}
}
삭제 버튼 자체를 기능을 포함하는 별도의 컴포넌트로 구현하는 방법.
기존 ReviewItem 컴포넌트에서 클라이언트 컴포넌트로써 데이터 페칭 작업을 수행하는 것은 삭제 버튼 뿐이기에, 별도의 '클라이언트 컴포넌트'로 독립시키는 것이 좋다.
ReviewItemDeleteButton 컴포넌트는 ReviewItem 컴포넌트에서 기능 수행에 필요한 reviewId, bookId의 값을 받아온다. 이들은 일전에 배웠던 input 태그의 hidden 옵션을 적용하여 form에서 다루되, 사용자에게 보이지 않도록 한다.
삭제 기능이기에 페칭 메소드에서 method: "DELETE"를 설정해준다.
삭제 기능 또한 리뷰를 삭제하고 업데이트 된 결과를 화면에 바로 렌더링해야한다. 액션 함수 내부에서 revalidateTag 메소드를 사용해서 필요한 캐시만 무효화 되도록 구현한다.
-> 리뷰 생성 기능에서 태그를 사용하고 있으니, 삭제 기능의 revalidateTag에서 그대로 활용하면 된다.
const formRef = useRef<HTMLFormElement>(null);
<form ref={formRef} action={formAction}>
<input name="reviewId" value={reviewId} hidden />
<input name="bookId" value={bookId} hidden />
{isPending ? (
<div>...</div>
) : (
<div onClick={() => formRef.current?.requestSubmit()}>
삭제하기
</div>
)}
</form>
React에서 DOM을 제어하는 useRef Hook을 이용한다.
form 태그를 ref 속성을 이용해서 useRef에서 form을 제어하도록 설정한다.
삭제하기 버튼의 onClick 속성에서 formRef.current?.requestSubmit()으로 해당 div 태그에 submit 기능이 작동하도록 구현한다.
그냥 submit을 사용하면 되는데, 굳이 이렇게 복잡한 방법을 사용하는 까닭은? submit 메소드는 기본적으로 유효성 검사나 이벤트 핸들러 등을 모두 무시하고 강제적으로 form을 제출시키는 문제점이 존재한다.
반면 requestSubmit() 메소드는 비교적 안전하게 submit을 진행시킬 수 있다. React 환경에서 더 권장되는 방법. 자세한 이유는 아래 ChatGPT 답변을 참조.

