[Next.js 챌린지] DAY17

정재은·2024년 10월 2일

Next.js 챌린지

목록 보기
16/16
post-thumbnail

#section7

4-5. 재검증 방식

1. revalidatePath('페이지 경로')

Next서버에게 '페이지 경로'에 해당하는 페이지를 재검증(다시 생성)할 것을 요청하는 메서드

import { revalidatePath } from 'next/cache'

주의할 점
1. 오직 서버측에서만 호출 가능한 메서드
2. 경로에 해당하는 페이지 전체를 재검증하기 때문에 해당 페이지의 모든 캐시가 무효화된다
3. 메서드가 호출되면 풀라우트 캐시까지도 함께 삭제된다
(새롭게 생성된 페이지의 캐시를 풀라우트 캐시에 다시 저장해주지는 않는다)

이런식으로 동작하는 이유는?
재접속시 무조건 최신의 데이터를 보장하기 위해서

revalidatePath의 작동방식


2. revalidatePath('폴더 또는 파일의 경로', 'page')

특정 경로의 모든 동적 페이지를 재검증


3. revalidatePath('레이아웃 파일의 경로', 'layout')

특정 레이아웃을 갖는 모든 페이지 재검증


4. revalidatePath('/', 'layout');

모든 데이터를 재검증


5. revalidateTag('태그이름')

특정 태그와 연관된 캐시 데이터를 재검증




6. 클라이언트 컴포넌트에서의 서버액션

useActionState

React19 버전의 Hook
form 태그의 상태를 쉽게 핸들링 해주는 역할을 한다

useActionState(핸들링 하려는 폼의 액션함수, 폼 상태의 초기값)


실습
① useActionState를 import
② useActionState 호출
③ useActionState로 부터 반환받은 formAction을 form태그의 action값으로 설정

import { useActionState } from 'react';

export default function ReviewEditor({ bookId }: { bookId: string }) {
  const [state, formAction, isPending] = useActionState(createReviewAction, null);
  return (
  	<section>
      <form className={style.form_container} action={formAction}>
      ...
  )

⁕ 정리 ⁕
① form이 제출되면 formAction 실행
② useActionState가 인수로 전달한 서버액션(createReviewAction)을 자동으로 실행
③ useActionState가 state, isPending으로 상태까지 알아서 관리

isPending : 현재 서버액션이 실행중인지 아닌지를 의미하는 값




7. 리뷰 삭제 기능 구현하기

form 태그가 아닌 경우(ex.div) 서버액션 사용하기

  1. 삭제기능을 하는 div 태그를 클라이언트 페이지로 분리하기
    review-item-delete-button.tsx → 'use client'

  2. 리뷰를 삭제하는 서버액션 파일 생성하기
    delete-review.action.ts → 'use server'

  3. div 태그를 분리했던 페이지
    div 태그는 attribute를 붙여도 작동하지 않기 때문에 프로그래매틱하게 변형

3-1. formRef 생성

const formRef = useRef<HTMLFormElement>(null)

3-2. form 태그에 formRef 연결
div 태그에 onClick을 사용해 requestSubmit메서드를 호출하도록 설정 → 클릭시 form 태그 강제 제출

<div onClick={()=>formRef.current?.requestSubmit()}>삭제하기</div>

3-3. props로 reviewId, bookId 받아오기

3-4. form 태그에 input 코드 추가

<form>
  <input name='reviewId' value={reviewId} hidden />
  <input name='bookId' value={bookId} hidden />
  ...
</form>

3-5. useActionState 호출

const [state, formAction, isPending] = useActionState(deleteReviewAction, null);

3-6. form 태그에 action={formAction} 적용

3-7. 삼항연산자를 활용하여 현재 서버액션의 상태(isPending)에 따른 반환값 지정

{isPending ? (
  <div>...</div>
) : (
  <div onClick={() => formRef.current?.requestSubmit()}>
    삭제하기
  </div>
)}


코드 정리

클라이언트 페이지로 분리한 삭제기능 파일 (div 태그 분리)

// 📄 src/components/review-item-delete-button.tsx
'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>
  );
}

서버액션 기능을 하는 파일

// 📄 delete-review.action.ts
'use server';

import { revalidateTag } from 'next/cache';

export default async function deleteReviewAction(
  _: any,
  formData: FormData
) {
  const reviewId = formData.get('reviewId')?.toString();
  const bookId = formData.get('bookId')?.toString();

  if (!reviewId || !bookId) {
    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}`,
    };
  }
}
profile
프론트엔드

0개의 댓글