[Next.js 챌린지] DAY16 서버액션

정재은·2024년 10월 2일

Next.js 챌린지

목록 보기
15/16
post-thumbnail

#section7

1. 서버액션을 소개합니다

서버 액션이란?

서버에서 실행되는 비동기 함수를 브라우저에서 호출하는 방법

과정
1. 브라우저에서 특정 form에 대한 submit 이벤트 발생
2. 서버에서만 실행되는 함수를 브라우저가 직접 호출
3. 데이터를 FormData 형식으로 전달


적용하기

  1. form 태그 작성하기
function ReviewEditor() {
  return (
    <section>
      <form>
        <input name='content' placeholder='리뷰 내용' />
        <input name='author' placeholder='작성자' />
        <button type='submit'>작성하기</button>
      </form>
    </section>
  );
}

  1. 서버 액션 만들기
function ReviewEditor() {
  async function createReviewAction() {
  'use server';
  }
  
  return (
  ...
  )
}

  1. form 태그에 action 적용하기
<form action={createReviewAction}>
 ...
</form>

→ 서버액션을 만들면 자동으로 API 생성 + 폼태그 제출시 해당 API가 자동 호출



브라우저로부터 전달받은 FormData 사용하기

props를 통해 FormData를 전달받아 사용할 수 있다

async function createReviewAction(formData: FormData) {
  'use server';
  console.log(formData);
}


필요한 데이터 가져오기 get()

async function createReviewAction(formData: FormData) {
  'use server';
  const content = formData.get('content')?.toString()
  const author = formData.get('author')?.toString();
}

?.toString() → 해당 값이 있는 경우에만 get메서드를 호출 + 값을 문자열로 변환



❓서버액션을 사용하는 이유

  1. 코드가 매우 간결함
  2. 오직 서버측에서만 실행되기 때문에 브라우저 측에서는 호출만 할 수 있다 → 보안상 유용



2. 리뷰 추가 기능 구현하기

기존에 사용하던 백엔드 서버의 API를 활용한다

  1. try ~ catch 활용
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;
}

  1. 서버액션 분리작업
    src/actions 폴더 생성
    src/actions/create-review.action.ts 파일 생성

    use server 키워드를 최상단으로 이동
    bookIdFormData에서 추출할 수 있도록 설정
// 📄 src/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) {
    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;
  }
}

  1. 기존파일에서 서버액션 코드 삭제 + 분리해놓은 createReviewAction을 import
// 📄 src/app/book/[id]/page.tsx
import { createReviewAction } from '@/actions/create-review.action';

  1. bookId를 받아오기 위해 form 태그 안에 input 코드 추가 (일종의 트릭)

    hidden → input 태그를 브라우저로 부터 감춰준다
function ReviewEditor({ bookId }: { bookId: string }) {
  return (
    <section>
      <form action={createReviewAction}>
        <input name='bookId' value={bookId} hidden />
        ...

input 태그는 총 3개지만 화면에는 input이 2개만 나타나고
form 제출시 bookId, content, author 3개의 FormData가 확인된다


입력한 데이터는
백엔드 서버 터미널에서 $ npx prisma studiohttp://localhost:5555/ 접속하여 확인할 수 있다


최종 코드

// 📄 src/app/book/[id]/page.tsx
import { notFound } from 'next/navigation';
import style from './page.module.css';
import { createReviewAction } from '@/actions/create-review.action';

export function generateStaticParams() {
  return [{ id: '1' }, { id: '2' }, { id: '3' }];
}

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

  if (!response.ok) {
    if (response.status === 404) {
      notFound();
    }
    return <div>오류가 발생했습니다 ...</div>;
  }

  const book = await response.json();

  const {
    id,
    title,
    subTitle,
    description,
    author,
    publisher,
    coverImgUrl,
  } = book;

  return (
    <section>
      <div
        className={style.cover_img_container}
        style={{ backgroundImage: `url('${coverImgUrl}')` }}
      >
        <img src={coverImgUrl} />
      </div>
      <div className={style.title}>{title}</div>
      <div className={style.subTitle}>{subTitle}</div>
      <div className={style.author}>
        {author} | {publisher}
      </div>
      <div className={style.description}>{description}</div>
    </section>
  );
}

function ReviewEditor({ bookId }: { bookId: string }) {
  return (
    <section>
      <form action={createReviewAction}>
        <input name='bookId' value={bookId} hidden />
        <input required name='content' placeholder='리뷰 내용' />
        <input required 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 bookId={params.id} />
    </div>
  );
}
// 📄 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) {
    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;
  }
}



profile
프론트엔드

0개의 댓글