
Next서버에게 '페이지 경로'에 해당하는 페이지를 재검증(다시 생성)할 것을 요청하는 메서드
import { revalidatePath } from 'next/cache'
주의할 점
1. 오직 서버측에서만 호출 가능한 메서드
2. 경로에 해당하는 페이지 전체를 재검증하기 때문에 해당 페이지의 모든 캐시가 무효화된다
3. 메서드가 호출되면 풀라우트 캐시까지도 함께 삭제된다
(새롭게 생성된 페이지의 캐시를 풀라우트 캐시에 다시 저장해주지는 않는다)
이런식으로 동작하는 이유는?
재접속시 무조건 최신의 데이터를 보장하기 위해서
revalidatePath의 작동방식
특정 경로의 모든 동적 페이지를 재검증
특정 레이아웃을 갖는 모든 페이지 재검증
모든 데이터를 재검증
특정 태그와 연관된 캐시 데이터를 재검증
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 : 현재 서버액션이 실행중인지 아닌지를 의미하는 값
삭제기능을 하는 div 태그를 클라이언트 페이지로 분리하기
review-item-delete-button.tsx → 'use client'
리뷰를 삭제하는 서버액션 파일 생성하기
delete-review.action.ts → 'use server'
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}`,
};
}
}