학습 Next.js - Day 20 / 서버 액션, 실습

이유승·2024년 10월 11일

Next.js 학습

목록 보기
21/27



1. Next 서버 액션

  • 별도의 API 없이, 간단한 함수 하나만으로 브라우저에서 Next 서버의 함수를 직접 호출할 수 있다.

  • 이 코드는 사용자에게 어떤 양식을 입력받는 form 태그를 반환하고 있다.

  • 양식을 입력하고, submit 버튼이 입력되면.. action 옵션으로 삽입된 saveName 함수가 실행된다.

  • 그런데 'use server'라는 지시자를 입력해주면, 이 함수는 Next 서버에서만 실행되는 서버 액션으로 간주된다.

  • 서버에서만 실행이 가능한, DB 관련 작업들을 실행할 수 있는. Next 서버 액션이 되는 것이다.

  • API를 사용해야 했던 브라우저 - 서버 사이의 데이터를 오가는 작업을 서버 액션만으로 수행할 수 있게 된다.

  • 특정 form이 제출되거나 했을 때, 서버 측에서 실행되는 기능 함수들을 클라이언트에서 실행할 수 있게 된다.



서버 액션 사용해보기

function ReviewEditor() {
  async function createReviewAction(formData: FormData) {
    "use server";

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

    console.log(content, author);
  }

  return (
    <section>
      <form action={createReviewAction}>
        <input name="content" placeholder="리뷰 내용" />
        <input name="author" placeholder="작성자" />
        <button type="submit">작성하기</button>
      </form>
    </section>
  );
}

export default function Page({ params }: { params: { id: string } }) {
  return (
    <div className={style.container}>
      <BookDetail bookId={params.id} />
      <ReviewEditor />
    </div>
  );
}
  • "use server"; 지시자를 사용하여 서버 액션이 적용된 createReviewAction 함수를 작성하였다.

  • 책에 대한 리뷰 내용을 작성하여, Submit을 실행한다. 서버 액션에 의해 createReviewAction 함수에서는 formData Props로 리뷰 데이터들을 받아오게 된다.

  • 서버 액션을 호출하는 HTTP 요청이 서버로 넘어가는데, 이들은 자동으로 특정 해시값을 갖는 API로써 간주된다. 또한 form data의 형식으로 서버로 Payload 형태로 같이 전송된다.
    const content = formData.get("content")?.toString();
    const author = formData.get("author")?.toString();
  • 서버 액션 함수 내부에서 받아온 데이터를 가공한다던지, 데이터베이스 관련 로직을 처리한다던지 등의 작업을 수행할 수 있다.

  • 서버 액션 함수 내부에서의 formData는 FormDataEntryValue라는 고유 타입으로 간주된다. 따라서 TypeScript 환경에서는 데이터의 타입을 올바르게 바꿔야할 수도 있다.
    -> 학습 프로젝트의 경우 String 타입이어야 하므로, toString()함수로 형변환을 실행해주었다.



서버 액션은 왜 사용해야하는가?

  • 일단 코드가 간결하다. 다만 간단한 수준의 기능에는 적합하지만 복잡한 기능들에는 조금 사용하기가 어렵다.

  • API를 이용하는 경우에는 별도의 파일 생성, 요청 처리, 예외 처리 등의 작업들이 필요하다.

  • 또한 사용시 참고. 서버 액션은 서버에서만 실행될 뿐, 클라이언트에서는 호출할 수만 있다. 따라서 보안에서 민감한 데이터 등을 다루는게 유용하게 사용될 수 있다.



2. Next 서버 액션 적용 실습 1 - 리뷰 작성 기능

"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 (!bookId || !content || !author) {
    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.error(err);
    return;
  }
}
  • 당연하게도 서버 액션은 별도의 파일로 분리할 수 있다. 서버 액션이 별도 파일로 분리될 경우에는 "use server"; 지시자는 함수 내부보다는 파일 최상단에 위치하는 것이 좋다.

  • 학습 프로젝트 단계에서는 데이터베이스까지 다루기에는 필요한 패키지, 설정 등이 많아서 진행하기가 어렵다. 대신 백엔드단과 데이터를 주고받은 수준에서 서버 액션의 실습을 진행하려고 한다.

  • 예외 처리를 잊지 말 것! 특히 데이터가 제대로 들어오지 않았거나, 존재하지 않을 경우에는 후속 기능이 동작하지 않아야 한다.

<input required name="content" placeholder="리뷰 내용" />
  • 입력값의 유효성 검사도 필수. input 태그의 경우 required 옵션만 걸어도 프론트단에서 한번 빈값의 입력을 방지할 수 있다.
    -> 유효성 검사는 프론트에서 1차, 2차로 실시하고 백엔드에서도 진행해야한다. 양 쪽에서 검사를 진행한다는 것은 검사의 신뢰성이 높아진다는 뜻.

  • JSON.stringify? 프론트엔드의 객체 데이터를 그대로 백엔드에 넘겨줄 수는 없다. 표준 데이터 형식인 JSON으로 변환해주어야 하고, 또 문자 형태로 직렬화 해줘야 한다.



npx prisma studio

  • 학습 프로젝트에서 사용 가능한 현재 데이터베이스의 내용을 확인할 수 있는 대시보드.



form 태그에 id값을 같이 포함하는 방법.

      <form action={createReviewAction}>
        <input name="bookId" value={bookId} hidden />
        <input required name="content" placeholder="리뷰 내용" />
        <input required name="author" placeholder="작성자" />
        <button type="submit">작성하기</button>
      </form>
  • form + 서버 액션으로 데이터를 다루게 될 때, id 값이 포함되어야 하는 것은 당연한 이야기.

  • 그런데 사용자 입력 UI에 id 값을 입력하게 하는 것은 다소 이상한 방법.

  • 이럴 때에는 hidden 옵션을 적용한 input 태그에 id 값을 value로 넣어 사용할 수 있다.

  • 사용자에게 보이진 않지만, form 내부에 포함되어 있으니 서버 액션에서도 정상적으로 값을 받아 사용할 수 있다.



3. Next 서버 액션 적용 실습 2 - 리뷰 조회 기능

async function ReviewList({ bookId }: { bookId: string }) {
  const response = await fetch(
    `${process.env.NEXT_PUBLIC_API_SERVER_URL}/review/book/${bookId}`
  );

  if (!response.ok) {
    throw new Error(`Review fetch failed : ${response.statusText}`);
  }

  const reviews: ReviewData[] = await response.json();

  return (
    <section>
      {reviews.map((review) => (
        <ReviewItem key={`review-item-${review.id}`} {...review} />
      ))}
    </section>
  );
}

export default function Page({ params }: { params: { id: string } }) {
  return (
    <div className={style.container}>
      <BookDetail bookId={params.id} />
      <ReviewEditor bookId={params.id} />
      <ReviewList bookId={params.id} />
    </div>
  );
}
import { ReviewData } from "@/types";
import style from "./review-item.module.css";

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}>삭제하기</div>
      </div>
    </div>
  );
}
  • id 값을 받아 특정 서적의 리뷰 데이터만 조회한 다음, 화면에 렌더링하는 컴포넌트 ReviewList. 그리고 리뷰 내용이 렌더링될 ReviewItem 컴포넌트.

  • 에러 처리의 경우 throw new Error로 에러를 throw 해주기만 하면, error 핸들링을 위한 에러 컴포넌트로 자동으로 넘어가진다.

  • 백엔드에서 넘어오는 데이터는 json() 메소드로 변환해주는 것이 좋다. 어떤 형식으로 넘어올지 모르기 때문.









00. 강의 소개.

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

0개의 댓글