4 분 소요


1. 서버 액션 개요

1.1 서버 액션의 개념

서버 액션은 Next.js 13.4에서 도입된 기능으로, 클라이언트에서 발생한 이벤트나 요청을 서버에서 처리하는 비동기 함수이다.

  • 서버에서 데이터베이스 작업, 파일 시스템 접근 등 복잡한 서버 로직을 클라이언트에서 쉽게 실행할 수 있다.
  • 클라이언트는 단순히 서버 액션을 호출하고, 서버가 반환한 데이터를 받는 형태로 동작한다.


1.2 서버 액션의 작동 원리

  1. 클라이언트에서 서버 액션 호출: 클라이언트 컴포넌트에서 직접 서버 액션을 호출한다.
  2. 서버에서 액션 실행: 서버는 액션을 처리하여 비즈니스 로직을 실행한다.
  3. 서버에서 결과 반환 및 UI 업데이트: 서버는 처리 결과를 클라이언트로 반환하고, UI가 업데이트된다.ㄴ


1.3 서버 액션의 장점

서버 사이드 로직의 분리

  • 서버 액션은 비즈니스 로직을 서버에서 처리하고, 클라이언트는 단순히 데이터를 요청하는 방식이다.
  • 이를 통해 서버와 클라이언트의 역할을 명확히 구분할 수 있어, 코드가 더 깔끔하고 유지보수가 용이해진다.


성능 최적화

  • 서버에서 데이터를 처리하고 응답을 보내는 방식은 네트워크 요청 횟수를 줄이고 데이터 처리 부담을 서버에 맡긴다
  • 이는 초기 로딩 시간을 단축시키고 클라이언트 성능을 개선하는 데 도움이 된다.


보안 강화

  • 서버 액션은 서버에서 민감한 로직이나 데이터를 처리하므로, 클라이언트에서 직접적으로 중요한 정보를 다루지 않게 된다.
  • 서버 사이드에서만 데이터가 처리되어 보안상 더 안전한 환경을 제공한다.


1.4 서버 액션의 활용

폼 처리

  • 클라이언트에서 폼을 제출하면 서버 액션을 통해 데이터베이스에 저장하거나 파일을 업로드
  • ex: 회원 가입, 주문 처리.


클라이언트에서 데이터를 가져오는 대신 서버에서 데이터 처리 후 전달

  • 데이터를 서버에서 직접 처리하고 클라이언트에 전달
  • ex: 서버 사이드 렌더링(SSR), 초기 페이지 로딩


보안이 중요한 작업 처리

  • 클라이언트에서 민감한 정보 처리 없이 서버에서만 처리하고 결과만 클라이언트에 전달. (클라이언트는 단순히 결과를 받는다.)
  • ex: 로그인, 결제 처리


1.4 데이터 패칭 vs 서버 액션

  • 데이터 패칭
    • 클라이언트가 서버에서 데이터만 요청하여 가져온다.
    • 주로 GET 요청을 사용하여 데이터를 읽기 전용으로 가져오는 데 사용된다.
  • 서버 액션
    • 서버에서 로직 처리 후 결과를 클라이언트에 전달한다.
    • POST, PUT, DELETE 등을 사용하여 데이터 변경 작업을 처리한다.

💡 데이터 패칭은 데이터를 읽기 위해 클라이언트가 서버에서 요청하는 방식이고, 서버 액션은 서버에서 로직을 처리하고 데이터를 변경하는 작업을 클라이언트에 전달하는 방식이다.
⚠️ GET요청은 서버 액션에서 사용할 수 없다! GET 요청은 단순한 데이터 조회를 위해 주로 클라이언트에서 사용되고, Next.js 서버 액션에서는 POST, PUT, DELETE와 같은 데이터 변경 작업만 처리할 수 있기 때문이다.



2. 서버 액션 사용하기

서버 액션을 사용하려면, 먼저 서버 액션을 정의하고, 그 후 클라이언트에서 호출하여 데이터를 처리하면 된다.

2.1 서버 액션 정의하기

"use server"라는 지시어를 함수 최상단에 추가하면 해당 함수가 서버에서 실행되도록 할 수 있다.

// app/actions/todo-actions.ts
"use server";

import { Todo } from "../types/todo-types";

const baseURL = process.env.NEXT_PUBLIC_SERVER_URL;

export const addTodo = async (todo: Todo) => {
  const response = await fetch(`${baseURL}/todos`, {
    method: "post",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(todo),
  });
  const data = await response.json();

  return data;
};
  • 위 코드에서 addTodo라는 함수가 서버에서 실행되며, 할 일을 POST 방식으로 서버에 전송하고 결과를 반환한다.
  • 클라이언트에서 이 서버 액션을 호출하여 사용자가 리스트를 가져올 수 있다.


