본문 바로가기
기술, 개발/nextjs

9차 강의 노트 (서버 액션)

by Jaejin Sim 2025. 8. 28.
반응형

서버 액션(Server Actions)이란?

브라우저(클라이언트)에서 호출할 수 있는 서버에서 실행되는 비동기 함수

  • 별도 API 라우트 생성 없이 서버 측 로직을 직접 처리
  • 'use server' 지시어로 서버 전용 함수임을 명시

핵심 특징

  1. 'use server' 지시어 사용
    • 함수나 파일 상단에 선언하여 서버에서만 실행되도록 지정
  2. 별도 API 라우트 불필요
    • 기존: 컴포넌트 → API 라우트 → 서버 로직
    • 서버 액션: 컴포넌트 → 서버 액션 (직접 호출)
  3. 서버 전용 작업 가능
    • 데이터베이스 접근, 파일 시스템 조작, 환경 변수 사용 등

기본 사용법

function ReviewEditor() {
  async function createReviewAction(formData) { // ✅ formData 매개변수 추가
    'use server'

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

    console.log("server action called", { content, author });
    // 실제 DB 저장 로직 등...
  }

  return (
    <section>
      <form action={createReviewAction}>
        <input name="content" placeholder="리뷰를 작성하세요." />
        <input name="author" placeholder="작성자 이름" />
        <button type="submit">작성하기</button>
      </form>
    </section>
  );
}

왜 사용하는가?

  1. 간결한 개발: 함수 하나로 API 역할을 수행
  2. 직접적인 서버 로직 처리: AJAX → Controller 없이 바로 서버 작업 가능
  3. 타입 안전성: 클라이언트-서버 간 타입 공유 자연스럽게 처리

주의사항

  • App Router 전용 (Pages Router에서는 사용 불가)
  • 폼 액션으로 주로 사용 (<form action={serverAction}>)
  • 클라이언트 컴포넌트에서는 import해서 사용

Hidden Input과 readOnly

<input 
  type="hidden" 
  name="bookId" 
  value={bookId} 
  readOnly // ✅ 필수! 없으면 React 경고 발생
/>

왜 readOnly가 필요한가?

  • React에서 value prop이 있는 input은 제어 컴포넌트로 인식
  • onChange 핸들러 없이 value만 있으면 "uncontrolled to controlled" 경고 발생
  • readOnly 추가하면 경고 해결 (값이 변경되지 않음을 명시)

데이터 재검증 (Revalidation)

revalidatePath

서버 컴포넌트와 서버 액션에서만 사용 가능

async function createReviewAction(formData) {
  'use server'

  // 리뷰 저장 로직...
  revalidatePath(`/book/${bookId}`); // 특정 페이지 재검증
}

  1. 문제 해결
  2. 문제: 리뷰 등록 후 페이지 새로고침해야만 새 리뷰가 보임 해결: revalidatePath 사용하면 자동으로 최신 데이터 반영
  3. 사용 제한사항
    • 서버 컴포넌트에서만 사용 가능
    • 서버 액션에서만 사용 가능
    • 클라이언트 컴포넌트에서는 직접 호출 불가
  4. 특징:
    • 페이지의 모든 캐시를 무효화 (Data Cache, Full Route Cache)
    • force-cache 옵션이 있어도 캐시 삭제됨
    • 페이지 새로고침 시 Full Route Cache 업데이트

다양한 재검증 방식

// 1. 특정 페이지만 재검증
revalidatePath(`/book/${bookId}`);

// 2. 특정 경로의 모든 동적 페이지 재검증
revalidatePath("/book/[id]", "page");

// 3. 특정 레이아웃의 모든 페이지 재검증
revalidatePath("/(with-searchbar)", "layout");

// 4. 모든 데이터 재검증
revalidatePath("/", "layout");

// 5. 태그 기준 데이터 캐시만 재검증 ⭐ 추천
revalidateTag(`review-${bookId}`);

revalidateTag (권장)

더 정밀한 캐시 제어가 가능

// 서버 액션에서
async function createReviewAction(formData) {
  'use server'
  // 리뷰 저장 로직...
  revalidateTag(`review-${bookId}`); // 특정 태그만 재검증
}

// fetch에서 태그 설정
const response = await fetch(
  `${process.env.NEXT_PUBLIC_API_SERVER_URL}/review/book/${bookId}`,
  {
    next: {
      tags: [`review-${bookId}`] // 태그 지정
    }
  }
);

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

useActionState 활용

'use client'

import { useActionState, useEffect } from 'react';

export default function ReviewEditor({ bookId }) {
  const [state, formAction, isPending] = useActionState(createReviewAction, null);

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

  return (
    <section>
      <form action={formAction}>
        <input
          type="hidden"
          name="bookId"
          value={bookId}
          readOnly // ✅ hidden input에는 readOnly 필요
        />
        <textarea
          disabled={isPending}
          required
          name="content"
          placeholder="리뷰를 작성하세요."
        />
        <div>
          <input
            disabled={isPending}
            required
            name="author"
            placeholder="작성자 이름"
          />
          <button disabled={isPending} type="submit">
            {isPending ? "..." : "작성하기"}
          </button>
        </div>
      </form>
    </section>
  );
}

서버 액션 함수 (별도 파일)

'use server'

export async function createReviewAction(prevState, formData) {
  const bookId = formData.get("bookId")?.toString();
  const content = formData.get("content")?.toString();
  const author = formData.get("author")?.toString();

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

  try {
    // 실제 DB 저장 로직
    // await saveReview({ bookId, content, author });

    revalidateTag(`review-${bookId}`);

    return {
      status: true,
      error: ""
    };
  } catch (error) {
    return {
      status: false,
      error: "리뷰 저장에 실패했습니다"
    };
  }
}

핵심 포인트

  1. 서버 액션은 기존 AJAX → API Controller 패턴을 대체
  2. revalidateTag가 가장 정밀한 캐시 제어 방식
  3. useActionState로 로딩 상태와 에러 처리 간편하게 처리
  4. hidden input에는 readOnly 속성 필요
  5. 서버에서만 실행되므로 민감한 로직 안전하게 처리 가능

 

이 링크를 통해 구매하시면 제가 수익을 받을 수 있어요. 🤗

https://inf.run/4oB9v

반응형