브라우저에서 호출할 수 있는 서버에서 실행되는 비동기 함수입니다.
서버 액션을 사용하는 이유는 사용하기 간단하기 때문입니다. 서버 액션들은 컴파일 결과, 자동으로 특정한 해시값을 갖는 API로서 설정되기 때문에, 브라우저 측에서 이 서버액션을 호출할 때 request 헤더에 next action이라는 이름으로 현재 호출하고자 하는 서버 액션의 해시값까지 함께 명시됩니다. 우리가 따로 API 경로를 만들거나, JSON 파싱을 하지 않아도 이 모든 걸 Next가 알아서 해준다는 점!
"use server";
import { revalidateTag } from "next/cache";
export async function createReviewAction(
_: any, formData: FormData) {
const bookId = formData.get("bookId")?.toString();
const content = formData.get("content")?.toString();
const author = formData.get("author")?.toString();
...
return (
<div>
<form className={style.form_container} action={formAction}>
<input name="bookId" value={bookId} hidden readOnly />
<textarea disabled={isPending} required name="content" placeholder="리뷰 내용" />
<div className={style.submit_container}>
<input disabled={isPending} required name="author" placeholder="작성자" />
<button disabled={isPending}>{
isPending ? "..." : "작성하기"}</button>
</div>
</form>
</div>
"use server" 코드만 붙이면 서버 액션으로 동작합니다.
formData 형식으로 form 에 있는 값을 간단히 가져올 수 있어요.
form 태그의 action에 함수만 넣어주면 formData를 알아서 가져와 동작합니다.

revalidatePath가 동작하면 풀라우트 캐시와 데이터 캐시를 PURGE(숙청)합니다!
캐시를 없애고, 다시 데이터 캐시를 가져와요. 그리고 다시 경로에 접속하면 그제서야 풀라우트 캐시를 가져와요. revalidate가 어떻게 동작하는지 보았으니 revalidate 종류 5가지를 알아봅시다!
// 1. 특정 주소의 해당하는 페이지만 재검증, 하지만 페이지의 모든 데이터 캐시를 삭제
// revalidatePath(`/book/${bookId}`)
// // 2. 특정 경로의 모든 동적 페이지를 재검증
// revalidatePath('book/[id]', "page");
// // 3. 특정 레이아웃을 갖는 모든 페이지를 재검증
// revalidatePath('/(with-searchbar)', "layout");
// // 4. 모든 데이터 재검증
// revalidatePath("/","layout");
// 5. 태그 기준, 데이터 캐시 재검증
revalidateTag(`review-${bookId}`);
경우에 따라 다르지만, 하나의 캐시만 재검증이 필요할 경우 revalidateTag가 가장 효율적이에요.
클라이언트 컴포넌트에서의 서버액션은 언제 필요할까요? 서버액션이 진행 중일 때, 예시로 리뷰를 Post 중일 때, submit 버튼이 동작하게 되면 리뷰를 중복으로 남기게 되는 버그가 발생할 수 있어요!
이를 방지하기 위해서 Post 중일 때는, 버튼이 동작하지 못하게 만들면 되지 않을까요?
이 때 useActionState 훅을 사용해봅시다!
const [state, formAction, isPending] = useActionState(createReviewAction, null);
useEffect(() => {
if (state && !state.status) {
alert(state.error);
}
}, [state])
return (
<div>
<form className={style.form_container} action={formAction}>
<input name="bookId" value={bookId} hidden readOnly />
<textarea disabled={isPending} required name="content" placeholder="리뷰 내용" />
<div className={style.submit_container}>
<input disabled={isPending} required name="author" placeholder="작성자" />
<button disabled={isPending}>{
isPending ? "..." : "작성하기"}</button>
</div>
</form>
</div>
);
const [state, formAction, isPending] = useActionState(createReviewAction, null);
useActionState의 첫번째 인수는 함수, 서버 액션 함수를 넣습니다. 두번째 인수는 state의 초기값을 넣어줍니다. 그 결과 값은 차례로 state, formAction 이라는 함수, 로딩유무값인 isPending 가 반환됩니다.
export async function createReviewAction(
_: any, formData: FormData) {
const bookId = formData.get("bookId")?.toString();
const content = formData.get("content")?.toString();
const author = formData.get("author")?.toString();
if (!content || !author || !bookId) {
return {
status: false,
error: "리뷰 내용과 작성자를 입력해주세요."
}
}
useActionState를 사용하면, createReviewAction 도 수정이 필요합니다.
첫번째 인수를 설정해야하는데, : any 는 state: any 와 같지만, state 값을 사용하지 않을 것을 나타내기 위해 state 대신 를 사용했어요.
return 값도 변화한 것을 볼 수 있는데, 이제부터는 state를 반환할 수 있기에 객체형태로 state를 반환해줍니다.
useActionState를 이용해서 중복 데이터 전송 방지, 에러 핸들링 등을 처리할 수 있게되었어요!