2.2 클라이언트에서 서버 액션 호출하기

클라이언트는 서버 액션을 호출하기만 하고, 그에 대한 응답을 받아 처리한다.

mutation을 사용하여 서버 액션(addTodo)을 클라이언트에서 호출해주었다.

export const useAddTodoMutation = () => {
  const queryClient = useQueryClient();

  // Add Todo Mutation
  const { mutate: addTodoMutate } = useMutation({
    mutationFn: addTodo,
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: [QUERY_KEYS.TODOS] });
      alert("추가 완료!");
    },
    onError: (error) => {
      console.error("추가 실패:", error);
      alert("추가 실패. 다시 시도해 주세요.");
    },
  });

  return { addTodoMutate };
};


이제 addTodoMutate를 통해 서버 액션을 호출할 수 있다.

"use client";

import { useAddTodoMutation } from "@/app/hooks/query/useTodosMutation";
import { useId } from "react";

const TodoForm = () => {
  // 추가
  const { addTodoMutate } = useAddTodoMutation();

  const handleOnSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();

    const form = e.currentTarget;
    const formData = new FormData(e.currentTarget);
    const title = formData.get("title") as string;
    const content = formData.get("content") as string;

    const nextTodo = {
      id: crypto.randomUUID(),
      title,
      content,
      isDone: false,
      deadline: new Date().toLocaleDateString(),
    };

    addTodoMutate(nextTodo);

    form.reset();
  };

  const titleId = useId();
  const contentId = useId();

  return (
    <div>
      <form onSubmit={handleOnSubmit}>
        <label htmlFor={titleId}>Title</label>
        <input type="text" id={titleId} name="title" required />
        <label htmlFor={contentId}>content</label>
        <input type="text" id={contentId} name="content" required />
        <button type="submit">추가하기</button>
      </form>
    </div>
  );
};

export default TodoForm;

서버액션 사용 전

서버 액션 사용 후



3. 클라이언트와 서버 로직을 같은 컴포넌트 안에서 처리하기

서버 로직을 클라이언트 컴포넌트 안에서 함께 작성할 수 있다.

3.1 동작 원리

Server Actions을 사용하면 클라이언트에서 action 속성으로 서버 코드가 포함된 함수를 호출할 수 있다.

  • 이때 서버 코드는 자동으로 서버에서 실행된다.
  • 즉, 클라이언트 컴포넌트에서 서버 코드를 직접 작성하고, 이를 서버에서만 실행되도록 할 수 있다는 것이다.


기존 방식 (클라이언트와 서버 분리)

  • 기존 방식에서는 클라이언트 컴포넌트에서 폼을 제출하면, 서버로 API 요청을 보내서 데이터를 처리한다.
  • 예를 들어, 클라이언트에서 서버에 POST 요청을 보내서 데이터를 저장한다고 가정해보자.
  1. 클라이언트 컴포넌트에서 서버 API로 요청
  2. 서버에서 요청을 받아 DB에 저장

💡 하지만, 서버 액션을 사용하면 별도로 API를 작성할 필요 없이, 클라이언트에서 서버 로직을 바로 처리할 수 있다.


서버 액션을 사용하여 폼 제출을 통해 서버에서 DB에 데이터를 저장하는 예시

// app/write2/page.js
import { connectDB } from "@/util/database"; // DB 연결 유틸 함수

export default async function Write2() {
  // 서버 액션 함수 (서버 코드)
  async function handleSubmit(formData) {
    "use server"; // 서버 코드임을 표시
    const db = (await connectDB).db("forum");
    await db
      .collection("post_test")
      .insertOne({ title: formData.get("post1") });
  }

  // 폼 구성
  return (
    <form action={handleSubmit}>
      <input type="text" name="post1" />
      <button type="submit">Submit</button>
    </form>
  );
}
  • 클라이언트 컴포넌트: UI와 사용자의 입력을 받는 역할을 한다.
  • 서버 코드: 폼 제출 시 데이터를 처리하고 DB에 저장하는 작업을 서버에서만 실행한다.

💡 이렇게 서버와 클라이언트 로직이 같은 파일 안에서 처리할 수 있다. 즉, 별도로 API 파일을 작성할 필요 없이, 서버와 클라이언트 로직을 하나의 컴포넌트에서 동시에 관리할 수 있는 것이다.



참조


카테고리:

업데이트:

댓글남기기