학습 Next.js - Day 23 / 전체 복습, 리뷰 삭제 기능

이유승·2024년 10월 13일

Next.js 학습

목록 보기
24/27



1. 리뷰 삭제 기능

  • 지금까지 배운 내용들을 모두 활용하여 구현하는 리뷰 삭제 기능.
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에서 그대로 활용하면 된다.



submit 옵션이 포함된 button 태그를 사용하지 않고, form에서 submit을 실행하게 하는 방법

	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 답변을 참조.

  • 또한 button 태그를 대신에 div 태그 등을 활용해야하는 상황 (디자인적 이유라던지..)에서 유용한 방법이기도 하다.









00. 강의 소개.

profile
프론트엔드 개발자를 준비하고 있습니다.

0개의 댓글