NextJS - 서버 액션

김명원·2025년 3월 21일

learnNextjs

목록 보기
22/24

서버 액션

서버 액션이란?

브라우저에서 호출할 수 있는 서버에서 실행되는 비동기 함수 입니다.
그래서 서버 액션을 활용하면 별도의 API를 만들 필요 없이 간단한 함수 하나만으로도 직접 호출 할 수 있습니다.

함수 최상단에 'use server'를 적으면

기존의 API를 통해서만 잔행했어야 하는 브라우저와 서버 간의 데이터 통신을 오직 자바스크립트 함수 하나만으로 쉽고 간결하게 설정할 수 있습니다.

예를들어 리뷰를 작성하는 form이 있다고 하면 그 리뷰를 서버로 전송하기 위해 비동기 함수를 만들어 함수 최상단에 'use server'를 작성하면 별도의 API를 만들 필요 없이 직접 호출할 수 있는 것입니다.

function ReviewDeitor() {
  async function createReviewaction(FormData: FormData) {
    "use server";
  }
  return (
    <section>
      <form action={createReviewaction}>
        <input name="content" placeholder="리뷰 내용" />
        <input name="author" placeholder="작성자" />
        <button type="submit">작성하기</button>
      </form>
    </section>
  );
}

그러면 폼 데이터로 전달받은 값을 서버 액션 내부에서 직접 꺼내서 활용하는 방법은
formData에서 get메서드를 통해 가져오면 됩니다. 하지만 가져오는 값의 타입이 formDataEntryValue 이거나 null 타입으로 추론이 됩니다. 이때 formDataEntryValue 타입은 스트링이나 파일 타입을 의미합니다. 지금처럼 스트링 타입의 값을 전달받고 있는 상황에서 적절하지 않기에 값이 있을 경우에만 toString 메서드를 호출하면 됩니다.

function ReviewDeitor() {
  async function createReviewaction(formData: FormData) {
    "use server";

    const content = formData.get("content")?.toString();
    const author = formData.get("author")?.toString();
  }

리뷰 추가 기능은?

서버 액션을 통해 전달받은 값을 이용해서 실제로 데이터베이스에 추가하는 기능을 만들어줘야 합니다.

api를 호출하면서 적절한 값을 받은 후 데이터베이스에 저장할 수 있습니다.
이럴때는 코드가 복잡해지니 actions이라는 폴더를 따로 만들어주고 create-review.action.ts 파일명으로 보관을 해주면 가독성이 더 좋아집니다.

"use server";
export async function createReviewaction(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;
  }

  try {
    const response = await fetch(
      `${process.env.NEXT_PUBLIC_API_SERVER_URL}/review`,
      { method: "POST", body: JSON.stringify({ bookId, content, author }) }
    );
    console.log(JSON.stringify({ bookId, content, author }));

    console.log(response.status);
  } catch (err) {
    console.log(err);
    return;
  }
}
function ReviewDeitor({ bookId }: { bookId: string }) {
  return (
    <section>
      <form action={createReviewaction}>
        <input name="bookId" value={bookId} hidden readOnly />
        <input required name="content" placeholder="리뷰 내용" />
        <input required name="author" placeholder="작성자" />
        <button type="submit">작성하기</button>
      </form>
    </section>
  );
}

또한 form 태그에서 id를 보내야 하면 input 태그에 value 값에 bookId를 전달하고 hidden과 readOnly를 이용해서 전달하면 사용자에게는 보이지 않고 전달이 가능합니다.

재검증 방식의 revalidatePath 메서드

만약 리뷰를 추가하는 과정을 만들었다고 가정을 해보면

"use server";

import { revalidatePath } from "next/cache";

export async function createReviewaction(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;
  }

  try {
    const response = await fetch(
      `${process.env.NEXT_PUBLIC_API_SERVER_URL}/review`,
      { method: "POST", body: JSON.stringify({ bookId, content, author }) }
    );

    console.log(response.status);
 
  } catch (err) {
    console.log(err);
    return;
  }
}
import style from "./review-editor.module.css";
import { createReviewaction } from "@/actions/create-review.action";

export function ReviewDeitor({ bookId }: { bookId: string }) {
  return (
    <section>
      <form action={createReviewaction} className={style.from_container}>
        <input name="bookId" value={bookId} hidden readOnly />
        <textarea required name="content" placeholder="리뷰 내용" />
        <div className={style.submit_container}>
          <input required name="author" placeholder="작성자" />
          <button type="submit">작성하기</button>
        </div>
      </form>
    </section>
  );
}

이상태에서 작성하기 버튼을 눌러도 새로고침을 눌러야만 화면에 리뷰가 생깁니다.
사용자 편의 개선을 위해서 새로고침을 누르지 않아도 바로 리뷰가 작성되게 보이는 메서드가 바로 revalidatePath()메서드 입니다. 이 메서드를 통해 리뷰를 작성하자마자 바로 나타나게 됩니다.

"use server";

import { revalidatePath } from "next/cache";

export async function createReviewaction(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;
  }

  try {
    const response = await fetch(
      `${process.env.NEXT_PUBLIC_API_SERVER_URL}/review`,
      { method: "POST", body: JSON.stringify({ bookId, content, author }) }
    );

    console.log(response.status);
    revalidatePath(`/book/${bookId}`); // revalidatePath(주소)
  } catch (err) {
    console.log(err);
    return;
  }
}

하지만 revalidatePath 메서드가 호출되면 데이터 캐시뿐만아니라 풀 라우트 캐시까지도 삭제가 됩니다.

만약 기존의 데이터를 백엔드 서버로 불러오는 과정이 위와 실행이되고
빌드 타임이 종료가 된고


revaludatePath 메서드가 호출이 되서 재검증하라는 요청이 들어오면, 풀 라우트 캐시와 데이터 캐시를 모두 퍼지(제거)해 버리게 됩니다. 그 후 다시 백엔드 서버로 부터 불러온 데이터를 데이터 캐시에 저장하게 되고 브라우저에게 응답해주고 됩니다.

하지만 이때에는 풀라우트 캐시에 데이터가 업데이트가 안됩니다. 다음번에 추가적인 요청이 들어왔을때 그때 풀라우트 캐시에 저장이 되게 됩니다.

다양한 재검증 방식

revalidatePath(실제 브라우저 경로)

위에서 했던거와 같이 특정 주소의 해당하는 페이지만 재검증

revalidatePath(폴더 경로, 'page')

폴더 경로를 갖는 모든 동적 page들이 전부 재검증

revalidatePath(기준이 되는 레이아웃 파일이 위치한 경로, 'layout')

해당 파일의 레이아웃을 갖는 모든 페이지 재검증

revalidatePath('/', 'layout')

모든 데이터 재검증

reavludateTag('tag')

태그 기준, 데이터 캐시 재검증

tags 옵션이란? 태그를 통해서 데이터 캐시를 초기화한다거나 또는 재검증 시키도록 설정하는 옵션입니다.

오직 태그 값만 캐시 재검증을 할 수 있다면 경제적이고 효율적인 방법입니다.

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

예를들어 버튼을 클릭 했는데 멈춰있는 시간이 있다면 이게 클릭이 완료가 된 것인지 로딩이 된 것인지 몰라서 여러번 클릭하여 중복 제출이 될 수 있는 불편함이 있습니다.

import style from "./review-editor.module.css";
import { createReviewaction } from "@/actions/create-review.action";

export function ReviewDeitor({ bookId }: { bookId: string }) {
  return (
    <section>
      <form action={createReviewaction} className={style.from_container}>
        <input name="bookId" value={bookId} hidden readOnly />
        <textarea required name="content" placeholder="리뷰 내용" />
        <div className={style.submit_container}>
          <input required name="author" placeholder="작성자" />
          <button type="submit">작성하기</button>
        </div>
      </form>
    </section>
  );
}

이런 코드가 있다면 클라이언트 컴포넌트로 바꾼 후 reactHooks를 사용해서 로딩 상태를 설정하고 중복 제출을 막아야 합니다.

이럴때 사용하면 좋은 것이 useActionState 리액트 훅입니다.
2개의 인수를 전달 받습니다.
첫번째 인수에 핸들링하려는 폼에 액션 함수를 넣어야합니다.
두번째 인수에는 폼의 상태의 초기 값을 넣어주면 됩니다.
그러면 배열값으로 3개의 인자를 반환하게 됩니다.

첫번째 값으로는 폼의 state가, 두번째 값으로는 action을 의미하는 formAction, 세번째 값으로는 현재 폼의 로딩 상태를 의미하는 isPending이 반환이 됩니다.

from 상태에 action값으로 formAction을 전달하면 됩니다.

오류가 발생하면 state에 오류값이 저장되게 됩니다.
그러면 그전에 오류의 값을 확인하던 return 문에 객체 형태로

{
  status:false // 오류가 없을때는 true
  error: "오류 내용" // 오류가 없다면 빈값 ""
}

을 전달해주면 됩니다.
예시

if (!content || !author || !bookId) {
  return {
    status: false,
    error: "리뷰 내용과 작성자를 입력해주세요",
  };
}

그러면 실패한 값이나 성공한 값이 state에 담기게 됩니다.

추가로 서버액션에서 첫번째 값으로 state를 받기 때문에 state를 파라미터를 추가해야 합니다.

그렇다면 사용은??

isPending은 로딩 상태를 나타내게 됩니다. 그렇기에 isPending이 true 라면 아직 서버 액션이 종료가 되지 않았다는것을 의미하는 것이니 그때 로딩 UI를 표시해주면 됩니다.
그래서 로딩 중일때는 원하는 것이 안나타나길 원하면 disable 옵션을 사용하면 됩니다.
예시

<section>
  <form action={formAction} className={style.from_container}>
    <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} type="submit">
        {isPending ? "..." : "작성하기"}
      </button>
    </div>
  </form>
</section>

사용자에게 까지 오류 내용을 나타내기 위해서

useEffect(() => {
  if (state && !state.status) {
    alert(state.error);
  }
}, [state]);

useEffect를 사용해서 state의 값이 변할때마다 파악해서 alert 메서드로 나타내면 됩니다.

profile
개발자가 되고 싶은 정치학도생의 기술 블로그

0개의 댓글